1
1
import type { TokenResource } from '@clerk/types' ;
2
- import { afterAll , beforeAll , describe , expect , it , vi } from 'vitest' ;
2
+ import { afterAll , afterEach , beforeAll , describe , expect , it , vi } from 'vitest' ;
3
3
4
4
import { Token } from '../resources/internal' ;
5
5
import { SessionTokenCache } from '../tokenCache' ;
@@ -16,6 +16,30 @@ vi.mock('../resources/Base', () => {
16
16
const jwt =
17
17
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NzU4NzY3OTAsImRhdGEiOiJmb29iYXIiLCJpYXQiOjE2NzU4NzY3MzB9.Z1BC47lImYvaAtluJlY-kBo0qOoAk42Xb-gNrB2SxJg' ;
18
18
19
+ // Helper function to create JWT with custom exp and iat values using the same structure as the working JWT
20
+ function createJwtWithTtl ( ttlSeconds : number ) : string {
21
+ // Use the existing JWT as template
22
+ const baseJwt =
23
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NzU4NzY3OTAsImRhdGEiOiJmb29iYXIiLCJpYXQiOjE2NzU4NzY3MzB9.Z1BC47lImYvaAtluJlY-kBo0qOoAk42Xb-gNrB2SxJg' ;
24
+ const [ headerB64 , , signature ] = baseJwt . split ( '.' ) ;
25
+
26
+ // Use the same iat as the original working JWT to maintain consistency with test environment
27
+ // Original JWT: iat: 1675876730, exp: 1675876790 (60 second TTL)
28
+ const baseIat = 1675876730 ;
29
+ const payload = {
30
+ exp : baseIat + ttlSeconds ,
31
+ data : 'foobar' , // Keep same data as original
32
+ iat : baseIat ,
33
+ } ;
34
+
35
+ // Encode the new payload using base64url encoding (like JWT standard)
36
+ const payloadString = JSON . stringify ( payload ) ;
37
+ // Use proper base64url encoding: standard base64 but replace + with -, / with _, and remove padding =
38
+ const newPayloadB64 = btoa ( payloadString ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = / g, '' ) ;
39
+
40
+ return `${ headerB64 } .${ newPayloadB64 } .${ signature } ` ;
41
+ }
42
+
19
43
describe ( 'MemoryTokenCache' , ( ) => {
20
44
beforeAll ( ( ) => {
21
45
vi . useFakeTimers ( ) ;
@@ -163,4 +187,85 @@ describe('MemoryTokenCache', () => {
163
187
expect ( cache . get ( key , 0 ) ) . toBeUndefined ( ) ;
164
188
} ) ;
165
189
} ) ;
190
+
191
+ describe ( 'dynamic TTL calculation' , ( ) => {
192
+ let dateNowSpy : ReturnType < typeof vi . spyOn > ;
193
+
194
+ afterEach ( ( ) => {
195
+ dateNowSpy . mockRestore ( ) ;
196
+ } ) ;
197
+
198
+ it ( 'calculates expiresIn from JWT exp and iat claims and sets timeout based on calculated TTL' , async ( ) => {
199
+ const cache = SessionTokenCache ;
200
+
201
+ // Mock Date.now to return a fixed timestamp initially
202
+ const initialTime = 1675876730000 ; // Same as our JWT's iat in milliseconds
203
+ dateNowSpy = vi . spyOn ( Date , 'now' ) . mockImplementation ( ( ) => initialTime ) ;
204
+
205
+ // Test with a 30-second TTL
206
+ const shortTtlJwt = createJwtWithTtl ( 30 ) ;
207
+ const shortTtlToken = new Token ( {
208
+ object : 'token' ,
209
+ id : 'short-ttl' ,
210
+ jwt : shortTtlJwt ,
211
+ } ) ;
212
+
213
+ const shortTtlKey = { tokenId : 'short-ttl' , audience : 'test' } ;
214
+ const shortTtlResolver = Promise . resolve ( shortTtlToken ) ;
215
+ cache . set ( { ...shortTtlKey , tokenResolver : shortTtlResolver } ) ;
216
+ await shortTtlResolver ;
217
+
218
+ const cachedEntry = cache . get ( shortTtlKey ) ;
219
+ expect ( cachedEntry ) . toMatchObject ( shortTtlKey ) ;
220
+
221
+ // Advance both the timer and the mocked current time
222
+ const advanceBy = 31 * 1000 ;
223
+ vi . advanceTimersByTime ( advanceBy ) ;
224
+ dateNowSpy . mockImplementation ( ( ) => initialTime + advanceBy ) ;
225
+
226
+ const cachedEntry2 = cache . get ( shortTtlKey ) ;
227
+ expect ( cachedEntry2 ) . toBeUndefined ( ) ;
228
+ } ) ;
229
+
230
+ it ( 'handles tokens with TTL greater than 60 seconds correctly' , async ( ) => {
231
+ const cache = SessionTokenCache ;
232
+
233
+ // Mock Date.now to return a fixed timestamp initially
234
+ const initialTime = 1675876730000 ; // Same as our JWT's iat in milliseconds
235
+ dateNowSpy = vi . spyOn ( Date , 'now' ) . mockImplementation ( ( ) => initialTime ) ;
236
+
237
+ // Test with a 120-second TTL
238
+ const longTtlJwt = createJwtWithTtl ( 120 ) ;
239
+ const longTtlToken = new Token ( {
240
+ object : 'token' ,
241
+ id : 'long-ttl' ,
242
+ jwt : longTtlJwt ,
243
+ } ) ;
244
+
245
+ const longTtlKey = { tokenId : 'long-ttl' , audience : 'test' } ;
246
+ const longTtlResolver = Promise . resolve ( longTtlToken ) ;
247
+ cache . set ( { ...longTtlKey , tokenResolver : longTtlResolver } ) ;
248
+ await longTtlResolver ;
249
+
250
+ // Check token is cached initially
251
+ const cachedEntry = cache . get ( longTtlKey ) ;
252
+ expect ( cachedEntry ) . toMatchObject ( longTtlKey ) ;
253
+
254
+ // Advance 90 seconds - token should still be cached
255
+ const firstAdvance = 90 * 1000 ;
256
+ vi . advanceTimersByTime ( firstAdvance ) ;
257
+ dateNowSpy . mockImplementation ( ( ) => initialTime + firstAdvance ) ;
258
+
259
+ const cachedEntryAfter90s = cache . get ( longTtlKey ) ;
260
+ expect ( cachedEntryAfter90s ) . toMatchObject ( longTtlKey ) ;
261
+
262
+ // Advance to 121 seconds - token should be removed
263
+ const secondAdvance = 31 * 1000 ;
264
+ vi . advanceTimersByTime ( secondAdvance ) ;
265
+ dateNowSpy . mockImplementation ( ( ) => initialTime + firstAdvance + secondAdvance ) ;
266
+
267
+ const cachedEntryAfter121s = cache . get ( longTtlKey ) ;
268
+ expect ( cachedEntryAfter121s ) . toBeUndefined ( ) ;
269
+ } ) ;
270
+ } ) ;
166
271
} ) ;
0 commit comments