mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
129 lines
5.2 KiB
JavaScript
129 lines
5.2 KiB
JavaScript
'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
|
|
});
|
|
});
|