From 1b96516b9ba9ed3be19c4f93bddbb833883d0a77 Mon Sep 17 00:00:00 2001 From: Qasim Date: Tue, 3 Feb 2026 10:11:40 -0500 Subject: [PATCH] fix(auth): update config file when switching default grant Previously, `nylas auth switch` only updated the default grant in the keyring but not in the config file (~/.config/nylas/config.yaml). This caused the config file to show stale default_grant values. Now both SwitchGrant and SwitchGrantByEmail update the keyring (authoritative) and the config file. Config save errors are non-fatal since the keyring is the source of truth. Added unit tests for GrantService switch operations. --- internal/app/auth/grants.go | 23 +++- internal/app/auth/grants_test.go | 213 +++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 internal/app/auth/grants_test.go diff --git a/internal/app/auth/grants.go b/internal/app/auth/grants.go index 0f7924a..754f15e 100644 --- a/internal/app/auth/grants.go +++ b/internal/app/auth/grants.go @@ -127,7 +127,7 @@ func (s *GrantService) SwitchGrant(grantID string) error { if _, err := s.grantStore.GetGrant(grantID); err != nil { return err } - return s.grantStore.SetDefaultGrant(grantID) + return s.setDefaultGrant(grantID) } // SwitchGrantByEmail switches the default grant by email. @@ -136,7 +136,26 @@ func (s *GrantService) SwitchGrantByEmail(email string) error { if err != nil { return err } - return s.grantStore.SetDefaultGrant(info.ID) + return s.setDefaultGrant(info.ID) +} + +// setDefaultGrant updates the default grant in both keyring and config file. +func (s *GrantService) setDefaultGrant(grantID string) error { + // Update keyring + if err := s.grantStore.SetDefaultGrant(grantID); err != nil { + return err + } + + // Update config file + cfg, err := s.config.Load() + if err != nil { + // Non-fatal: keyring is the authoritative source + return nil + } + cfg.DefaultGrant = grantID + _ = s.config.Save(cfg) // Ignore save errors, keyring is authoritative + + return nil } // ValidateGrant checks if a grant is valid. diff --git a/internal/app/auth/grants_test.go b/internal/app/auth/grants_test.go new file mode 100644 index 0000000..e32fd03 --- /dev/null +++ b/internal/app/auth/grants_test.go @@ -0,0 +1,213 @@ +package auth + +import ( + "testing" + + "github.com/nylas/cli/internal/adapters/nylas" + "github.com/nylas/cli/internal/domain" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGrantService_SwitchGrant(t *testing.T) { + t.Run("updates both keyring and config file", func(t *testing.T) { + grantStore := newMockGrantStore() + configStore := newMockConfigStore() + client := nylas.NewMockClient() + + // Set up existing grants + grantStore.grants["grant-1"] = domain.GrantInfo{ID: "grant-1", Email: "user1@example.com"} + grantStore.grants["grant-2"] = domain.GrantInfo{ID: "grant-2", Email: "user2@example.com"} + grantStore.defaultGrant = "grant-1" + configStore.config.DefaultGrant = "grant-1" + + svc := NewGrantService(client, grantStore, configStore) + + err := svc.SwitchGrant("grant-2") + + require.NoError(t, err) + + // Verify keyring was updated + defaultID, err := grantStore.GetDefaultGrant() + require.NoError(t, err) + assert.Equal(t, "grant-2", defaultID) + + // Verify config file was updated + assert.Equal(t, "grant-2", configStore.config.DefaultGrant) + }) + + t.Run("returns error for non-existent grant", func(t *testing.T) { + grantStore := newMockGrantStore() + configStore := newMockConfigStore() + client := nylas.NewMockClient() + + grantStore.grants["grant-1"] = domain.GrantInfo{ID: "grant-1", Email: "user1@example.com"} + grantStore.defaultGrant = "grant-1" + + svc := NewGrantService(client, grantStore, configStore) + + err := svc.SwitchGrant("non-existent") + + assert.ErrorIs(t, err, domain.ErrGrantNotFound) + + // Verify default was not changed + defaultID, err := grantStore.GetDefaultGrant() + require.NoError(t, err) + assert.Equal(t, "grant-1", defaultID) + }) + + t.Run("succeeds even if config save fails", func(t *testing.T) { + grantStore := newMockGrantStore() + configStore := &failingSaveConfigStore{config: &domain.Config{DefaultGrant: "grant-1"}} + client := nylas.NewMockClient() + + grantStore.grants["grant-1"] = domain.GrantInfo{ID: "grant-1", Email: "user1@example.com"} + grantStore.grants["grant-2"] = domain.GrantInfo{ID: "grant-2", Email: "user2@example.com"} + grantStore.defaultGrant = "grant-1" + + svc := NewGrantService(client, grantStore, configStore) + + // Should succeed - keyring is authoritative, config save failure is non-fatal + err := svc.SwitchGrant("grant-2") + + require.NoError(t, err) + + // Verify keyring was updated + defaultID, err := grantStore.GetDefaultGrant() + require.NoError(t, err) + assert.Equal(t, "grant-2", defaultID) + }) +} + +func TestGrantService_SwitchGrantByEmail(t *testing.T) { + t.Run("updates both keyring and config file", func(t *testing.T) { + grantStore := newMockGrantStore() + configStore := newMockConfigStore() + client := nylas.NewMockClient() + + // Set up existing grants + grantStore.grants["grant-1"] = domain.GrantInfo{ID: "grant-1", Email: "user1@example.com"} + grantStore.grants["grant-2"] = domain.GrantInfo{ID: "grant-2", Email: "user2@example.com"} + grantStore.defaultGrant = "grant-1" + configStore.config.DefaultGrant = "grant-1" + + svc := NewGrantService(client, grantStore, configStore) + + err := svc.SwitchGrantByEmail("user2@example.com") + + require.NoError(t, err) + + // Verify keyring was updated + defaultID, err := grantStore.GetDefaultGrant() + require.NoError(t, err) + assert.Equal(t, "grant-2", defaultID) + + // Verify config file was updated + assert.Equal(t, "grant-2", configStore.config.DefaultGrant) + }) + + t.Run("returns error for non-existent email", func(t *testing.T) { + grantStore := newMockGrantStore() + configStore := newMockConfigStore() + client := nylas.NewMockClient() + + grantStore.grants["grant-1"] = domain.GrantInfo{ID: "grant-1", Email: "user1@example.com"} + grantStore.defaultGrant = "grant-1" + + svc := NewGrantService(client, grantStore, configStore) + + err := svc.SwitchGrantByEmail("nonexistent@example.com") + + assert.ErrorIs(t, err, domain.ErrGrantNotFound) + + // Verify default was not changed + defaultID, err := grantStore.GetDefaultGrant() + require.NoError(t, err) + assert.Equal(t, "grant-1", defaultID) + }) +} + +func TestGrantService_AddGrant(t *testing.T) { + t.Run("adds grant and sets as default when requested", func(t *testing.T) { + grantStore := newMockGrantStore() + configStore := newMockConfigStore() + client := nylas.NewMockClient() + + svc := NewGrantService(client, grantStore, configStore) + + err := svc.AddGrant("grant-1", "user@example.com", domain.ProviderGoogle, true) + + require.NoError(t, err) + + // Verify grant was saved + grant, err := grantStore.GetGrant("grant-1") + require.NoError(t, err) + assert.Equal(t, "user@example.com", grant.Email) + + // Verify it's set as default + defaultID, err := grantStore.GetDefaultGrant() + require.NoError(t, err) + assert.Equal(t, "grant-1", defaultID) + }) + + t.Run("auto-sets first grant as default", func(t *testing.T) { + grantStore := newMockGrantStore() + configStore := newMockConfigStore() + client := nylas.NewMockClient() + + svc := NewGrantService(client, grantStore, configStore) + + // Add grant without setDefault=true, but it's the first grant + err := svc.AddGrant("grant-1", "user@example.com", domain.ProviderGoogle, false) + + require.NoError(t, err) + + // Should still be set as default since it's the first + defaultID, err := grantStore.GetDefaultGrant() + require.NoError(t, err) + assert.Equal(t, "grant-1", defaultID) + }) + + t.Run("does not override existing default", func(t *testing.T) { + grantStore := newMockGrantStore() + configStore := newMockConfigStore() + client := nylas.NewMockClient() + + // Set up existing grant as default + grantStore.grants["grant-1"] = domain.GrantInfo{ID: "grant-1", Email: "user1@example.com"} + grantStore.defaultGrant = "grant-1" + + svc := NewGrantService(client, grantStore, configStore) + + // Add second grant without setDefault + err := svc.AddGrant("grant-2", "user2@example.com", domain.ProviderMicrosoft, false) + + require.NoError(t, err) + + // Original default should be preserved + defaultID, err := grantStore.GetDefaultGrant() + require.NoError(t, err) + assert.Equal(t, "grant-1", defaultID) + }) +} + +// failingSaveConfigStore is a mock that fails on Save +type failingSaveConfigStore struct { + config *domain.Config +} + +func (m *failingSaveConfigStore) Load() (*domain.Config, error) { + return m.config, nil +} + +func (m *failingSaveConfigStore) Save(cfg *domain.Config) error { + return domain.ErrInvalidInput // Simulate save failure +} + +func (m *failingSaveConfigStore) Path() string { + return "/tmp/test-config.yaml" +} + +func (m *failingSaveConfigStore) Exists() bool { + return true +}