JwtPayload already declares iat?: number, so the & { iat?: number } in
makeRequireAuth and slideSessionIfNeeded was a no-op. The unit test had a
local TestPayload duplicating the same shape — replaced with the canonical
import.
118 lines
4.4 KiB
TypeScript
118 lines
4.4 KiB
TypeScript
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<FastifyInstance> {
|
|
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<string, unknown> }> } & Record<string, unknown> {
|
|
const setCookieCalls: Array<{ name: string; value: string; opts: Record<string, unknown> }> = [];
|
|
return {
|
|
setCookieCalls,
|
|
setCookie(name: string, value: string, opts: Record<string, unknown>) {
|
|
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');
|
|
});
|
|
});
|