|
| 1 | +import type { JWT, TokenResource } from '@clerk/shared/types'; |
| 2 | +import { describe, expect, it } from 'vitest'; |
| 3 | + |
| 4 | +import { pickFreshestJwt } from '../tokenFreshness'; |
| 5 | + |
| 6 | +function makeToken(opts: { oiat?: number; iat?: number } = {}): TokenResource { |
| 7 | + return { |
| 8 | + jwt: { |
| 9 | + header: { alg: 'RS256', kid: 'kid_1', ...(opts.oiat != null ? { oiat: opts.oiat } : {}) }, |
| 10 | + claims: { ...(opts.iat != null ? { iat: opts.iat } : {}) }, |
| 11 | + }, |
| 12 | + getRawString: () => 'mock-jwt', |
| 13 | + } as unknown as TokenResource; |
| 14 | +} |
| 15 | + |
| 16 | +function makeJwt(opts: { oiat?: number; iat?: number } = {}): JWT { |
| 17 | + return { |
| 18 | + header: { alg: 'RS256', kid: 'kid_1', ...(opts.oiat != null ? { oiat: opts.oiat } : {}) }, |
| 19 | + claims: { ...(opts.iat != null ? { iat: opts.iat } : {}) }, |
| 20 | + } as unknown as JWT; |
| 21 | +} |
| 22 | + |
| 23 | +describe('pickFreshestJwt', () => { |
| 24 | + describe('both have oiat (the only reachable path post-rollout)', () => { |
| 25 | + it('picks existing when existing oiat > incoming oiat', () => { |
| 26 | + const existing = makeToken({ oiat: 100 }); |
| 27 | + const incoming = makeToken({ oiat: 90 }); |
| 28 | + expect(pickFreshestJwt(existing, incoming)).toBe(existing); |
| 29 | + }); |
| 30 | + |
| 31 | + it('picks incoming when existing oiat < incoming oiat', () => { |
| 32 | + const existing = makeToken({ oiat: 90 }); |
| 33 | + const incoming = makeToken({ oiat: 100 }); |
| 34 | + expect(pickFreshestJwt(existing, incoming)).toBe(incoming); |
| 35 | + }); |
| 36 | + |
| 37 | + it('picks existing when oiat equal and existing iat > incoming iat', () => { |
| 38 | + const existing = makeToken({ oiat: 100, iat: 200 }); |
| 39 | + const incoming = makeToken({ oiat: 100, iat: 150 }); |
| 40 | + expect(pickFreshestJwt(existing, incoming)).toBe(existing); |
| 41 | + }); |
| 42 | + |
| 43 | + it('picks incoming when oiat equal and existing iat < incoming iat', () => { |
| 44 | + const existing = makeToken({ oiat: 100, iat: 150 }); |
| 45 | + const incoming = makeToken({ oiat: 100, iat: 200 }); |
| 46 | + expect(pickFreshestJwt(existing, incoming)).toBe(incoming); |
| 47 | + }); |
| 48 | + |
| 49 | + it('picks incoming when oiat equal and iat equal (other claims may differ)', () => { |
| 50 | + // Two tokens with identical oiat+iat may still differ in other claims |
| 51 | + // (azp, org_id, etc.) during a token-format rollout. Only suppress when |
| 52 | + // existing is strictly fresher; on full ties, let incoming through. |
| 53 | + const existing = makeToken({ oiat: 100, iat: 150 }); |
| 54 | + const incoming = makeToken({ oiat: 100, iat: 150 }); |
| 55 | + expect(pickFreshestJwt(existing, incoming)).toBe(incoming); |
| 56 | + }); |
| 57 | + |
| 58 | + it('picks existing when oiat equal and incoming iat missing (treated as 0)', () => { |
| 59 | + const existing = makeToken({ oiat: 100, iat: 150 }); |
| 60 | + const incoming = makeToken({ oiat: 100 }); |
| 61 | + expect(pickFreshestJwt(existing, incoming)).toBe(existing); |
| 62 | + }); |
| 63 | + }); |
| 64 | + |
| 65 | + describe('legacy (missing oiat) safety net', () => { |
| 66 | + it('picks existing when incoming is legacy (no oiat) and existing has oiat', () => { |
| 67 | + const existing = makeToken({ oiat: 100 }); |
| 68 | + const incoming = makeToken({ iat: 9999 }); |
| 69 | + expect(pickFreshestJwt(existing, incoming)).toBe(existing); |
| 70 | + }); |
| 71 | + |
| 72 | + it('picks incoming when existing is legacy and incoming has oiat', () => { |
| 73 | + const existing = makeToken({ iat: 9999 }); |
| 74 | + const incoming = makeToken({ oiat: 100 }); |
| 75 | + expect(pickFreshestJwt(existing, incoming)).toBe(incoming); |
| 76 | + }); |
| 77 | + |
| 78 | + it('picks incoming when both sides are legacy (cannot rank, safe default)', () => { |
| 79 | + const existing = makeToken({ iat: 200 }); |
| 80 | + const incoming = makeToken({ iat: 100 }); |
| 81 | + expect(pickFreshestJwt(existing, incoming)).toBe(incoming); |
| 82 | + }); |
| 83 | + }); |
| 84 | + |
| 85 | + describe('same object reference', () => { |
| 86 | + // When the cache hands back the same object that is already stored as |
| 87 | + // lastActiveToken, callers use `pickFreshestJwt(a, b) === a` to detect |
| 88 | + // "existing won, suppress redundant emit". This test documents that |
| 89 | + // intentional behavior. |
| 90 | + it('returns the same reference when both args are the same object', () => { |
| 91 | + const token = makeToken({ oiat: 100, iat: 150 }); |
| 92 | + expect(pickFreshestJwt(token, token)).toBe(token); |
| 93 | + }); |
| 94 | + }); |
| 95 | + |
| 96 | + describe('JWT input (cookie path)', () => { |
| 97 | + it('accepts raw decoded JWT for both arguments', () => { |
| 98 | + const a = makeJwt({ oiat: 100 }); |
| 99 | + const b = makeJwt({ oiat: 200 }); |
| 100 | + expect(pickFreshestJwt(a, b)).toBe(b); |
| 101 | + expect(pickFreshestJwt(b, a)).toBe(b); |
| 102 | + }); |
| 103 | + |
| 104 | + it('tie-breaks by iat on equal oiat for raw JWT inputs', () => { |
| 105 | + const a = makeJwt({ oiat: 100, iat: 150 }); |
| 106 | + const b = makeJwt({ oiat: 100, iat: 200 }); |
| 107 | + expect(pickFreshestJwt(a, b)).toBe(b); |
| 108 | + expect(pickFreshestJwt(b, a)).toBe(b); |
| 109 | + }); |
| 110 | + }); |
| 111 | +}); |
0 commit comments