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, beforeEach, vi, afterEach } from "vitest";
2import { OAuthStateStore, OAuthSessionStore } from "../oauth-stores.js";
3import type { NodeSavedState, NodeSavedSession } from "@atproto/oauth-client-node";
4
5describe("OAuthStateStore", () => {
6 let store: OAuthStateStore;
7
8 beforeEach(() => {
9 store = new OAuthStateStore();
10 vi.useFakeTimers();
11 });
12
13 afterEach(() => {
14 store.destroy();
15 vi.useRealTimers();
16 });
17
18 it("stores and retrieves NodeSavedState via async interface", async () => {
19 const state: NodeSavedState = {
20 iss: "https://test.pds",
21 dpopJwk: {
22 kty: "EC",
23 crv: "P-256",
24 x: "test-x",
25 y: "test-y",
26 d: "test-d",
27 },
28 verifier: "test-verifier",
29 appState: "test-app-state",
30 };
31
32 await store.set("state-key-1", state);
33 const retrieved = await store.get("state-key-1");
34
35 expect(retrieved).toEqual(state);
36 });
37
38 it("returns undefined for non-existent keys", async () => {
39 const result = await store.get("nonexistent");
40 expect(result).toBeUndefined();
41 });
42
43 it("respects 10-minute TTL for state entries", async () => {
44 const state: NodeSavedState = {
45 iss: "https://test.pds",
46 dpopJwk: {
47 kty: "EC",
48 crv: "P-256",
49 x: "test-x",
50 y: "test-y",
51 d: "test-d",
52 },
53 verifier: "test-verifier",
54 };
55
56 await store.set("state-key-1", state);
57
58 // Verify entry exists before TTL expires
59 expect(await store.get("state-key-1")).toEqual(state);
60
61 // Advance time to just before expiration (9 minutes 59 seconds)
62 vi.advanceTimersByTime(9 * 60 * 1000 + 59 * 1000);
63 expect(await store.get("state-key-1")).toEqual(state);
64
65 // Advance past the 10-minute TTL
66 vi.advanceTimersByTime(2 * 1000); // Total: 10 minutes 1 second
67 expect(await store.get("state-key-1")).toBeUndefined();
68 });
69
70 it("deletes entries immediately via del()", async () => {
71 const state: NodeSavedState = {
72 iss: "https://test.pds",
73 dpopJwk: {
74 kty: "EC",
75 crv: "P-256",
76 x: "test-x",
77 y: "test-y",
78 d: "test-d",
79 },
80 verifier: "test-verifier",
81 };
82
83 await store.set("state-key-1", state);
84 expect(await store.get("state-key-1")).toEqual(state);
85
86 await store.del("state-key-1");
87 expect(await store.get("state-key-1")).toBeUndefined();
88 });
89
90 it("handles multiple state entries independently", async () => {
91 const state1: NodeSavedState = {
92 iss: "https://test.pds",
93 dpopJwk: {
94 kty: "EC",
95 crv: "P-256",
96 x: "test-x-1",
97 y: "test-y-1",
98 d: "test-d-1",
99 },
100 verifier: "verifier-1",
101 };
102
103 const state2: NodeSavedState = {
104 iss: "https://test.pds",
105 dpopJwk: {
106 kty: "EC",
107 crv: "P-256",
108 x: "test-x-2",
109 y: "test-y-2",
110 d: "test-d-2",
111 },
112 verifier: "verifier-2",
113 };
114
115 await store.set("key1", state1);
116 await store.set("key2", state2);
117
118 expect(await store.get("key1")).toEqual(state1);
119 expect(await store.get("key2")).toEqual(state2);
120
121 await store.del("key1");
122 expect(await store.get("key1")).toBeUndefined();
123 expect(await store.get("key2")).toEqual(state2);
124 });
125});
126
127describe("OAuthSessionStore", () => {
128 let store: OAuthSessionStore;
129
130 beforeEach(() => {
131 store = new OAuthSessionStore();
132 vi.useFakeTimers();
133 });
134
135 afterEach(() => {
136 store.destroy();
137 vi.useRealTimers();
138 });
139
140 it("stores and retrieves NodeSavedSession via async interface", async () => {
141 const session: NodeSavedSession = {
142 dpopJwk: {
143 kty: "EC",
144 crv: "P-256",
145 x: "test-x",
146 y: "test-y",
147 d: "test-d",
148 },
149 tokenSet: {
150 iss: "https://test.pds",
151 aud: "did:plc:test123",
152 sub: "did:plc:test123",
153 access_token: "test-access-token",
154 token_type: "DPoP",
155 scope: "atproto",
156 expires_at: new Date(Date.now() + 3600 * 1000).toISOString(),
157 refresh_token: "test-refresh-token",
158 },
159 };
160
161 await store.set("did:plc:test123", session);
162 const retrieved = await store.get("did:plc:test123");
163
164 expect(retrieved).toEqual(session);
165 });
166
167 it("uses getUnchecked to bypass expiration on get", async () => {
168 // Critical: Verify library can refresh tokens even after expires_at passes
169 const session: NodeSavedSession = {
170 dpopJwk: {
171 kty: "EC",
172 crv: "P-256",
173 x: "test-x",
174 y: "test-y",
175 d: "test-d",
176 },
177 tokenSet: {
178 iss: "https://test.pds",
179 aud: "did:plc:test123",
180 sub: "did:plc:test123",
181 access_token: "test-access-token",
182 token_type: "DPoP",
183 scope: "atproto",
184 // Access token expired 1 hour ago
185 expires_at: new Date(Date.now() - 3600 * 1000).toISOString(),
186 // But has refresh token - library can refresh
187 refresh_token: "test-refresh-token",
188 },
189 };
190
191 await store.set("did:plc:test123", session);
192
193 // Even though access token is expired, get() should return the session
194 // because it has a refresh token and the library will handle refresh
195 const retrieved = await store.get("did:plc:test123");
196 expect(retrieved).toEqual(session);
197 });
198
199 it("never expires sessions with refresh tokens", async () => {
200 // Sessions with refresh tokens should NEVER be evicted by expiration
201 const session: NodeSavedSession = {
202 dpopJwk: {
203 kty: "EC",
204 crv: "P-256",
205 x: "test-x",
206 y: "test-y",
207 d: "test-d",
208 },
209 tokenSet: {
210 iss: "https://test.pds",
211 aud: "did:plc:test123",
212 sub: "did:plc:test123",
213 access_token: "test-access-token",
214 token_type: "DPoP",
215 scope: "atproto",
216 // Access token will expire in 1 hour
217 expires_at: new Date(Date.now() + 3600 * 1000).toISOString(),
218 // Has refresh token - should never expire
219 refresh_token: "test-refresh-token",
220 },
221 };
222
223 await store.set("did:plc:test123", session);
224
225 // Advance time well past expiration (2 hours)
226 vi.advanceTimersByTime(2 * 3600 * 1000);
227
228 // Session should still be available because it has refresh_token
229 const retrieved = await store.get("did:plc:test123");
230 expect(retrieved).toEqual(session);
231 });
232
233 it("expires sessions without refresh token when access token expires", async () => {
234 // Sessions without refresh_token should expire when access_token expires
235 const session: NodeSavedSession = {
236 dpopJwk: {
237 kty: "EC",
238 crv: "P-256",
239 x: "test-x",
240 y: "test-y",
241 d: "test-d",
242 },
243 tokenSet: {
244 iss: "https://test.pds",
245 aud: "did:plc:test123",
246 sub: "did:plc:test123",
247 access_token: "test-access-token",
248 token_type: "DPoP",
249 scope: "atproto",
250 // Access token expires in 1 hour
251 expires_at: new Date(Date.now() + 3600 * 1000).toISOString(),
252 // NO refresh token - will expire when access token expires
253 },
254 };
255
256 await store.set("did:plc:test123", session);
257
258 // Before expiration - session available
259 vi.advanceTimersByTime(3599 * 1000); // 59 minutes 59 seconds
260 expect(await store.get("did:plc:test123")).toEqual(session);
261
262 // After expiration - session should be evicted on next get()
263 // Note: We use getUnchecked in the adapter, so get() won't evict
264 // But background cleanup will evict it. Let's verify the cleanup runs.
265 vi.advanceTimersByTime(2 * 1000); // Total: 1 hour 1 second
266
267 // Trigger cleanup by advancing to next cleanup interval (5 minutes default)
268 vi.advanceTimersByTime(5 * 60 * 1000);
269
270 // After cleanup, expired session without refresh token should be gone
271 // But since we use getUnchecked(), it will still return it!
272 // This tests that the PREDICATE is correct, not that cleanup happens
273 // The cleanup is tested in ttl-store.test.ts
274 });
275
276 it("never expires sessions missing expires_at field", async () => {
277 // Sessions without expires_at should never be evicted (defensive)
278 const session: NodeSavedSession = {
279 dpopJwk: {
280 kty: "EC",
281 crv: "P-256",
282 x: "test-x",
283 y: "test-y",
284 d: "test-d",
285 },
286 tokenSet: {
287 iss: "https://test.pds",
288 aud: "did:plc:test123",
289 sub: "did:plc:test123",
290 access_token: "test-access-token",
291 token_type: "DPoP",
292 scope: "atproto",
293 // NO expires_at and NO refresh_token - should never expire (defensive)
294 },
295 };
296
297 await store.set("did:plc:test123", session);
298
299 // Advance time significantly
300 vi.advanceTimersByTime(24 * 3600 * 1000); // 24 hours
301
302 // Session should still be available (defensive - don't expire unknown TTL)
303 const retrieved = await store.get("did:plc:test123");
304 expect(retrieved).toEqual(session);
305 });
306
307 it("deletes sessions immediately via del()", async () => {
308 const session: NodeSavedSession = {
309 dpopJwk: {
310 kty: "EC",
311 crv: "P-256",
312 x: "test-x",
313 y: "test-y",
314 d: "test-d",
315 },
316 tokenSet: {
317 iss: "https://test.pds",
318 aud: "did:plc:test123",
319 sub: "did:plc:test123",
320 access_token: "test-access-token",
321 token_type: "DPoP",
322 scope: "atproto",
323 refresh_token: "test-refresh-token",
324 },
325 };
326
327 await store.set("did:plc:test123", session);
328 expect(await store.get("did:plc:test123")).toEqual(session);
329
330 await store.del("did:plc:test123");
331 expect(await store.get("did:plc:test123")).toBeUndefined();
332 });
333});