|
| 1 | +import { describe, it, expect, vi, beforeEach } from 'vitest'; |
| 2 | +import { OAuth2Client } from 'arctic'; |
| 3 | +import { OAuth2ClientWithConfig } from './client'; |
| 4 | + |
| 5 | +// Build a JWT-shaped token (header.payload.signature) with a base64 payload. |
| 6 | +function createMockJWT(payload: Record<string, unknown>): string { |
| 7 | + const header = btoa(JSON.stringify({ alg: 'RS256', typ: 'JWT' })); |
| 8 | + const body = btoa(JSON.stringify(payload)); |
| 9 | + return `${header}.${body}.mock-signature`; |
| 10 | +} |
| 11 | + |
| 12 | +describe('OAuth2ClientWithConfig', () => { |
| 13 | + let client: OAuth2ClientWithConfig; |
| 14 | + |
| 15 | + beforeEach(() => { |
| 16 | + // arctic's OAuth2Client is globally mocked to return a plain object, which |
| 17 | + // would hijack `this` in the subclass constructor and strip its prototype |
| 18 | + // methods. Reset it to a no-op constructor so the real subclass instance is |
| 19 | + // built. |
| 20 | + vi.mocked(OAuth2Client).mockImplementation(function () {} as never); |
| 21 | + client = new OAuth2ClientWithConfig('id', 'secret', 'http://localhost/callback'); |
| 22 | + }); |
| 23 | + |
| 24 | + describe('constructor', () => { |
| 25 | + it('creates an instance exposing the custom methods', () => { |
| 26 | + expect(client.OIDCConfig).toBeUndefined(); |
| 27 | + expect(typeof client.checkAccessTokenExpiration).toBe('function'); |
| 28 | + }); |
| 29 | + }); |
| 30 | + |
| 31 | + describe('checkAccessTokenExpiration (fail closed)', () => { |
| 32 | + it('returns false for a token whose expiry is in the future', async () => { |
| 33 | + const token = createMockJWT({ exp: Math.floor(Date.now() / 1000) + 3600 }); |
| 34 | + await expect(client.checkAccessTokenExpiration(token)).resolves.toBe(false); |
| 35 | + }); |
| 36 | + |
| 37 | + it('returns true for an expired token', async () => { |
| 38 | + const token = createMockJWT({ exp: Math.floor(Date.now() / 1000) - 3600 }); |
| 39 | + await expect(client.checkAccessTokenExpiration(token)).resolves.toBe(true); |
| 40 | + }); |
| 41 | + |
| 42 | + it('treats a token without an exp claim as expired', async () => { |
| 43 | + const token = createMockJWT({ sub: 'user-1' }); |
| 44 | + await expect(client.checkAccessTokenExpiration(token)).resolves.toBe(true); |
| 45 | + }); |
| 46 | + |
| 47 | + it('treats an undecodable token as expired instead of throwing', async () => { |
| 48 | + await expect(client.checkAccessTokenExpiration('not.a.jwt')).resolves.toBe(true); |
| 49 | + }); |
| 50 | + |
| 51 | + it('treats a malformed JWT payload as expired instead of throwing', async () => { |
| 52 | + await expect( |
| 53 | + client.checkAccessTokenExpiration('eyJhbGciOiJSUzI1NiJ9.invalid-payload.sig') |
| 54 | + ).resolves.toBe(true); |
| 55 | + }); |
| 56 | + }); |
| 57 | +}); |
0 commit comments