import { describe, it, expect, vi, beforeEach } from "vitest"; import { restoreOAuthSession } from "../session.js"; import { createMockLogger } from "./mock-logger.js"; import type { AppContext } from "../app-context.js"; /** * Create a minimal mock AppContext with controllable cookieSessionStore * and oauthClient behavior for testing restoreOAuthSession. */ function createMockAppContext(overrides?: { cookieSession?: { did: string; handle?: string; expiresAt: Date; createdAt: Date } | null; restoreResult?: unknown; restoreError?: Error; }): AppContext { const cookieSessionStore = { get: vi.fn().mockReturnValue(overrides?.cookieSession ?? null), set: vi.fn(), delete: vi.fn(), destroy: vi.fn(), }; const oauthClient = { restore: vi.fn(), }; if (overrides?.restoreError) { oauthClient.restore.mockRejectedValue(overrides.restoreError); } else if (overrides?.restoreResult !== undefined) { oauthClient.restore.mockResolvedValue(overrides.restoreResult); } return { cookieSessionStore, oauthClient, logger: createMockLogger(), // Remaining fields are not used by restoreOAuthSession config: {} as any, db: {} as any, firehose: {} as any, oauthStateStore: {} as any, oauthSessionStore: {} as any, } as unknown as AppContext; } describe("restoreOAuthSession", () => { beforeEach(() => { vi.restoreAllMocks(); }); it("returns null when cookie session does not exist", async () => { const ctx = createMockAppContext({ cookieSession: null }); const result = await restoreOAuthSession(ctx, "nonexistent-token"); expect(result).toBeNull(); expect(ctx.oauthClient.restore).not.toHaveBeenCalled(); }); it("returns both OAuth and cookie sessions when restore succeeds", async () => { const mockOAuthSession = { did: "did:plc:test-user", serverMetadata: { issuer: "https://test.pds" }, }; const mockCookieSession = { did: "did:plc:test-user", handle: "testuser.test", expiresAt: new Date(Date.now() + 3600_000), createdAt: new Date(), }; const ctx = createMockAppContext({ cookieSession: mockCookieSession, restoreResult: mockOAuthSession, }); const result = await restoreOAuthSession(ctx, "valid-token"); expect(result).toEqual({ oauthSession: mockOAuthSession, cookieSession: mockCookieSession, }); expect(ctx.oauthClient.restore).toHaveBeenCalledWith("did:plc:test-user"); }); it("returns null when OAuth restore returns null", async () => { const ctx = createMockAppContext({ cookieSession: { did: "did:plc:test-user", expiresAt: new Date(Date.now() + 3600_000), createdAt: new Date(), }, restoreResult: null, }); const result = await restoreOAuthSession(ctx, "expired-oauth-token"); expect(result).toBeNull(); }); it("returns null when OAuth restore throws a 'not found' error", async () => { const ctx = createMockAppContext({ cookieSession: { did: "did:plc:test-user", expiresAt: new Date(Date.now() + 3600_000), createdAt: new Date(), }, restoreError: new Error("Session not found for DID"), }); const result = await restoreOAuthSession(ctx, "expired-oauth-token"); expect(result).toBeNull(); }); it("re-throws unexpected errors from OAuth restore", async () => { const networkError = new Error("fetch failed: ECONNREFUSED"); const ctx = createMockAppContext({ cookieSession: { did: "did:plc:test-user", expiresAt: new Date(Date.now() + 3600_000), createdAt: new Date(), }, restoreError: networkError, }); await expect(restoreOAuthSession(ctx, "valid-token")).rejects.toThrow( "fetch failed: ECONNREFUSED" ); // Verify the error was logged before re-throwing expect(ctx.logger.error).toHaveBeenCalledWith( "Unexpected error restoring OAuth session", expect.objectContaining({ operation: "restoreOAuthSession", did: "did:plc:test-user", error: "fetch failed: ECONNREFUSED", }) ); }); it("logs structured context when unexpected error occurs", async () => { const dbError = new Error("Database connection lost"); const ctx = createMockAppContext({ cookieSession: { did: "did:plc:another-user", handle: "another.test", expiresAt: new Date(Date.now() + 3600_000), createdAt: new Date(), }, restoreError: dbError, }); await expect(restoreOAuthSession(ctx, "some-token")).rejects.toThrow( "Database connection lost" ); expect(ctx.logger.error).toHaveBeenCalledWith( "Unexpected error restoring OAuth session", { operation: "restoreOAuthSession", did: "did:plc:another-user", error: "Database connection lost", } ); }); it("handles non-Error thrown values", async () => { const mockLogger = createMockLogger(); const cookieSessionStore = { get: vi.fn().mockReturnValue({ did: "did:plc:test-user", expiresAt: new Date(Date.now() + 3600_000), createdAt: new Date(), }), }; const oauthClient = { restore: vi.fn().mockRejectedValue("string error"), }; const ctx = { cookieSessionStore, oauthClient, logger: mockLogger, config: {} as any, db: {} as any, firehose: {} as any, oauthStateStore: {} as any, oauthSessionStore: {} as any, } as unknown as AppContext; await expect(restoreOAuthSession(ctx, "valid-token")).rejects.toBe( "string error" ); // Non-Error values should be stringified in the log expect(mockLogger.error).toHaveBeenCalledWith( "Unexpected error restoring OAuth session", expect.objectContaining({ error: "string error", }) ); }); });