diff --git a/test/integration/awsconfig_test.go b/test/integration/awsconfig_test.go index df406829..e40f7f8e 100644 --- a/test/integration/awsconfig_test.go +++ b/test/integration/awsconfig_test.go @@ -153,5 +153,6 @@ func TestStartNonInteractiveEmitsNoteWhenAWSProfileMissing(t *testing.T) { "start", ) require.NoError(t, err) + requireExitCode(t, 0, err) assert.Contains(t, stdout, "No complete LocalStack AWS profile found") } diff --git a/test/integration/config_test.go b/test/integration/config_test.go index 3d829c29..1522708c 100644 --- a/test/integration/config_test.go +++ b/test/integration/config_test.go @@ -22,6 +22,7 @@ func TestConfigFileCreatedOnStartup(t *testing.T) { e := testEnvWithHome(tmpHome, xdgOverride) _, stderr, err := runLstk(t, testContext(t), workDir, e, "logout") require.NoError(t, err, stderr) + requireExitCode(t, 0, err) expectedConfigFile := filepath.Join(tmpHome, ".config", "lstk", "config.toml") assert.FileExists(t, expectedConfigFile) @@ -36,6 +37,7 @@ func TestConfigFileCreatedOnStartup(t *testing.T) { e := testEnvWithHome(tmpHome, xdgOverride) _, stderr, err := runLstk(t, testContext(t), workDir, e, "logout") require.NoError(t, err, stderr) + requireExitCode(t, 0, err) expectedConfigFile := filepath.Join(expectedOSConfigDir(tmpHome, xdgOverride), "config.toml") assert.FileExists(t, expectedConfigFile) @@ -69,6 +71,7 @@ IAM_SOFT_MODE = "1" ctx := testContext(t) _, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL), "--config", configFile, "start") require.NoError(t, err, "lstk start failed: %s", stderr) + requireExitCode(t, 0, err) inspect, err := dockerClient.ContainerInspect(ctx, containerName) require.NoError(t, err, "failed to inspect container") @@ -81,6 +84,7 @@ func TestConfigFlagOverridesConfigPath(t *testing.T) { stdout, stderr, err := runLstk(t, testContext(t), t.TempDir(), os.Environ(), "--config", customConfig, "config", "path") require.NoError(t, err, stderr) + requireExitCode(t, 0, err) assertSamePath(t, customConfig, stdout) } @@ -98,6 +102,7 @@ func TestLocalConfigTakesPrecedence(t *testing.T) { e := testEnvWithHome(tmpHome, xdgOverride) stdout, stderr, err := runLstk(t, testContext(t), workDir, e, "config", "path") require.NoError(t, err, stderr) + requireExitCode(t, 0, err) expectedLocalPath, err := filepath.Abs(localConfigFile) require.NoError(t, err) @@ -117,6 +122,7 @@ func TestXDGConfigTakesPrecedence(t *testing.T) { e := testEnvWithHome(tmpHome, xdgOverride) stdout, stderr, err := runLstk(t, testContext(t), workDir, e, "config", "path") require.NoError(t, err, stderr) + requireExitCode(t, 0, err) assertSamePath(t, xdgConfigFile, stdout) } @@ -130,6 +136,7 @@ func TestConfigPathCommand(t *testing.T) { e := testEnvWithHome(tmpHome, filepath.Join(tmpHome, "xdg-config-home")) stdout, stderr, err := runLstk(t, testContext(t), workDir, e, "config", "path") require.NoError(t, err, stderr) + requireExitCode(t, 0, err) assertSamePath(t, xdgConfigFile, stdout) } @@ -143,6 +150,7 @@ func TestConfigPathCommandDoesNotCreateConfig(t *testing.T) { e := testEnvWithHome(tmpHome, xdgOverride) stdout, stderr, err := runLstk(t, testContext(t), workDir, e, "config", "path") require.NoError(t, err, stderr) + requireExitCode(t, 0, err) assertSamePath(t, expectedConfigFile, stdout) assert.NoFileExists(t, expectedConfigFile) diff --git a/test/integration/license_test.go b/test/integration/license_test.go index e34b7a10..6aa8a884 100644 --- a/test/integration/license_test.go +++ b/test/integration/license_test.go @@ -65,6 +65,7 @@ func TestLicenseValidationSuccess(t *testing.T) { } require.NoError(t, err, "lstk start failed: %s", stderr) + requireExitCode(t, 0, err) inspect, err := dockerClient.ContainerInspect(ctx, containerName) require.NoError(t, err, "failed to inspect container") @@ -82,6 +83,7 @@ func TestLicenseValidationFailure(t *testing.T) { ctx := testContext(t) _, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL).With(env.AuthToken, "test-token-for-license-validation"), "start") require.Error(t, err, "expected lstk start to fail with forbidden license") + requireExitCode(t, 1, err) assert.Contains(t, stderr, "license validation failed") assert.Contains(t, stderr, "invalid, inactive, or expired") diff --git a/test/integration/login_test.go b/test/integration/login_test.go index dbd5ea9a..0f1787b0 100644 --- a/test/integration/login_test.go +++ b/test/integration/login_test.go @@ -120,6 +120,7 @@ func TestDeviceFlowSuccess(t *testing.T) { out := output.String() require.NoError(t, err, "login should succeed: %s", out) + requireExitCode(t, 0, err) assert.Contains(t, out, "Opening browser to login...") assert.Contains(t, out, "Browser didn't open? Visit") assert.Contains(t, out, "/auth/request/test-auth-req-id?code=TEST123") @@ -176,6 +177,7 @@ func TestDeviceFlowFailure_RequestNotConfirmed(t *testing.T) { out := output.String() require.Error(t, err, "expected login to fail when request not confirmed") + requireExitCode(t, 1, err) assert.Contains(t, out, "Opening browser to login...") assert.Contains(t, out, "Browser didn't open? Visit") assert.Contains(t, out, "/auth/request/test-auth-req-id?code=TEST123") diff --git a/test/integration/logout_test.go b/test/integration/logout_test.go index c3d8e204..6c0d8766 100644 --- a/test/integration/logout_test.go +++ b/test/integration/logout_test.go @@ -19,6 +19,7 @@ func TestLogoutCommandRemovesToken(t *testing.T) { stdout, stderr, err := runLstk(t, testContext(t), "", nil, "logout") require.NoError(t, err, "lstk logout failed: %s", stderr) + requireExitCode(t, 0, err) assert.Contains(t, stdout, "Logged out successfully") _, err = GetAuthTokenFromKeyring() @@ -30,6 +31,7 @@ func TestLogoutCommandSucceedsWhenNoToken(t *testing.T) { stdout, stderr, err := runLstk(t, testContext(t), "", env.Without(env.AuthToken), "logout") require.NoError(t, err, "lstk logout should succeed even with no token: %s", stderr) + requireExitCode(t, 0, err) assert.Contains(t, stdout, "Not currently logged in") } @@ -38,6 +40,7 @@ func TestLogoutCommandWithEnvVarToken(t *testing.T) { stdout, stderr, err := runLstk(t, testContext(t), "", env.Without(env.AuthToken).With(env.AuthToken, "test-env-token"), "logout") require.NoError(t, err, "lstk logout should succeed: %s", stderr) + requireExitCode(t, 0, err) assert.Contains(t, stdout, "LOCALSTACK_AUTH_TOKEN") } @@ -57,5 +60,6 @@ func TestLogoutCommandNotesWhenEmulatorStillRunning(t *testing.T) { stdout, stderr, err := runLstk(t, ctx, "", nil, "logout") require.NoError(t, err, "lstk logout failed: %s", stderr) + requireExitCode(t, 0, err) assert.Contains(t, stdout, "LocalStack is still running in the background") } diff --git a/test/integration/logs_test.go b/test/integration/logs_test.go index ce08427c..70ac1b9a 100644 --- a/test/integration/logs_test.go +++ b/test/integration/logs_test.go @@ -22,6 +22,7 @@ func TestLogsExitsByDefault(t *testing.T) { _, _, err := runLstk(t, ctx, "", nil, "logs") require.NoError(t, err, "lstk logs should exit cleanly when container is running") + requireExitCode(t, 0, err) } func TestLogsCommandFailsWhenNotRunning(t *testing.T) { @@ -31,6 +32,7 @@ func TestLogsCommandFailsWhenNotRunning(t *testing.T) { _, stderr, err := runLstk(t, testContext(t), "", nil, "logs", "--follow") require.Error(t, err, "expected lstk logs --follow to fail when container not running") + requireExitCode(t, 1, err) assert.Contains(t, stderr, "emulator is not running") } diff --git a/test/integration/main_test.go b/test/integration/main_test.go index ebceb5de..12c5f299 100644 --- a/test/integration/main_test.go +++ b/test/integration/main_test.go @@ -237,6 +237,17 @@ func runLstkInPTY(t *testing.T, ctx context.Context, environ []string, args ...s return strings.TrimSpace(out.String()), err } +func requireExitCode(t *testing.T, expected int, err error) { + t.Helper() + if expected == 0 { + require.NoError(t, err) + return + } + var exitErr *exec.ExitError + require.ErrorAs(t, err, &exitErr) + require.Equal(t, expected, exitErr.ExitCode()) +} + func createMockLicenseServer(success bool) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" && r.URL.Path == "/v1/license/request" { diff --git a/test/integration/non_interactive_test.go b/test/integration/non_interactive_test.go index 0aad0210..fcd27739 100644 --- a/test/integration/non_interactive_test.go +++ b/test/integration/non_interactive_test.go @@ -11,6 +11,7 @@ import ( func TestNonInteractiveFlagBlocksLogin(t *testing.T) { out, err := runLstkInPTY(t, testContext(t), nil, "login", "--non-interactive") require.Error(t, err, "expected login --non-interactive to fail") + requireExitCode(t, 1, err) assert.Contains(t, out, "login requires an interactive terminal") } @@ -24,6 +25,7 @@ func TestNonInteractiveFlagFailsWithoutToken(t *testing.T) { out, err := runLstkInPTY(t, testContext(t), env.Without(env.AuthToken).With(env.APIEndpoint, mockServer.URL), "start", "--non-interactive") require.Error(t, err, "expected start --non-interactive to fail with no auth token") + requireExitCode(t, 1, err) assert.Contains(t, out, "authentication required: set LOCALSTACK_AUTH_TOKEN or run in interactive mode") } @@ -37,5 +39,6 @@ func TestRootNonInteractiveFlagFailsWithoutToken(t *testing.T) { out, err := runLstkInPTY(t, testContext(t), env.Without(env.AuthToken).With(env.APIEndpoint, mockServer.URL), "--non-interactive") require.Error(t, err, "expected lstk --non-interactive to fail with no auth token") + requireExitCode(t, 1, err) assert.Contains(t, out, "authentication required: set LOCALSTACK_AUTH_TOKEN or run in interactive mode") } diff --git a/test/integration/start_test.go b/test/integration/start_test.go index 2bf88f6d..51375210 100644 --- a/test/integration/start_test.go +++ b/test/integration/start_test.go @@ -24,6 +24,7 @@ func TestStartCommandSucceedsWithValidToken(t *testing.T) { ctx := testContext(t) _, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL), "start") require.NoError(t, err, "lstk start failed: %s", stderr) + requireExitCode(t, 0, err) inspect, err := dockerClient.ContainerInspect(ctx, containerName) require.NoError(t, err, "failed to inspect container") @@ -47,6 +48,7 @@ func TestStartCommandSucceedsWithKeyringToken(t *testing.T) { // Run without LOCALSTACK_AUTH_TOKEN should use keyring _, stderr, err := runLstk(t, ctx, "", env.Without(env.AuthToken).With(env.APIEndpoint, mockServer.URL), "start") require.NoError(t, err, "lstk start failed: %s", stderr) + requireExitCode(t, 0, err) inspect, err := dockerClient.ContainerInspect(ctx, containerName) require.NoError(t, err, "failed to inspect container") @@ -63,6 +65,7 @@ func TestStartCommandFailsWithInvalidToken(t *testing.T) { _, stderr, err := runLstk(t, testContext(t), "", env.With(env.AuthToken, "invalid-token").With(env.APIEndpoint, mockServer.URL), "start") require.Error(t, err, "expected lstk start to fail with invalid token") + requireExitCode(t, 1, err) assert.Contains(t, stderr, "license validation failed") } @@ -76,6 +79,7 @@ func TestStartCommandDoesNothingWhenAlreadyRunning(t *testing.T) { stdout, stderr, err := runLstk(t, ctx, "", env.With(env.AuthToken, "fake-token"), "start") require.NoError(t, err, "lstk start should succeed when container is already running: %s", stderr) + requireExitCode(t, 0, err) assert.Contains(t, stdout, "already running") } @@ -90,6 +94,7 @@ func TestStartCommandFailsWhenPortInUse(t *testing.T) { stdout, _, err := runLstk(t, testContext(t), "", env.With(env.AuthToken, "fake-token"), "start") require.Error(t, err, "expected lstk start to fail when port is in use") + requireExitCode(t, 1, err) assert.Contains(t, stdout, "Port 4566 already in use") assert.Contains(t, stdout, "LocalStack may already be running.") assert.Contains(t, stdout, "lstk stop") diff --git a/test/integration/stop_test.go b/test/integration/stop_test.go index 737c7510..6ac92b40 100644 --- a/test/integration/stop_test.go +++ b/test/integration/stop_test.go @@ -17,6 +17,7 @@ func TestStopCommandSucceeds(t *testing.T) { stdout, stderr, err := runLstk(t, ctx, "", nil, "stop") require.NoError(t, err, "lstk stop failed: %s", stderr) + requireExitCode(t, 0, err) assert.Contains(t, stdout, "Stopping", "should show stopping message") assert.Contains(t, stdout, "stopped", "should show stopped message") @@ -31,6 +32,7 @@ func TestStopCommandFailsWhenNotRunning(t *testing.T) { _, stderr, err := runLstk(t, testContext(t), "", nil, "stop") require.Error(t, err, "expected lstk stop to fail when container not running") + requireExitCode(t, 1, err) assert.Contains(t, stderr, "is not running") } @@ -44,10 +46,12 @@ func TestStopCommandIsIdempotent(t *testing.T) { _, stderr, err := runLstk(t, ctx, "", nil, "stop") require.NoError(t, err, "first lstk stop failed: %s", stderr) + requireExitCode(t, 0, err) _, err = dockerClient.ContainerInspect(ctx, containerName) require.Error(t, err, "container should not exist after first stop") _, _, err = runLstk(t, ctx, "", nil, "stop") assert.Error(t, err, "second lstk stop should fail since container already removed") + requireExitCode(t, 1, err) } diff --git a/test/integration/telemetry_test.go b/test/integration/telemetry_test.go index 21c8293c..f1dfd0e0 100644 --- a/test/integration/telemetry_test.go +++ b/test/integration/telemetry_test.go @@ -64,6 +64,7 @@ func TestStartCommandSendsTelemetryEvent(t *testing.T) { With(env.AnalyticsEndpoint, analyticsSrv.URL) out, err := cmd.CombinedOutput() require.NoError(t, err, "lstk start failed: %s", out) + requireExitCode(t, 0, err) // The telemetry goroutine is async; wait up to 3s for the event to arrive. select { @@ -106,6 +107,7 @@ func TestStartCommandSucceedsWhenAnalyticsEndpointUnreachable(t *testing.T) { out, err := cmd.CombinedOutput() require.NoError(t, err, "lstk start should succeed even when analytics endpoint is unreachable: %s", out) + requireExitCode(t, 0, err) } func TestStartCommandDoesNotSendTelemetryWhenDisabled(t *testing.T) { @@ -126,6 +128,7 @@ func TestStartCommandDoesNotSendTelemetryWhenDisabled(t *testing.T) { With(env.DisableEvents, "1") out, err := cmd.CombinedOutput() require.NoError(t, err, "lstk start failed: %s", out) + requireExitCode(t, 0, err) // Wait long enough that a goroutine would have fired if enabled. select { diff --git a/test/integration/update_test.go b/test/integration/update_test.go index 25a87be9..c8eea617 100644 --- a/test/integration/update_test.go +++ b/test/integration/update_test.go @@ -17,6 +17,7 @@ func TestUpdateCheckCommand(t *testing.T) { stdout, stderr, err := runLstk(t, ctx, "", nil, "update", "--check") require.NoError(t, err, "lstk update --check failed: %s", stderr) + requireExitCode(t, 0, err) // Dev builds report a note about skipping update check assert.Contains(t, stdout, "Note:", "should show a note (dev build or up-to-date)") @@ -27,6 +28,7 @@ func TestUpdateCheckCommandNonInteractive(t *testing.T) { stdout, stderr, err := runLstk(t, ctx, "", nil, "update", "--check", "--non-interactive") require.NoError(t, err, "lstk update --check --non-interactive failed: %s", stderr) + requireExitCode(t, 0, err) assert.Contains(t, stdout, "Note:", "should show a note in non-interactive mode") } @@ -90,6 +92,7 @@ func TestUpdateNPMLocalInstall(t *testing.T) { stdoutStr := string(stdout) require.NoError(t, err, "lstk update failed: %s", stdoutStr) + requireExitCode(t, 0, err) assert.Contains(t, stdoutStr, "npm (local)", "should detect local npm install") assert.Contains(t, stdoutStr, projectDir, "should show the project directory") assert.Contains(t, stdoutStr, "Updated to", "should complete the update") @@ -127,6 +130,7 @@ func TestUpdateBinaryInPlace(t *testing.T) { updateOut, err := updateCmd.CombinedOutput() updateStr := string(updateOut) require.NoError(t, err, "lstk update failed: %s", updateStr) + requireExitCode(t, 0, err) assert.Contains(t, updateStr, "Update available: 0.0.1", "should detect update") assert.Contains(t, updateStr, "Downloading update", "should download binary") assert.Contains(t, updateStr, "Updated to", "should complete the update") @@ -196,6 +200,7 @@ func TestUpdateHomebrew(t *testing.T) { updateOut, err := updateCmd.CombinedOutput() updateStr := string(updateOut) require.NoError(t, err, "lstk update failed: %s", updateStr) + requireExitCode(t, 0, err) assert.Contains(t, updateStr, "Homebrew", "should detect Homebrew install") assert.Contains(t, updateStr, "brew upgrade", "should mention brew upgrade") }