Skip to content

Commit

Permalink
test(authenticate): add validation for authenticate command
Browse files Browse the repository at this point in the history
  • Loading branch information
Integralist committed Sep 12, 2023
1 parent 831dbcd commit 34b99fd
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 4 deletions.
179 changes: 179 additions & 0 deletions pkg/commands/authenticate/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package authenticate_test

import (
"bytes"
"errors"
"fmt"
"io"
"strings"
"testing"
"time"

"github.com/fastly/cli/pkg/app"
"github.com/fastly/cli/pkg/auth"
"github.com/fastly/cli/pkg/config"
"github.com/fastly/cli/pkg/mock"
"github.com/fastly/cli/pkg/testutil"
)

func TestAuth(t *testing.T) {
args := testutil.Args
type ts struct {
testutil.TestScenario

AuthResult *auth.AuthorizationResult
ConfigProfile *config.Profile
Opener func(input string) error
Stdin []string
}
scenarios := []ts{
// User cancels authentication prompt
{
TestScenario: testutil.TestScenario{
Args: args("authenticate"),
WantError: "user cancelled execution",
},
Stdin: []string{
"N", // when prompted to open a web browser to start authentication
},
},
// Error opening web browser
{
TestScenario: testutil.TestScenario{
Args: args("authenticate"),
WantError: "failed to open web browser",
},
Opener: func(input string) error {
return errors.New("failed to open web browser")
},
Stdin: []string{
"Y", // when prompted to open a web browser to start authentication
},
},
// Error processing OAuth flow (error encountered)
{
TestScenario: testutil.TestScenario{
Args: args("authenticate"),
WantError: "failed to authorize: no authorization code returned",
},
AuthResult: &auth.AuthorizationResult{
Err: errors.New("no authorization code returned"),
},
Stdin: []string{
"Y", // when prompted to open a web browser to start authentication
},
},
// Error processing OAuth flow (empty SessionToken field)
{
TestScenario: testutil.TestScenario{
Args: args("authenticate"),
WantError: "failed to authorize: no session token",
},
AuthResult: &auth.AuthorizationResult{
SessionToken: "",
},
Stdin: []string{
"Y", // when prompted to open a web browser to start authentication
},
},
// Success processing OAuth flow
{
TestScenario: testutil.TestScenario{
Args: args("authenticate"),
WantOutput: "Session token (persisted to your local configuration): 123",
},
AuthResult: &auth.AuthorizationResult{
SessionToken: "123",
},
ConfigProfile: &config.Profile{
Token: "123",
},
Stdin: []string{
"Y", // when prompted to open a web browser to start authentication
},
},
}

for testcaseIdx := range scenarios {
testcase := &scenarios[testcaseIdx]
t.Run(testcase.Name, func(t *testing.T) {
var stdout bytes.Buffer
opts := testutil.NewRunOpts(testcase.Args, &stdout)
opts.APIClient = mock.APIClient(testcase.API)

if testcase.AuthResult != nil {
result := make(chan auth.AuthorizationResult)
opts.AuthServer = testutil.MockAuthServer{
Result: result,
}
go func() {
result <- *testcase.AuthResult
}()
}
if testcase.Opener != nil {
opts.Opener = testcase.Opener
}

var err error

if len(testcase.Stdin) > 1 {
// To handle multiple prompt input from the user we need to do some
// coordination around io pipes to mimic the required user behaviour.
stdin, prompt := io.Pipe()
opts.Stdin = stdin

// Wait for user input and write it to the prompt
inputc := make(chan string)
go func() {
for input := range inputc {
fmt.Fprintln(prompt, input)
}
}()

// We need a channel so we wait for `Run()` to complete
done := make(chan bool)

// Call `app.Run()` and wait for response
go func() {
err = app.Run(opts)
done <- true
}()

// User provides input
//
// NOTE: Must provide as much input as is expected to be waited on by `run()`.
// For example, if `run()` calls `input()` twice, then provide two messages.
// Otherwise the select statement will trigger the timeout error.
for _, input := range testcase.Stdin {
inputc <- input
}

select {
case <-done:
// Wait for app.Run() to finish
case <-time.After(10 * time.Second):
t.Fatalf("unexpected timeout waiting for mocked prompt inputs to be processed")
}
} else {
stdin := ""
if len(testcase.Stdin) > 0 {
stdin = testcase.Stdin[0]
}
opts.Stdin = strings.NewReader(stdin)
err = app.Run(opts)
}

if testcase.ConfigProfile != nil {
userProfile := opts.ConfigFile.Profiles["user"]
if userProfile.Token != testcase.ConfigProfile.Token {
t.Errorf("want token: %s, got token: %s", testcase.ConfigProfile.Token, userProfile.Token)
}
}

t.Log(stdout.String())

testutil.AssertErrorContains(t, err, testcase.WantError)
testutil.AssertStringContains(t, stdout.String(), testcase.WantOutput)
})
}
}
7 changes: 6 additions & 1 deletion pkg/commands/authenticate/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package authenticate

import (
"errors"
"fmt"
"io"
"time"
Expand Down Expand Up @@ -107,8 +108,12 @@ func (c *RootCommand) Exec(in io.Reader, out io.Writer) error {

ar := <-c.authServer.GetResult()
if ar.Err != nil || ar.SessionToken == "" {
err := ar.Err
if ar.Err == nil {
err = errors.New("no session token")
}
return fsterr.RemediationError{
Inner: fmt.Errorf("failed to authorize: %w", ar.Err),
Inner: fmt.Errorf("failed to authorize: %w", err),
Remediation: auth.Remediation,
}
}
Expand Down
33 changes: 30 additions & 3 deletions pkg/testutil/args.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package testutil

import (
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"

"github.com/hashicorp/cap/oidc"

"github.com/fastly/cli/pkg/app"
"github.com/fastly/cli/pkg/auth"
"github.com/fastly/cli/pkg/config"
Expand Down Expand Up @@ -54,6 +55,33 @@ func Args(args string) []string {
return s
}

// MockAuthServer is used to no-op the authentication server.
type MockAuthServer struct {
auth.Starter

Result chan auth.AuthorizationResult
}

func (s MockAuthServer) GetResult() chan auth.AuthorizationResult {

Check failure on line 65 in pkg/testutil/args.go

View workflow job for this annotation

GitHub Actions / lint

exported method MockAuthServer.GetResult should have comment or be unexported
return s.Result
}

func (s MockAuthServer) SetAccountEndpoint(_ string) {

Check failure on line 69 in pkg/testutil/args.go

View workflow job for this annotation

GitHub Actions / lint

exported method MockAuthServer.SetAccountEndpoint should have comment or be unexported
// no-op
}

func (s MockAuthServer) SetAPIEndpoint(_ string) {

Check failure on line 73 in pkg/testutil/args.go

View workflow job for this annotation

GitHub Actions / lint

exported method MockAuthServer.SetAPIEndpoint should have comment or be unexported
// no-op
}

func (s MockAuthServer) SetVerifier(_ *oidc.S256Verifier) {

Check failure on line 77 in pkg/testutil/args.go

View workflow job for this annotation

GitHub Actions / lint

exported method MockAuthServer.SetVerifier should have comment or be unexported
// no-op
}

func (s MockAuthServer) Start() error {

Check failure on line 81 in pkg/testutil/args.go

View workflow job for this annotation

GitHub Actions / lint

exported method MockAuthServer.Start should have comment or be unexported
return nil // no-op
}

// NewRunOpts returns a struct that can be used to populate a call to app.Run()
// while the majority of fields will be pre-populated and only those fields
// commonly changed for testing purposes will need to be provided.
Expand All @@ -68,7 +96,7 @@ func NewRunOpts(args []string, stdout io.Writer) app.RunOpts {
ConfigPath: "/dev/null",
Args: args,
APIClient: mock.APIClient(mock.API{}),
AuthServer: &auth.Server{},
AuthServer: &MockAuthServer{},
Env: config.Environment{},
ErrLog: errors.Log,
ConfigFile: config.File{
Expand All @@ -77,7 +105,6 @@ func NewRunOpts(args []string, stdout io.Writer) app.RunOpts {
HTTPClient: &http.Client{Timeout: time.Second * 5},
Manifest: &md,
Opener: func(input string) error {
fmt.Printf("open url: %s\n", input)
return nil // no-op
},
Stdout: stdout,
Expand Down

0 comments on commit 34b99fd

Please sign in to comment.