/** * Unit tests for JWT token generation and verification logic. * Tests the jose-based JWT flow independent of the database. */ import { describe, it, expect } from 'bun:test'; import { SignJWT, jwtVerify } from 'jose'; const TEST_SECRET = new TextEncoder().encode( 'test-secret-for-unit-tests-at-least-16-chars', ); describe('JWT Token Generation', () => { it('should generate a valid JWT with expected claims', async () => { const token = await new SignJWT({ sub: 'user-001', email: 'test@example.com', role: 'admin', displayName: 'Test User', }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('7d') .sign(TEST_SECRET); expect(typeof token).toBe('string'); expect(token.split('.').length).toBe(3); // JWT has 3 parts // Verify the token const { payload } = await jwtVerify(token, TEST_SECRET); expect(payload.sub).toBe('user-001'); expect(payload.email).toBe('test@example.com'); expect(payload.role).toBe('admin'); expect(payload.displayName).toBe('Test User'); expect(payload.exp).toBeDefined(); expect(payload.iat).toBeDefined(); }); it('should reject tampered tokens', async () => { const token = await new SignJWT({ sub: 'user-001' }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('1h') .sign(TEST_SECRET); // Tamper with the token by changing a character const parts = token.split('.'); parts[1] = parts[1].slice(0, -1) + (parts[1].slice(-1) === 'A' ? 'B' : 'A'); const tampered = parts.join('.'); try { await jwtVerify(tampered, TEST_SECRET); expect(true).toBe(false); // Should not reach here } catch (err: any) { expect(err.code).toBe('ERR_JWS_SIGNATURE_VERIFICATION_FAILED'); } }); it('should reject tokens signed with wrong secret', async () => { const wrongSecret = new TextEncoder().encode('wrong-secret-that-is-long-enough'); const token = await new SignJWT({ sub: 'user-001' }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('1h') .sign(wrongSecret); try { await jwtVerify(token, TEST_SECRET); expect(true).toBe(false); } catch (err: any) { expect(err.code).toBe('ERR_JWS_SIGNATURE_VERIFICATION_FAILED'); } }); it('should reject expired tokens', async () => { const token = await new SignJWT({ sub: 'user-001' }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('0s') // Already expired .sign(TEST_SECRET); // Wait a tiny bit to ensure expiration await new Promise((resolve) => setTimeout(resolve, 10)); try { await jwtVerify(token, TEST_SECRET); expect(true).toBe(false); } catch (err: any) { expect(err.code).toBe('ERR_JWT_EXPIRED'); } }); it('should include all role types in valid token payloads', async () => { const roles = ['admin', 'manager', 'developer', 'viewer']; for (const role of roles) { const token = await new SignJWT({ sub: `user-${role}`, email: `${role}@test.com`, role, displayName: `Test ${role}`, }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('1h') .sign(TEST_SECRET); const { payload } = await jwtVerify(token, TEST_SECRET); expect(payload.role).toBe(role); } }); });