From bb80e79dfba2fc1baf6f31da220aaa564180d7aa Mon Sep 17 00:00:00 2001 From: carole-lavillonniere Date: Wed, 4 Feb 2026 19:42:33 +0100 Subject: [PATCH] logout command --- cmd/logout.go | 24 ++++++++++++++ internal/auth/auth.go | 12 +++++++ internal/auth/keyring.go | 5 +++ internal/auth/mock_keyring_test.go | 14 ++++++++ test/integration/logout_test.go | 52 ++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+) create mode 100644 cmd/logout.go create mode 100644 test/integration/logout_test.go diff --git a/cmd/logout.go b/cmd/logout.go new file mode 100644 index 0000000..f90721d --- /dev/null +++ b/cmd/logout.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "fmt" + + "github.com/localstack/lstk/internal/auth" + "github.com/spf13/cobra" +) + +var logoutCmd = &cobra.Command{ + Use: "logout", + Short: "Remove stored authentication token", + RunE: func(cmd *cobra.Command, args []string) error { + if err := auth.New().Logout(); err != nil { + return fmt.Errorf("failed to logout: %w", err) + } + fmt.Println("Logged out successfully.") + return nil + }, +} + +func init() { + rootCmd.AddCommand(logoutCmd) +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go index c1fdbe1..8a4cacf 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -2,8 +2,11 @@ package auth import ( "context" + "errors" "log" "os" + + "github.com/zalando/go-keyring" ) type Auth struct { @@ -42,3 +45,12 @@ func (a *Auth) GetToken(ctx context.Context) (string, error) { log.Println("Login successful.") return token, nil } + +// Logout removes the stored auth token from the keyring +func (a *Auth) Logout() error { + err := a.keyring.Delete(keyringService, keyringUser) + if errors.Is(err, keyring.ErrNotFound) { + return nil + } + return err +} diff --git a/internal/auth/keyring.go b/internal/auth/keyring.go index e9eba28..2b601f2 100644 --- a/internal/auth/keyring.go +++ b/internal/auth/keyring.go @@ -12,6 +12,7 @@ const ( type Keyring interface { Get(service, user string) (string, error) Set(service, user, password string) error + Delete(service, user string) error } type systemKeyring struct{} @@ -23,3 +24,7 @@ func (systemKeyring) Get(service, user string) (string, error) { func (systemKeyring) Set(service, user, password string) error { return keyring.Set(service, user, password) } + +func (systemKeyring) Delete(service, user string) error { + return keyring.Delete(service, user) +} diff --git a/internal/auth/mock_keyring_test.go b/internal/auth/mock_keyring_test.go index 36b2b10..d2f4e93 100644 --- a/internal/auth/mock_keyring_test.go +++ b/internal/auth/mock_keyring_test.go @@ -39,6 +39,20 @@ func (m *MockKeyring) EXPECT() *MockKeyringMockRecorder { return m.recorder } +// Delete mocks base method. +func (m *MockKeyring) Delete(service, user string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", service, user) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockKeyringMockRecorder) Delete(service, user any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockKeyring)(nil).Delete), service, user) +} + // Get mocks base method. func (m *MockKeyring) Get(service, user string) (string, error) { m.ctrl.T.Helper() diff --git a/test/integration/logout_test.go b/test/integration/logout_test.go new file mode 100644 index 0000000..cba9115 --- /dev/null +++ b/test/integration/logout_test.go @@ -0,0 +1,52 @@ +package integration_test + +import ( + "context" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zalando/go-keyring" +) + +func TestLogoutCommandRemovesToken(t *testing.T) { + // Clean up any existing token + _ = keyring.Delete(keyringService, keyringUser) + t.Cleanup(func() { + _ = keyring.Delete(keyringService, keyringUser) + }) + + // Store a token in keyring + err := keyring.Set(keyringService, keyringUser, "test-token") + require.NoError(t, err, "failed to store token in keyring") + + // Run logout command + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "../../bin/lstk", "logout") + output, err := cmd.CombinedOutput() + + require.NoError(t, err, "lstk logout failed: %s", output) + assert.Contains(t, string(output), "Logged out successfully") + + // Verify token was removed + _, err = keyring.Get(keyringService, keyringUser) + assert.Error(t, err, "token should be removed from keyring") +} + +func TestLogoutCommandSucceedsWhenNoToken(t *testing.T) { + // Ensure no token exists + _ = keyring.Delete(keyringService, keyringUser) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "../../bin/lstk", "logout") + output, err := cmd.CombinedOutput() + + // Should succeed even if no token + require.NoError(t, err, "lstk logout should succeed even with no token: %s", output) +}