package auth import ( "context" "errors" "testing" "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" ) // MockKeyring is an in-memory implementation of Keyring for testing. type MockKeyring struct { data map[string]map[string]string // service -> key -> value } func NewMockKeyring() *MockKeyring { return &MockKeyring{ data: make(map[string]map[string]string), } } var ErrNotFound = errors.New("secret not found in keyring") func (m *MockKeyring) Get(service, key string) (string, error) { if svc, ok := m.data[service]; ok { if val, ok := svc[key]; ok { return val, nil } } return "", ErrNotFound } func (m *MockKeyring) Set(service, key, value string) error { if _, ok := m.data[service]; !ok { m.data[service] = make(map[string]string) } m.data[service][key] = value return nil } func (m *MockKeyring) Delete(service, key string) error { if svc, ok := m.data[service]; ok { delete(svc, key) } return nil } func TestSessionKey(t *testing.T) { did, _ := syntax.ParseDID("did:plc:test123") key := sessionKey(did, "session-abc") expected := "session:did:plc:test123:session-abc" if key != expected { t.Errorf("sessionKey() = %q, want %q", key, expected) } } func TestAuthRequestKey(t *testing.T) { key := authRequestKey("state123") expected := "auth-request:state123" if key != expected { t.Errorf("authRequestKey() = %q, want %q", key, expected) } } func TestSaveAndGetSession(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) ctx := context.Background() did, _ := syntax.ParseDID("did:plc:testuser") sess := oauth.ClientSessionData{ AccountDID: did, SessionID: "test-session-id", } // Save session if err := store.SaveSession(ctx, sess); err != nil { t.Fatalf("SaveSession() error = %v", err) } // Get session retrieved, err := store.GetSession(ctx, did, "test-session-id") if err != nil { t.Fatalf("GetSession() error = %v", err) } if retrieved.AccountDID.String() != did.String() { t.Errorf("GetSession() DID = %q, want %q", retrieved.AccountDID.String(), did.String()) } if retrieved.SessionID != "test-session-id" { t.Errorf("GetSession() SessionID = %q, want %q", retrieved.SessionID, "test-session-id") } } func TestDeleteSession(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) ctx := context.Background() did, _ := syntax.ParseDID("did:plc:testuser") sess := oauth.ClientSessionData{ AccountDID: did, SessionID: "test-session-id", } // Save then delete store.SaveSession(ctx, sess) if err := store.DeleteSession(ctx, did, "test-session-id"); err != nil { t.Fatalf("DeleteSession() error = %v", err) } // Should not be found _, err := store.GetSession(ctx, did, "test-session-id") if err == nil { t.Error("GetSession() expected error after delete, got nil") } } func TestSaveAndGetAuthRequestInfo(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) ctx := context.Background() info := oauth.AuthRequestData{ State: "test-state-123", } // Save auth request if err := store.SaveAuthRequestInfo(ctx, info); err != nil { t.Fatalf("SaveAuthRequestInfo() error = %v", err) } // Get auth request retrieved, err := store.GetAuthRequestInfo(ctx, "test-state-123") if err != nil { t.Fatalf("GetAuthRequestInfo() error = %v", err) } if retrieved.State != "test-state-123" { t.Errorf("GetAuthRequestInfo() State = %q, want %q", retrieved.State, "test-state-123") } // Pending auth state should also be set state, err := store.GetPendingAuthState() if err != nil { t.Fatalf("GetPendingAuthState() error = %v", err) } if state != "test-state-123" { t.Errorf("GetPendingAuthState() = %q, want %q", state, "test-state-123") } } func TestDeleteAuthRequestInfo(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) ctx := context.Background() info := oauth.AuthRequestData{ State: "test-state-456", } store.SaveAuthRequestInfo(ctx, info) if err := store.DeleteAuthRequestInfo(ctx, "test-state-456"); err != nil { t.Fatalf("DeleteAuthRequestInfo() error = %v", err) } _, err := store.GetAuthRequestInfo(ctx, "test-state-456") if err == nil { t.Error("GetAuthRequestInfo() expected error after delete, got nil") } } func TestClearPendingAuthState(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) ctx := context.Background() info := oauth.AuthRequestData{ State: "test-state-789", } store.SaveAuthRequestInfo(ctx, info) if err := store.ClearPendingAuthState(); err != nil { t.Fatalf("ClearPendingAuthState() error = %v", err) } _, err := store.GetPendingAuthState() if err == nil { t.Error("GetPendingAuthState() expected error after clear, got nil") } } func TestSetAndGetCurrentSession(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) ctx := context.Background() did, _ := syntax.ParseDID("did:plc:currentuser") sess := oauth.ClientSessionData{ AccountDID: did, SessionID: "current-session-id", } // Save the actual session first if err := store.SaveSession(ctx, sess); err != nil { t.Fatalf("SaveSession() error = %v", err) } // Set as current session if err := store.SetCurrentSession(ctx, &sess); err != nil { t.Fatalf("SetCurrentSession() error = %v", err) } // Get current session retrieved, err := store.GetCurrentSession(ctx) if err != nil { t.Fatalf("GetCurrentSession() error = %v", err) } if retrieved.AccountDID.String() != did.String() { t.Errorf("GetCurrentSession() DID = %q, want %q", retrieved.AccountDID.String(), did.String()) } if retrieved.SessionID != "current-session-id" { t.Errorf("GetCurrentSession() SessionID = %q, want %q", retrieved.SessionID, "current-session-id") } } func TestClearCurrentSession(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) ctx := context.Background() did, _ := syntax.ParseDID("did:plc:currentuser") sess := oauth.ClientSessionData{ AccountDID: did, SessionID: "current-session-id", } store.SaveSession(ctx, sess) store.SetCurrentSession(ctx, &sess) if err := store.ClearCurrentSession(); err != nil { t.Fatalf("ClearCurrentSession() error = %v", err) } _, err := store.GetCurrentSession(ctx) if err == nil { t.Error("GetCurrentSession() expected error after clear, got nil") } } func TestSetAndGetLoginIdentifier(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) if err := store.SetLoginIdentifier("user.bsky.social"); err != nil { t.Fatalf("SetLoginIdentifier() error = %v", err) } id, err := store.GetLoginIdentifier() if err != nil { t.Fatalf("GetLoginIdentifier() error = %v", err) } if id != "user.bsky.social" { t.Errorf("GetLoginIdentifier() = %q, want %q", id, "user.bsky.social") } } func TestClearLoginIdentifier(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) store.SetLoginIdentifier("user.bsky.social") if err := store.ClearLoginIdentifier(); err != nil { t.Fatalf("ClearLoginIdentifier() error = %v", err) } _, err := store.GetLoginIdentifier() if err == nil { t.Error("GetLoginIdentifier() expected error after clear, got nil") } } func TestGetSessionNotFound(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) ctx := context.Background() did, _ := syntax.ParseDID("did:plc:nonexistent") _, err := store.GetSession(ctx, did, "no-such-session") if err == nil { t.Error("GetSession() expected error for non-existent session, got nil") } } func TestGetCurrentSessionNotFound(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) ctx := context.Background() _, err := store.GetCurrentSession(ctx) if err == nil { t.Error("GetCurrentSession() expected error when no current session, got nil") } } func TestGetLoginIdentifierNotFound(t *testing.T) { mock := NewMockKeyring() store := NewKeyringAuthStoreWithKeyring(mock) _, err := store.GetLoginIdentifier() if err == nil { t.Error("GetLoginIdentifier() expected error when not set, got nil") } }