A Deno-compatible AT Protocol OAuth client that serves as a drop-in replacement for @atproto/oauth-client-node
1/**
2 * @fileoverview Tests for Session class
3 */
4
5import { assertEquals } from "@std/assert";
6import { Session, type SessionData } from "../src/session.ts";
7
8// Helper to create test session data
9function createTestSessionData(overrides: Partial<SessionData> = {}): SessionData {
10 return {
11 did: "did:plc:test123",
12 handle: "test.bsky.social",
13 pdsUrl: "https://test.bsky.social",
14 accessToken: "test_access_token",
15 refreshToken: "test_refresh_token",
16 dpopPrivateKeyJWK: {
17 kty: "EC",
18 crv: "P-256",
19 x: "test_x_value",
20 y: "test_y_value",
21 d: "test_d_value",
22 },
23 dpopPublicKeyJWK: {
24 kty: "EC",
25 crv: "P-256",
26 x: "test_x_value",
27 y: "test_y_value",
28 },
29 tokenExpiresAt: Date.now() + (60 * 60 * 1000), // 1 hour from now
30 ...overrides,
31 };
32}
33
34Deno.test("Session - Constructor and Basic Properties", async (t) => {
35 const sessionData = createTestSessionData();
36 const session = new Session(sessionData);
37
38 await t.step("should expose basic properties", () => {
39 assertEquals(session.did, "did:plc:test123");
40 assertEquals(session.handle, "test.bsky.social");
41 assertEquals(session.pdsUrl, "https://test.bsky.social");
42 assertEquals(session.accessToken, "test_access_token");
43 assertEquals(session.refreshToken, "test_refresh_token");
44 });
45
46 await t.step("should expose OAuthSession interface properties", () => {
47 assertEquals(session.sub, "did:plc:test123"); // same as DID
48 assertEquals(session.aud, "https://test.bsky.social"); // same as pdsUrl
49 });
50});
51
52Deno.test("Session - Expiration Logic", async (t) => {
53 await t.step("should not be expired for future tokens", () => {
54 const futureTime = Date.now() + (60 * 60 * 1000); // 1 hour from now
55 const sessionData = createTestSessionData({ tokenExpiresAt: futureTime });
56 const session = new Session(sessionData);
57
58 assertEquals(session.isExpired, false);
59 });
60
61 await t.step("should be expired for past tokens", () => {
62 const pastTime = Date.now() - (60 * 60 * 1000); // 1 hour ago
63 const sessionData = createTestSessionData({ tokenExpiresAt: pastTime });
64 const session = new Session(sessionData);
65
66 assertEquals(session.isExpired, true);
67 });
68
69 await t.step("should be expired for tokens expiring within 5 minutes", () => {
70 const soonTime = Date.now() + (2 * 60 * 1000); // 2 minutes from now (within 5min buffer)
71 const sessionData = createTestSessionData({ tokenExpiresAt: soonTime });
72 const session = new Session(sessionData);
73
74 assertEquals(session.isExpired, true);
75 });
76
77 await t.step("should not be expired for tokens expiring after 5 minutes", () => {
78 const laterTime = Date.now() + (10 * 60 * 1000); // 10 minutes from now (after 5min buffer)
79 const sessionData = createTestSessionData({ tokenExpiresAt: laterTime });
80 const session = new Session(sessionData);
81
82 assertEquals(session.isExpired, false);
83 });
84});
85
86Deno.test("Session - Time Until Expiry", async (t) => {
87 await t.step("should calculate correct time until expiry", () => {
88 const futureTime = Date.now() + (30 * 60 * 1000); // 30 minutes from now
89 const sessionData = createTestSessionData({ tokenExpiresAt: futureTime });
90 const session = new Session(sessionData);
91
92 const timeUntilExpiry = session.timeUntilExpiry;
93 // Allow small variance for test execution time
94 assertEquals(timeUntilExpiry > (29 * 60 * 1000), true);
95 assertEquals(timeUntilExpiry <= (30 * 60 * 1000), true);
96 });
97
98 await t.step("should return 0 for expired tokens", () => {
99 const pastTime = Date.now() - (60 * 60 * 1000); // 1 hour ago
100 const sessionData = createTestSessionData({ tokenExpiresAt: pastTime });
101 const session = new Session(sessionData);
102
103 assertEquals(session.timeUntilExpiry, 0);
104 });
105});
106
107Deno.test("Session - Serialization", async (t) => {
108 const originalData = createTestSessionData();
109 const session = new Session(originalData);
110
111 await t.step("toJSON should return session data", () => {
112 const jsonData = session.toJSON();
113 assertEquals(jsonData, originalData);
114 });
115
116 await t.step("fromJSON should create identical session", () => {
117 const jsonData = session.toJSON();
118 const restoredSession = Session.fromJSON(jsonData);
119
120 assertEquals(restoredSession.did, session.did);
121 assertEquals(restoredSession.handle, session.handle);
122 assertEquals(restoredSession.pdsUrl, session.pdsUrl);
123 assertEquals(restoredSession.accessToken, session.accessToken);
124 assertEquals(restoredSession.refreshToken, session.refreshToken);
125 assertEquals(restoredSession.isExpired, session.isExpired);
126 });
127
128 await t.step("round-trip serialization should preserve all data", () => {
129 const restoredSession = Session.fromJSON(session.toJSON());
130 assertEquals(restoredSession.toJSON(), originalData);
131 });
132});
133
134Deno.test("Session - Token Updates", async (t) => {
135 const sessionData = createTestSessionData();
136 const session = new Session(sessionData);
137 const originalRefreshToken = session.refreshToken;
138 const originalExpiry = session.timeUntilExpiry;
139
140 await t.step("updateTokens should update access token and expiry", () => {
141 const newTokens = {
142 accessToken: "new_access_token",
143 expiresIn: 7200, // 2 hours (longer than original 1 hour)
144 };
145
146 session.updateTokens(newTokens);
147
148 assertEquals(session.accessToken, "new_access_token");
149 assertEquals(session.refreshToken, originalRefreshToken); // Should remain unchanged
150
151 // New expiry should be roughly 2 hours from now (longer than original)
152 const newExpiry = session.timeUntilExpiry;
153 assertEquals(newExpiry > originalExpiry, true);
154 assertEquals(newExpiry > (110 * 60 * 1000), true); // At least 110 minutes
155 assertEquals(newExpiry <= (120 * 60 * 1000), true); // At most 120 minutes
156 });
157
158 await t.step("updateTokens should update refresh token when provided", () => {
159 const newTokens = {
160 accessToken: "newer_access_token",
161 refreshToken: "new_refresh_token",
162 expiresIn: 1800, // 30 minutes
163 };
164
165 session.updateTokens(newTokens);
166
167 assertEquals(session.accessToken, "newer_access_token");
168 assertEquals(session.refreshToken, "new_refresh_token");
169
170 // Expiry should be roughly 30 minutes from now
171 const newExpiry = session.timeUntilExpiry;
172 assertEquals(newExpiry > (25 * 60 * 1000), true); // At least 25 minutes
173 assertEquals(newExpiry <= (30 * 60 * 1000), true); // At most 30 minutes
174 });
175});
176
177Deno.test("Session - Edge Cases", async (t) => {
178 await t.step("should handle zero expiry time", () => {
179 const sessionData = createTestSessionData({ tokenExpiresAt: 0 });
180 const session = new Session(sessionData);
181
182 assertEquals(session.isExpired, true);
183 assertEquals(session.timeUntilExpiry, 0);
184 });
185
186 await t.step("should handle very large expiry time", () => {
187 const farFuture = Date.now() + (365 * 24 * 60 * 60 * 1000); // 1 year from now
188 const sessionData = createTestSessionData({ tokenExpiresAt: farFuture });
189 const session = new Session(sessionData);
190
191 assertEquals(session.isExpired, false);
192 assertEquals(session.timeUntilExpiry > (364 * 24 * 60 * 60 * 1000), true);
193 });
194
195 await t.step("should handle minimal session data", () => {
196 const minimalData = createTestSessionData({
197 did: "did:minimal",
198 handle: "minimal.test",
199 pdsUrl: "https://minimal.test",
200 accessToken: "min_access",
201 refreshToken: "min_refresh",
202 });
203 const session = new Session(minimalData);
204
205 assertEquals(session.did, "did:minimal");
206 assertEquals(session.handle, "minimal.test");
207 assertEquals(session.pdsUrl, "https://minimal.test");
208 assertEquals(session.accessToken, "min_access");
209 assertEquals(session.refreshToken, "min_refresh");
210 });
211});