Framework-agnostic OAuth integration for AT Protocol (Bluesky) applications.
1import { assertEquals, assertRejects } from "@std/assert";
2import { MemoryStorage } from "@tijs/atproto-storage";
3import { NetworkError, SessionError } from "@tijs/oauth-client-deno";
4import { OAuthSessions } from "./sessions.ts";
5import type { OAuthClientInterface, SessionInterface } from "./types.ts";
6import { noopLogger } from "./types.ts";
7
8/** Create a fake session for testing */
9function fakeSession(did: string): SessionInterface {
10 return {
11 did,
12 accessToken: "test-access-token",
13 pdsUrl: "https://pds.example.com",
14 timeUntilExpiry: 3600000,
15 makeRequest: () => Promise.resolve(new Response()),
16 toJSON: () => ({ did, accessToken: "test-access-token" }),
17 };
18}
19
20/** Create a mock OAuth client */
21function mockOAuthClient(
22 restoreFn: (sessionId: string) => Promise<SessionInterface | null>,
23): OAuthClientInterface {
24 return {
25 authorize: () => Promise.resolve(new URL("https://auth.example.com")),
26 callback: () =>
27 Promise.resolve({ session: fakeSession("did:plc:test"), state: null }),
28 restore: restoreFn,
29 };
30}
31
32Deno.test("getOAuthSession - returns session on success", async () => {
33 const session = fakeSession("did:plc:abc");
34 const sessions = new OAuthSessions({
35 oauthClient: mockOAuthClient(() => Promise.resolve(session)),
36 storage: new MemoryStorage(),
37 sessionTtl: 3600,
38 logger: noopLogger,
39 });
40
41 const result = await sessions.getOAuthSession("did:plc:abc");
42 assertEquals(result?.did, "did:plc:abc");
43});
44
45Deno.test("getOAuthSession - returns null when restore returns null", async () => {
46 const sessions = new OAuthSessions({
47 oauthClient: mockOAuthClient(() => Promise.resolve(null)),
48 storage: new MemoryStorage(),
49 sessionTtl: 3600,
50 logger: noopLogger,
51 });
52
53 const result = await sessions.getOAuthSession("did:plc:abc");
54 assertEquals(result, null);
55});
56
57Deno.test("getOAuthSession - returns null on SessionError (corrupt session)", async () => {
58 const storage = new MemoryStorage();
59 await storage.set("session:did:plc:abc", { corrupt: "data" });
60
61 const sessions = new OAuthSessions({
62 oauthClient: mockOAuthClient(() => {
63 throw new SessionError("Failed to restore session: did:plc:abc");
64 }),
65 storage,
66 sessionTtl: 3600,
67 logger: noopLogger,
68 });
69
70 const result = await sessions.getOAuthSession("did:plc:abc");
71 assertEquals(result, null);
72
73 // Should have cleaned up the dead session from storage
74 const stored = await storage.get("session:did:plc:abc");
75 assertEquals(stored, null);
76});
77
78Deno.test("getOAuthSession - returns null on token expiry errors", async () => {
79 const storage = new MemoryStorage();
80 await storage.set("session:did:plc:abc", { expired: true });
81
82 const sessions = new OAuthSessions({
83 oauthClient: mockOAuthClient(() => {
84 throw new Error("Refresh token has expired");
85 }),
86 storage,
87 sessionTtl: 3600,
88 logger: noopLogger,
89 });
90
91 const result = await sessions.getOAuthSession("did:plc:abc");
92 assertEquals(result, null);
93
94 // Should have cleaned up the dead session from storage
95 const stored = await storage.get("session:did:plc:abc");
96 assertEquals(stored, null);
97});
98
99Deno.test("getOAuthSession - re-throws NetworkError (transient)", async () => {
100 const sessions = new OAuthSessions({
101 oauthClient: mockOAuthClient(() => {
102 throw new NetworkError("Connection refused");
103 }),
104 storage: new MemoryStorage(),
105 sessionTtl: 3600,
106 logger: noopLogger,
107 });
108
109 await assertRejects(
110 () => sessions.getOAuthSession("did:plc:abc"),
111 NetworkError,
112 );
113});
114
115Deno.test("getOAuthSession - cleanup failure does not prevent null return", async () => {
116 // Storage that fails on delete
117 const storage = new MemoryStorage();
118 const originalDelete = storage.delete.bind(storage);
119 storage.delete = () => {
120 throw new Error("Storage delete failed");
121 };
122
123 const sessions = new OAuthSessions({
124 oauthClient: mockOAuthClient(() => {
125 throw new SessionError("Failed to restore session: did:plc:abc");
126 }),
127 storage,
128 sessionTtl: 3600,
129 logger: noopLogger,
130 });
131
132 // Should still return null even if cleanup fails
133 const result = await sessions.getOAuthSession("did:plc:abc");
134 assertEquals(result, null);
135
136 // Restore delete for cleanup
137 storage.delete = originalDelete;
138});