'use strict'; // Set secrets BEFORE requiring — jwt.sign/verify reads process.env at call time // but we want a clean, isolated secret that does not change across test runs. process.env.JWT_SECRET = 'test-jwt-secret-32chars-padding!!'; process.env.JWT_REFRESH_SECRET = 'test-refresh-secret-32chars-pad!!'; process.env.JWT_EXPIRES_IN = '15m'; process.env.JWT_REFRESH_EXPIRES_IN = '7d'; const jwt = require('jsonwebtoken'); const { generateTokens, verifyAccessToken, verifyRefreshToken, hashToken, shouldRotateRefreshToken, } = require('../../utils/token.util'); const MOCK_USER = { user_id: 1, email: 'test@example.com', acc_type: 'user', reg_type: 'system' }; // ── generateTokens ──────────────────────────────────────────────────────────── describe('generateTokens()', () => { test('returns accessToken and refreshToken strings', () => { const { accessToken, refreshToken } = generateTokens(MOCK_USER); expect(typeof accessToken).toBe('string'); expect(typeof refreshToken).toBe('string'); }); test('accessToken contains correct payload fields', () => { const { accessToken } = generateTokens(MOCK_USER); const decoded = jwt.decode(accessToken); expect(decoded.user_id).toBe(MOCK_USER.user_id); expect(decoded.email).toBe(MOCK_USER.email); expect(decoded.acc_type).toBe(MOCK_USER.acc_type); expect(decoded.reg_type).toBe(MOCK_USER.reg_type); }); test('refreshToken contains only user_id', () => { const { refreshToken } = generateTokens(MOCK_USER); const decoded = jwt.decode(refreshToken); expect(decoded.user_id).toBe(MOCK_USER.user_id); expect(decoded.email).toBeUndefined(); }); }); // ── verifyAccessToken ───────────────────────────────────────────────────────── describe('verifyAccessToken()', () => { test('valid token → returns decoded payload', () => { const { accessToken } = generateTokens(MOCK_USER); const decoded = verifyAccessToken(accessToken); expect(decoded.user_id).toBe(MOCK_USER.user_id); }); test('tampered token → throws JsonWebTokenError', () => { const { accessToken } = generateTokens(MOCK_USER); expect(() => verifyAccessToken(accessToken + 'tampered')).toThrow(); }); test('expired token → throws TokenExpiredError', () => { const expired = jwt.sign( { user_id: 99, exp: Math.floor(Date.now() / 1000) - 10 }, process.env.JWT_SECRET ); let err; try { verifyAccessToken(expired); } catch (e) { err = e; } expect(err.name).toBe('TokenExpiredError'); }); test('token signed with wrong secret → throws', () => { const bad = jwt.sign({ user_id: 1 }, 'wrong-secret'); expect(() => verifyAccessToken(bad)).toThrow(); }); }); // ── verifyRefreshToken ──────────────────────────────────────────────────────── describe('verifyRefreshToken()', () => { test('valid refresh token → returns payload with user_id', () => { const { refreshToken } = generateTokens(MOCK_USER); const decoded = verifyRefreshToken(refreshToken); expect(decoded.user_id).toBe(MOCK_USER.user_id); }); test('access token used as refresh token → throws (different secret)', () => { const { accessToken } = generateTokens(MOCK_USER); expect(() => verifyRefreshToken(accessToken)).toThrow(); }); }); // ── hashToken ───────────────────────────────────────────────────────────────── describe('hashToken()', () => { test('returns a 64-character hex string (SHA-256)', () => { expect(hashToken('some-token')).toMatch(/^[a-f0-9]{64}$/); }); test('is deterministic — same input produces same hash', () => { expect(hashToken('abc')).toBe(hashToken('abc')); }); test('different inputs produce different hashes', () => { expect(hashToken('token-a')).not.toBe(hashToken('token-b')); }); }); // ── shouldRotateRefreshToken ────────────────────────────────────────────────── describe('shouldRotateRefreshToken()', () => { const now = Math.floor(Date.now() / 1000); test('expires in 10 minutes (within 1-day threshold) → should rotate', () => { const decoded = { exp: now + 600 }; expect(shouldRotateRefreshToken(decoded, 1)).toBe(true); }); test('expires in 5 days (well beyond 1-day threshold) → should not rotate', () => { const decoded = { exp: now + 5 * 24 * 60 * 60 }; expect(shouldRotateRefreshToken(decoded, 1)).toBe(false); }); test('custom threshold is respected', () => { const decoded = { exp: now + 2 * 24 * 60 * 60 }; // 2 days left expect(shouldRotateRefreshToken(decoded, 3)).toBe(true); // 3-day threshold → rotate expect(shouldRotateRefreshToken(decoded, 1)).toBe(false); // 1-day threshold → don't rotate }); });