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 atb-52-css-token-extraction 333 lines 9.7 kB view raw
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});