Skip to content
Merged
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
24 changes: 24 additions & 0 deletions cmd/logout.go
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 12 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package auth

import (
"context"
"errors"
"log"
"os"

"github.com/zalando/go-keyring"
)

type Auth struct {
Expand Down Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions internal/auth/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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)
}
14 changes: 14 additions & 0 deletions internal/auth/mock_keyring_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions test/integration/logout_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading