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 main 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});