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