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
23 changes: 21 additions & 2 deletions internal/app/auth/grants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
213 changes: 213 additions & 0 deletions internal/app/auth/grants_test.go
Original file line number Diff line number Diff line change
@@ -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
}