import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import Fastify, { type FastifyInstance } from 'fastify'; import fastifyCookie from '@fastify/cookie'; import fastifyJwt from '@fastify/jwt'; import { slideSessionIfNeeded } from '../../src/middleware/session-renewal.ts'; import type { JwtPayload } from '../../src/types.ts'; import { SESSION_COOKIE_NAME, SESSION_RENEW_THRESHOLD_SECONDS, SESSION_TTL_SECONDS, } from '../../src/constants.ts'; async function buildTestServer(): Promise { const app = Fastify(); await app.register(fastifyCookie); await app.register(fastifyJwt, { secret: 'test-secret', cookie: { cookieName: SESSION_COOKIE_NAME, signed: false }, }); await app.ready(); return app; } const NOW_ISO = '2026-01-01T12:00:00Z'; const NOW_SEC = Math.floor(new Date(NOW_ISO).getTime() / 1000); describe('slideSessionIfNeeded', () => { let app: FastifyInstance; beforeEach(async () => { vi.useFakeTimers({ toFake: ['Date'] }); vi.setSystemTime(new Date(NOW_ISO)); app = await buildTestServer(); }); afterEach(async () => { vi.useRealTimers(); await app.close(); }); function fakeReply(): { setCookieCalls: Array<{ name: string; value: string; opts: Record }> } & Record { const setCookieCalls: Array<{ name: string; value: string; opts: Record }> = []; return { setCookieCalls, setCookie(name: string, value: string, opts: Record) { setCookieCalls.push({ name, value, opts }); return this; }, }; } function fakeRequest(url: string): { url: string } { return { url }; } it('re-issues cookie when now - iat >= SESSION_RENEW_THRESHOLD_SECONDS', () => { const reply = fakeReply(); const payload: JwtPayload = { sub: 1, username: 'alice', iat: NOW_SEC - SESSION_RENEW_THRESHOLD_SECONDS, }; slideSessionIfNeeded(fakeRequest('/upload') as never, reply as never, payload, app, false); expect(reply.setCookieCalls.length).toBe(1); expect(reply.setCookieCalls[0].name).toBe(SESSION_COOKIE_NAME); expect(reply.setCookieCalls[0].opts.maxAge).toBe(SESSION_TTL_SECONDS); }); it('skips re-issue when now - iat < SESSION_RENEW_THRESHOLD_SECONDS', () => { const reply = fakeReply(); const payload: JwtPayload = { sub: 1, username: 'alice', iat: NOW_SEC - (SESSION_RENEW_THRESHOLD_SECONDS - 1), }; slideSessionIfNeeded(fakeRequest('/upload') as never, reply as never, payload, app, false); expect(reply.setCookieCalls.length).toBe(0); }); it('skips re-issue on /logout path', () => { const reply = fakeReply(); const payload: JwtPayload = { sub: 1, username: 'alice', iat: 0 }; // ancient slideSessionIfNeeded(fakeRequest('/logout') as never, reply as never, payload, app, false); expect(reply.setCookieCalls.length).toBe(0); }); it('skips re-issue on /api/v1/auth/logout path', () => { const reply = fakeReply(); const payload: JwtPayload = { sub: 1, username: 'alice', iat: 0 }; slideSessionIfNeeded(fakeRequest('/api/v1/auth/logout') as never, reply as never, payload, app, false); expect(reply.setCookieCalls.length).toBe(0); }); it('skips re-issue when /logout has a query string', () => { const reply = fakeReply(); const payload: JwtPayload = { sub: 1, username: 'alice', iat: 0 }; slideSessionIfNeeded(fakeRequest('/logout?redirect=foo') as never, reply as never, payload, app, false); expect(reply.setCookieCalls.length).toBe(0); }); it('treats missing iat as 0 (forces refresh)', () => { const reply = fakeReply(); const payload: JwtPayload = { sub: 1, username: 'alice' }; slideSessionIfNeeded(fakeRequest('/upload') as never, reply as never, payload, app, false); expect(reply.setCookieCalls.length).toBe(1); }); it('produces a cookie carrying Max-Age=SESSION_TTL_SECONDS', () => { const reply = fakeReply(); const payload: JwtPayload = { sub: 1, username: 'alice', iat: 0 }; slideSessionIfNeeded(fakeRequest('/upload') as never, reply as never, payload, app, true); expect(reply.setCookieCalls[0].opts.maxAge).toBe(SESSION_TTL_SECONDS); expect(reply.setCookieCalls[0].opts.secure).toBe(true); expect(reply.setCookieCalls[0].opts.httpOnly).toBe(true); expect(reply.setCookieCalls[0].opts.sameSite).toBe('strict'); }); });