WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at root/atb-56-theme-caching-layer 207 lines 6.0 kB view raw
1import { describe, it, expect, vi, beforeEach } from "vitest"; 2import { restoreOAuthSession } from "../session.js"; 3import { createMockLogger } from "./mock-logger.js"; 4import type { AppContext } from "../app-context.js"; 5 6/** 7 * Create a minimal mock AppContext with controllable cookieSessionStore 8 * and oauthClient behavior for testing restoreOAuthSession. 9 */ 10function createMockAppContext(overrides?: { 11 cookieSession?: { did: string; handle?: string; expiresAt: Date; createdAt: Date } | null; 12 restoreResult?: unknown; 13 restoreError?: Error; 14}): AppContext { 15 const cookieSessionStore = { 16 get: vi.fn().mockReturnValue(overrides?.cookieSession ?? null), 17 set: vi.fn(), 18 delete: vi.fn(), 19 destroy: vi.fn(), 20 }; 21 22 const oauthClient = { 23 restore: vi.fn(), 24 }; 25 26 if (overrides?.restoreError) { 27 oauthClient.restore.mockRejectedValue(overrides.restoreError); 28 } else if (overrides?.restoreResult !== undefined) { 29 oauthClient.restore.mockResolvedValue(overrides.restoreResult); 30 } 31 32 return { 33 cookieSessionStore, 34 oauthClient, 35 logger: createMockLogger(), 36 // Remaining fields are not used by restoreOAuthSession 37 config: {} as any, 38 db: {} as any, 39 firehose: {} as any, 40 oauthStateStore: {} as any, 41 oauthSessionStore: {} as any, 42 } as unknown as AppContext; 43} 44 45describe("restoreOAuthSession", () => { 46 beforeEach(() => { 47 vi.restoreAllMocks(); 48 }); 49 50 it("returns null when cookie session does not exist", async () => { 51 const ctx = createMockAppContext({ cookieSession: null }); 52 53 const result = await restoreOAuthSession(ctx, "nonexistent-token"); 54 55 expect(result).toBeNull(); 56 expect(ctx.oauthClient.restore).not.toHaveBeenCalled(); 57 }); 58 59 it("returns both OAuth and cookie sessions when restore succeeds", async () => { 60 const mockOAuthSession = { 61 did: "did:plc:test-user", 62 serverMetadata: { issuer: "https://test.pds" }, 63 }; 64 65 const mockCookieSession = { 66 did: "did:plc:test-user", 67 handle: "testuser.test", 68 expiresAt: new Date(Date.now() + 3600_000), 69 createdAt: new Date(), 70 }; 71 72 const ctx = createMockAppContext({ 73 cookieSession: mockCookieSession, 74 restoreResult: mockOAuthSession, 75 }); 76 77 const result = await restoreOAuthSession(ctx, "valid-token"); 78 79 expect(result).toEqual({ 80 oauthSession: mockOAuthSession, 81 cookieSession: mockCookieSession, 82 }); 83 expect(ctx.oauthClient.restore).toHaveBeenCalledWith("did:plc:test-user"); 84 }); 85 86 it("returns null when OAuth restore returns null", async () => { 87 const ctx = createMockAppContext({ 88 cookieSession: { 89 did: "did:plc:test-user", 90 expiresAt: new Date(Date.now() + 3600_000), 91 createdAt: new Date(), 92 }, 93 restoreResult: null, 94 }); 95 96 const result = await restoreOAuthSession(ctx, "expired-oauth-token"); 97 98 expect(result).toBeNull(); 99 }); 100 101 it("returns null when OAuth restore throws a 'not found' error", async () => { 102 const ctx = createMockAppContext({ 103 cookieSession: { 104 did: "did:plc:test-user", 105 expiresAt: new Date(Date.now() + 3600_000), 106 createdAt: new Date(), 107 }, 108 restoreError: new Error("Session not found for DID"), 109 }); 110 111 const result = await restoreOAuthSession(ctx, "expired-oauth-token"); 112 113 expect(result).toBeNull(); 114 }); 115 116 it("re-throws unexpected errors from OAuth restore", async () => { 117 const networkError = new Error("fetch failed: ECONNREFUSED"); 118 119 const ctx = createMockAppContext({ 120 cookieSession: { 121 did: "did:plc:test-user", 122 expiresAt: new Date(Date.now() + 3600_000), 123 createdAt: new Date(), 124 }, 125 restoreError: networkError, 126 }); 127 128 await expect(restoreOAuthSession(ctx, "valid-token")).rejects.toThrow( 129 "fetch failed: ECONNREFUSED" 130 ); 131 132 // Verify the error was logged before re-throwing 133 expect(ctx.logger.error).toHaveBeenCalledWith( 134 "Unexpected error restoring OAuth session", 135 expect.objectContaining({ 136 operation: "restoreOAuthSession", 137 did: "did:plc:test-user", 138 error: "fetch failed: ECONNREFUSED", 139 }) 140 ); 141 }); 142 143 it("logs structured context when unexpected error occurs", async () => { 144 const dbError = new Error("Database connection lost"); 145 146 const ctx = createMockAppContext({ 147 cookieSession: { 148 did: "did:plc:another-user", 149 handle: "another.test", 150 expiresAt: new Date(Date.now() + 3600_000), 151 createdAt: new Date(), 152 }, 153 restoreError: dbError, 154 }); 155 156 await expect(restoreOAuthSession(ctx, "some-token")).rejects.toThrow( 157 "Database connection lost" 158 ); 159 160 expect(ctx.logger.error).toHaveBeenCalledWith( 161 "Unexpected error restoring OAuth session", 162 { 163 operation: "restoreOAuthSession", 164 did: "did:plc:another-user", 165 error: "Database connection lost", 166 } 167 ); 168 }); 169 170 it("handles non-Error thrown values", async () => { 171 const mockLogger = createMockLogger(); 172 173 const cookieSessionStore = { 174 get: vi.fn().mockReturnValue({ 175 did: "did:plc:test-user", 176 expiresAt: new Date(Date.now() + 3600_000), 177 createdAt: new Date(), 178 }), 179 }; 180 const oauthClient = { 181 restore: vi.fn().mockRejectedValue("string error"), 182 }; 183 184 const ctx = { 185 cookieSessionStore, 186 oauthClient, 187 logger: mockLogger, 188 config: {} as any, 189 db: {} as any, 190 firehose: {} as any, 191 oauthStateStore: {} as any, 192 oauthSessionStore: {} as any, 193 } as unknown as AppContext; 194 195 await expect(restoreOAuthSession(ctx, "valid-token")).rejects.toBe( 196 "string error" 197 ); 198 199 // Non-Error values should be stringified in the log 200 expect(mockLogger.error).toHaveBeenCalledWith( 201 "Unexpected error restoring OAuth session", 202 expect.objectContaining({ 203 error: "string error", 204 }) 205 ); 206 }); 207});