Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
### Bug Fixes:

- fix(compute/serve): provide default `override_host` to Viceroy via temporary manifest and handle IPv6 hostnames ([#1940](https://github.com/fastly/cli/pull/1940))
- fix(auth): ensure `fastly auth token` output is not corrupted by a version update notice written to stdout ([#1971](https://github.com/fastly/cli/pull/1971))

### Enhancements:

Expand Down
4 changes: 3 additions & 1 deletion pkg/app/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,9 @@ func Exec(data *global.Data) error {
f := checkForUpdates(data.Versioners.CLI, commandName)
defer func() {
if !data.Flags.Quiet && !data.Flags.JSON {
f(data.Output)
// always write update notification to stderr so it doesn't corrupt
// `fastly auth token` which might be piped or used in a command
f(data.ErrOutput)
}
}()

Expand Down
52 changes: 51 additions & 1 deletion pkg/app/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,52 @@ func TestExecJSONLeavesStdoutCleanAndWritesWarningToStderr(t *testing.T) {
}
}

func TestAuthTokenUpdateNoticeWritesToStderr(t *testing.T) {
origVersion := revision.AppVersion
origInit := app.Init
revision.AppVersion = "1.0.0"
t.Cleanup(func() {
revision.AppVersion = origVersion
app.Init = origInit
})

for _, testcase := range []struct {
name string
args string
latest string
wantNotice bool
}{
{name: "update available", args: "auth token", latest: "1.0.1", wantNotice: true},
{name: "quiet", args: "auth token --quiet", latest: "1.0.1"},
{name: "up to date", args: "auth token", latest: "1.0.0"},
} {
t.Run(testcase.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
args := testutil.SplitArgs(testcase.args)
app.Init = func(_ []string, _ io.Reader) (*global.Data, error) {
data := testutil.MockGlobalData(args, &stdout)
data.ErrOutput = &stderr
data.Versioners.CLI = mock.AssetVersioner{AssetVersion: testcase.latest}
return data, nil
}

if err := app.Run(args, nil); err != nil {
t.Fatalf("app.Run returned unexpected error: %v", err)
}
if got := stdout.String(); got != "mock-token" {
t.Errorf("expected only the token on stdout, got: %q", got)
}
if testcase.wantNotice {
if !strings.Contains(stderr.String(), "A new version of the Fastly CLI is available.") {
t.Errorf("expected update notice on stderr, got: %q", stderr.String())
}
} else if stderr.Len() != 0 {
t.Errorf("expected no stderr output, got: %q", stderr.String())
}
})
}
}

// TestStatsJSONSuppressesUpdateNotice verifies that --json and --format=json on
// stats commands suppress the deferred update-check notice, keeping stdout as
// clean JSON. This is the regression test for the timing bug where
Expand All @@ -238,10 +284,11 @@ func TestStatsJSONSuppressesUpdateNotice(t *testing.T) {

for _, flag := range []string{"--json", "--format=json"} {
t.Run(flag, func(t *testing.T) {
var stdout bytes.Buffer
var stdout, stderr bytes.Buffer
args := testutil.SplitArgs("stats aggregate " + flag)
app.Init = func(_ []string, _ io.Reader) (*global.Data, error) {
data := testutil.MockGlobalData(args, &stdout)
data.ErrOutput = &stderr
data.APIClientFactory = mock.APIClient(mock.API{
GetAggregateJSONFn: aggregateOK,
})
Expand All @@ -255,6 +302,9 @@ func TestStatsJSONSuppressesUpdateNotice(t *testing.T) {
if strings.Contains(stdout.String(), "new version") {
t.Errorf("update notice should be suppressed in JSON mode, got: %s", stdout.String())
}
if strings.Contains(stderr.String(), "new version") {
t.Errorf("update notice should be suppressed in JSON mode, got stderr: %s", stderr.String())
}
})
}
}
Expand Down
Loading