Skip to content

Commit 3864ac2

Browse files
test(auth): add negative test for keychain access failure
Adds ensureKeychainAccess check before OAuth flow to fail early when keychain is locked. Includes test to verify authorizeGoogle is not called when keychain check fails. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 19d833f commit 3864ac2

2 files changed

Lines changed: 64 additions & 4 deletions

File tree

internal/cmd/auth.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,24 @@ import (
1919
)
2020

2121
var (
22-
openSecretsStore = secrets.OpenDefault
23-
authorizeGoogle = googleauth.Authorize
24-
startManageServer = googleauth.StartManageServer
25-
checkRefreshToken = googleauth.CheckRefreshToken
22+
openSecretsStore = secrets.OpenDefault
23+
authorizeGoogle = googleauth.Authorize
24+
startManageServer = googleauth.StartManageServer
25+
checkRefreshToken = googleauth.CheckRefreshToken
26+
ensureKeychainAccess = defaultEnsureKeychainAccess
2627
)
2728

29+
// defaultEnsureKeychainAccess verifies keychain is accessible before starting OAuth flow.
30+
func defaultEnsureKeychainAccess() error {
31+
store, err := secrets.OpenDefault()
32+
if err != nil {
33+
return fmt.Errorf("keychain access: %w", err)
34+
}
35+
// Trigger a read to verify keychain access
36+
_, _ = store.Keys()
37+
return nil
38+
}
39+
2840
type AuthCmd struct {
2941
Credentials AuthCredentialsCmd `cmd:"" name:"credentials" help:"Store OAuth client credentials"`
3042
Add AuthAddCmd `cmd:"" name:"add" help:"Authorize and store a refresh token"`
@@ -300,6 +312,11 @@ type AuthAddCmd struct {
300312
func (c *AuthAddCmd) Run(ctx context.Context) error {
301313
u := ui.FromContext(ctx)
302314

315+
// Verify keychain access before starting the OAuth flow
316+
if err := ensureKeychainAccess(); err != nil {
317+
return err
318+
}
319+
303320
var services []googleauth.Service
304321
if strings.EqualFold(strings.TrimSpace(c.ServicesCSV), "") || strings.EqualFold(strings.TrimSpace(c.ServicesCSV), "all") {
305322
services = googleauth.AllServices()

internal/cmd/auth_add_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ package cmd
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
7+
"io"
68
"strings"
79
"testing"
810

911
"github.com/steipete/gogcli/internal/googleauth"
1012
"github.com/steipete/gogcli/internal/secrets"
13+
"github.com/steipete/gogcli/internal/ui"
1114
)
1215

1316
func TestAuthAddCmd_JSON(t *testing.T) {
@@ -70,3 +73,43 @@ func TestAuthAddCmd_JSON(t *testing.T) {
7073
t.Fatalf("unexpected token: %#v", tok)
7174
}
7275
}
76+
77+
func TestAuthAddCmd_KeychainError(t *testing.T) {
78+
origAuth := authorizeGoogle
79+
origOpen := openSecretsStore
80+
origKeychain := ensureKeychainAccess
81+
t.Cleanup(func() {
82+
authorizeGoogle = origAuth
83+
openSecretsStore = origOpen
84+
ensureKeychainAccess = origKeychain
85+
})
86+
87+
// Simulate keychain locked error
88+
ensureKeychainAccess = func() error {
89+
return errors.New("keychain is locked")
90+
}
91+
92+
authCalled := false
93+
authorizeGoogle = func(ctx context.Context, opts googleauth.AuthorizeOptions) (string, error) {
94+
authCalled = true
95+
return "rt", nil
96+
}
97+
98+
cmd := &AuthAddCmd{Email: "test@example.com", ServicesCSV: "gmail"}
99+
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
100+
if uiErr != nil {
101+
t.Fatalf("ui.New: %v", uiErr)
102+
}
103+
ctx := ui.WithUI(context.Background(), u)
104+
err := cmd.Run(ctx)
105+
106+
if err == nil {
107+
t.Fatal("expected error when keychain is locked")
108+
}
109+
if !strings.Contains(err.Error(), "keychain") {
110+
t.Errorf("expected error to mention keychain, got: %v", err)
111+
}
112+
if authCalled {
113+
t.Error("authorizeGoogle should not be called when keychain check fails")
114+
}
115+
}

0 commit comments

Comments
 (0)