Skip to content

Commit 8b848e5

Browse files
wesmclaude
andcommitted
Add test for inherited oauth_app token client validation
Table-driven test covers both paths: matching token reused, mismatched token rejected when binding is inherited from DB without explicit --oauth-app flag. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3d30ef9 commit 8b848e5

File tree

1 file changed

+94
-0
lines changed

1 file changed

+94
-0
lines changed

cmd/msgvault/cmd/addaccount_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,100 @@ func TestFindGmailSource(t *testing.T) {
6565
}
6666
}
6767

68+
// TestAddAccount_InheritedBindingValidatesToken verifies that re-running
69+
// add-account without --oauth-app on a named-app account validates the
70+
// token's client_id against the inherited binding.
71+
func TestAddAccount_InheritedBindingValidatesToken(t *testing.T) {
72+
for _, tc := range []struct {
73+
name string
74+
clientID string
75+
wantError bool
76+
}{
77+
{"matching token reused", "test.apps.googleusercontent.com", false},
78+
{"mismatched token rejected", "wrong.apps.googleusercontent.com", true},
79+
} {
80+
t.Run(tc.name, func(t *testing.T) {
81+
tmpDir := t.TempDir()
82+
dbPath := filepath.Join(tmpDir, "msgvault.db")
83+
84+
s, err := store.Open(dbPath)
85+
if err != nil {
86+
t.Fatalf("open store: %v", err)
87+
}
88+
if err := s.InitSchema(); err != nil {
89+
t.Fatalf("init schema: %v", err)
90+
}
91+
source, err := s.GetOrCreateSource("gmail", "user@acme.com")
92+
if err != nil {
93+
t.Fatalf("create source: %v", err)
94+
}
95+
err = s.UpdateSourceOAuthApp(source.ID, sql.NullString{String: "acme", Valid: true})
96+
if err != nil {
97+
t.Fatalf("set oauth_app: %v", err)
98+
}
99+
_ = s.Close()
100+
101+
tokensDir := filepath.Join(tmpDir, "tokens")
102+
if err := os.MkdirAll(tokensDir, 0700); err != nil {
103+
t.Fatalf("mkdir: %v", err)
104+
}
105+
tokenData, _ := json.Marshal(map[string]string{
106+
"access_token": "fake",
107+
"refresh_token": "fake",
108+
"token_type": "Bearer",
109+
"client_id": tc.clientID,
110+
})
111+
if err := os.WriteFile(filepath.Join(tokensDir, "user@acme.com.json"), tokenData, 0600); err != nil {
112+
t.Fatalf("write token: %v", err)
113+
}
114+
115+
secretsPath := filepath.Join(tmpDir, "secret.json")
116+
if err := os.WriteFile(secretsPath, []byte(fakeClientSecrets), 0600); err != nil {
117+
t.Fatalf("write secrets: %v", err)
118+
}
119+
120+
savedCfg, savedLogger, savedOAuthApp := cfg, logger, oauthAppName
121+
defer func() { cfg, logger, oauthAppName = savedCfg, savedLogger, savedOAuthApp }()
122+
123+
cfg = &config.Config{
124+
HomeDir: tmpDir,
125+
Data: config.DataConfig{DataDir: tmpDir},
126+
OAuth: config.OAuthConfig{
127+
Apps: map[string]config.OAuthApp{
128+
"acme": {ClientSecrets: secretsPath},
129+
},
130+
},
131+
}
132+
logger = slog.New(slog.NewTextHandler(os.Stderr, nil))
133+
134+
ctx, cancel := context.WithCancel(context.Background())
135+
cancel()
136+
137+
testCmd := &cobra.Command{
138+
Use: "add-account <email>", Args: cobra.ExactArgs(1),
139+
RunE: addAccountCmd.RunE,
140+
}
141+
testCmd.Flags().StringVar(&oauthAppName, "oauth-app", "", "")
142+
testCmd.Flags().BoolVar(&headless, "headless", false, "")
143+
testCmd.Flags().BoolVar(&forceReauth, "force", false, "")
144+
testCmd.Flags().StringVar(&accountDisplayName, "display-name", "", "")
145+
146+
root := newTestRootCmd()
147+
root.AddCommand(testCmd)
148+
// No --oauth-app flag: binding inherited from DB
149+
root.SetArgs([]string{"add-account", "user@acme.com"})
150+
151+
err = root.ExecuteContext(ctx)
152+
if tc.wantError && err == nil {
153+
t.Fatal("expected error for mismatched token")
154+
}
155+
if !tc.wantError && err != nil {
156+
t.Fatalf("unexpected error: %v", err)
157+
}
158+
})
159+
}
160+
}
161+
68162
// TestAddAccount_RebindWithExistingToken verifies that switching
69163
// OAuth app binding with an existing token updates the binding
70164
// without re-authorizing (headless rebind scenario).

0 commit comments

Comments
 (0)