import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as jwt from 'jsonwebtoken'; const jwtSecret = vi.hoisted(() => { const secret = 'a'.repeat(32); process.env.JWT_SECRET = secret; return secret; }); vi.mock('../src/auth', async (importOriginal) => { return await importOriginal(); }); vi.mock('../src/logger', () => ({ Logger: { info: vi.fn(), warn: vi.fn(), debug: vi.fn(), error: vi.fn(), }, })); import { verifyToken, revokeAllTokens } from '../src/auth'; import { authCache } from '../src/auth-cache'; import { prisma } from '../src/db'; const createToken = (tokenVersion: number = 0): string => jwt.sign({ userId: 1, email: 'user@example.com', tokenVersion }, jwtSecret, { expiresIn: '1h', }); describe('auth verification cache', () => { beforeEach(() => { vi.clearAllMocks(); authCache.clear(); }); it('should reuse a warm verified-token cache entry', async () => { vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: 1, tokenVersion: 0, isVerified: 1, } as any); const token = createToken(); await expect(verifyToken(token)).resolves.toEqual({ valid: true, userId: 1, email: 'user@example.com', }); await expect(verifyToken(token)).resolves.toEqual({ valid: true, userId: 1, email: 'user@example.com', }); expect(prisma.user.findUnique).toHaveBeenCalledTimes(1); }); it('should fall through to the database on tokenVersion mismatch', async () => { authCache.set(1, 1, true); vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: 1, tokenVersion: 0, isVerified: 1, } as any); await expect(verifyToken(createToken(0))).resolves.toEqual( expect.objectContaining({ valid: true }), ); expect(prisma.user.findUnique).toHaveBeenCalledTimes(1); }); it('should invalidate the cache when revoking all tokens', async () => { vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: 1, tokenVersion: 0, isVerified: 1, } as any); const token = createToken(); await verifyToken(token); expect(prisma.user.findUnique).toHaveBeenCalledTimes(1); vi.mocked(prisma.user.update).mockResolvedValue({} as any); await revokeAllTokens(1); vi.mocked(prisma.user.findUnique).mockClear(); await verifyToken(token); expect(prisma.user.findUnique).toHaveBeenCalledTimes(1); }); it('should not re-cache a token when invalidation happens during verification', async () => { const token = createToken(0); let resolveFindUnique!: (value: { id: number; tokenVersion: number; isVerified: number; }) => void; vi.mocked(prisma.user.findUnique) .mockReturnValueOnce( new Promise((resolve) => { resolveFindUnique = resolve; }) as ReturnType, ) .mockResolvedValueOnce({ id: 1, tokenVersion: 1, isVerified: 1, } as any); vi.mocked(prisma.user.update).mockResolvedValue({} as any); const inFlightVerification = verifyToken(token); await Promise.resolve(); await revokeAllTokens(1); resolveFindUnique({ id: 1, tokenVersion: 0, isVerified: 1 }); await expect(inFlightVerification).resolves.toEqual( expect.objectContaining({ valid: true }), ); await expect(verifyToken(token)).resolves.toEqual({ valid: false, reason: 'Token was revoked. Please log in again to get a new token.', }); expect(prisma.user.findUnique).toHaveBeenCalledTimes(2); }); it('bounds invalidationVersions and keeps recent invalidations newest', () => { // A long-ago invalidation must not pin heap forever. authCache.invalidate(1); expect(authCache.getInvalidationVersion(1)).toBe(1); // Push >10k distinct invalidations so user 1 (the oldest) is evicted. for (let userId = 2; userId <= 10_002; userId++) { authCache.invalidate(userId); } // Evicted -> reverts to the default (0). Memory is bounded. expect(authCache.getInvalidationVersion(1)).toBe(0); // A freshly-invalidated user sits at the MRU tail and is retained, so the // CAS race protection still holds for the window that matters. expect(authCache.getInvalidationVersion(10_002)).toBe(1); }); });