@@ -265,6 +265,146 @@ func TestAddAccount_NewRegistrationRejectsMismatchedToken(t *testing.T) {
265265 }
266266}
267267
268+ // TestAddAccount_ExplicitDefaultRejectsMismatchedToken verifies that
269+ // --oauth-app "" rejects a token minted by a different client.
270+ func TestAddAccount_ExplicitDefaultRejectsMismatchedToken (t * testing.T ) {
271+ tmpDir := t .TempDir ()
272+
273+ tokensDir := filepath .Join (tmpDir , "tokens" )
274+ if err := os .MkdirAll (tokensDir , 0700 ); err != nil {
275+ t .Fatalf ("mkdir tokens: %v" , err )
276+ }
277+ // Token with a client_id that does NOT match the default secrets
278+ tokenData , _ := json .Marshal (map [string ]string {
279+ "access_token" : "fake-access" ,
280+ "refresh_token" : "fake-refresh" ,
281+ "token_type" : "Bearer" ,
282+ "client_id" : "wrong-client.apps.googleusercontent.com" ,
283+ })
284+ if err := os .WriteFile (
285+ filepath .Join (tokensDir , "user@example.com.json" ),
286+ tokenData , 0600 ,
287+ ); err != nil {
288+ t .Fatalf ("write token: %v" , err )
289+ }
290+
291+ secretsPath := filepath .Join (tmpDir , "secret.json" )
292+ if err := os .WriteFile (
293+ secretsPath , []byte (fakeClientSecrets ), 0600 ,
294+ ); err != nil {
295+ t .Fatalf ("write secrets: %v" , err )
296+ }
297+
298+ savedCfg := cfg
299+ savedLogger := logger
300+ savedOAuthApp := oauthAppName
301+ defer func () {
302+ cfg = savedCfg
303+ logger = savedLogger
304+ oauthAppName = savedOAuthApp
305+ }()
306+
307+ cfg = & config.Config {
308+ HomeDir : tmpDir ,
309+ Data : config.DataConfig {DataDir : tmpDir },
310+ OAuth : config.OAuthConfig {ClientSecrets : secretsPath },
311+ }
312+ logger = slog .New (slog .NewTextHandler (os .Stderr , nil ))
313+
314+ ctx , cancel := context .WithCancel (context .Background ())
315+ cancel ()
316+
317+ testCmd := & cobra.Command {
318+ Use : "add-account <email>" ,
319+ Args : cobra .ExactArgs (1 ),
320+ RunE : addAccountCmd .RunE ,
321+ }
322+ testCmd .Flags ().StringVar (& oauthAppName , "oauth-app" , "" , "" )
323+ testCmd .Flags ().BoolVar (& headless , "headless" , false , "" )
324+ testCmd .Flags ().BoolVar (& forceReauth , "force" , false , "" )
325+ testCmd .Flags ().StringVar (& accountDisplayName , "display-name" , "" , "" )
326+
327+ root := newTestRootCmd ()
328+ root .AddCommand (testCmd )
329+ root .SetArgs ([]string {
330+ "add-account" , "user@example.com" , "--oauth-app" , "" ,
331+ })
332+
333+ err := root .ExecuteContext (ctx )
334+ if err == nil {
335+ t .Fatal ("expected error: mismatched token should be rejected with explicit --oauth-app \" \" " )
336+ }
337+ }
338+
339+ // TestAddAccount_ExplicitDefaultAcceptsMatchingToken verifies that
340+ // --oauth-app "" accepts a token minted by the default client.
341+ func TestAddAccount_ExplicitDefaultAcceptsMatchingToken (t * testing.T ) {
342+ tmpDir := t .TempDir ()
343+
344+ tokensDir := filepath .Join (tmpDir , "tokens" )
345+ if err := os .MkdirAll (tokensDir , 0700 ); err != nil {
346+ t .Fatalf ("mkdir tokens: %v" , err )
347+ }
348+ // Token with client_id matching the fake secrets
349+ tokenData , _ := json .Marshal (map [string ]string {
350+ "access_token" : "fake-access" ,
351+ "refresh_token" : "fake-refresh" ,
352+ "token_type" : "Bearer" ,
353+ "client_id" : "test.apps.googleusercontent.com" ,
354+ })
355+ if err := os .WriteFile (
356+ filepath .Join (tokensDir , "user@example.com.json" ),
357+ tokenData , 0600 ,
358+ ); err != nil {
359+ t .Fatalf ("write token: %v" , err )
360+ }
361+
362+ secretsPath := filepath .Join (tmpDir , "secret.json" )
363+ if err := os .WriteFile (
364+ secretsPath , []byte (fakeClientSecrets ), 0600 ,
365+ ); err != nil {
366+ t .Fatalf ("write secrets: %v" , err )
367+ }
368+
369+ savedCfg := cfg
370+ savedLogger := logger
371+ savedOAuthApp := oauthAppName
372+ defer func () {
373+ cfg = savedCfg
374+ logger = savedLogger
375+ oauthAppName = savedOAuthApp
376+ }()
377+
378+ cfg = & config.Config {
379+ HomeDir : tmpDir ,
380+ Data : config.DataConfig {DataDir : tmpDir },
381+ OAuth : config.OAuthConfig {ClientSecrets : secretsPath },
382+ }
383+ logger = slog .New (slog .NewTextHandler (os .Stderr , nil ))
384+
385+ testCmd := & cobra.Command {
386+ Use : "add-account <email>" ,
387+ Args : cobra .ExactArgs (1 ),
388+ RunE : addAccountCmd .RunE ,
389+ }
390+ testCmd .Flags ().StringVar (& oauthAppName , "oauth-app" , "" , "" )
391+ testCmd .Flags ().BoolVar (& headless , "headless" , false , "" )
392+ testCmd .Flags ().BoolVar (& forceReauth , "force" , false , "" )
393+ testCmd .Flags ().StringVar (& accountDisplayName , "display-name" , "" , "" )
394+
395+ root := newTestRootCmd ()
396+ root .AddCommand (testCmd )
397+ root .SetArgs ([]string {
398+ "add-account" , "user@example.com" , "--oauth-app" , "" ,
399+ })
400+
401+ // Should succeed: token's client_id matches default secrets
402+ err := root .Execute ()
403+ if err != nil {
404+ t .Fatalf ("unexpected error: %v" , err )
405+ }
406+ }
407+
268408func TestAddAccount_ForceRebindPreservesBindingOnFailure (t * testing.T ) {
269409 tmpDir := t .TempDir ()
270410 dbPath := filepath .Join (tmpDir , "msgvault.db" )
0 commit comments