import { describe, it, expect, beforeAll, afterAll } from "vitest"; import type { FastifyRequest } from "fastify"; import prisma from "../config/database"; import { refreshAccessToken, hashToken } from "../services/auth"; /** * Pinning tests for audit finding H1: a session terminated via the sessions * routes (replaced_at set, NO replaced_by_hash) presented on a later refresh * is NOT token theft — it must be rejected with a plain 401 WITHOUT revoking * the user's other sessions. Real theft (replay of a ROTATED token, i.e. * replaced_by_hash set) must keep revoking the whole family. */ const N = "sess_term_"; const stamp = Date.now().toString(36); const fakeReq = { log: { warn: () => {} }, ip: "127.0.0.1", headers: {}, } as unknown as FastifyRequest; let userId: number; const rawA = `${N}rawA_${stamp}`; const rawB = `${N}rawB_${stamp}`; const rawC = `${N}rawC_${stamp}`; const rawD = `${N}rawD_${stamp}`; let tokenAId: number; let tokenDId: number; async function cleanup() { const users = await prisma.users.findMany({ where: { username: { startsWith: N } }, select: { id: true }, }); const ids = users.map((u) => u.id); if (ids.length > 0) { await prisma.refresh_tokens.deleteMany({ where: { user_id: { in: ids } } }); await prisma.users.deleteMany({ where: { id: { in: ids } } }); } } beforeAll(async () => { await cleanup(); const role = await prisma.roles.findFirst(); if (!role) throw new Error("Test setup: no roles found — seed the database"); const user = await prisma.users.create({ data: { username: `${N}user_${stamp}`, email: `${N}${stamp}@test.local`, password_hash: "$2a$10$invalidinvalidinvalidinvalidinvalidinvalidinvalidinvali", first_name: "Session", last_name: "Terminate", is_active: true, role_id: role.id, }, }); userId = user.id; const future = new Date(Date.now() + 7 * 24 * 3600 * 1000); const tokenA = await prisma.refresh_tokens.create({ data: { user_id: userId, token_hash: hashToken(rawA), expires_at: future, remember_me: false, }, }); tokenAId = tokenA.id; // Token B: terminated the way DELETE /sessions/:id does it — replaced_at // ONLY, no replaced_by_hash, row kept. await prisma.refresh_tokens.create({ data: { user_id: userId, token_hash: hashToken(rawB), expires_at: future, remember_me: false, replaced_at: new Date(), }, }); // Token C: legitimately ROTATED (both replaced_at and replaced_by_hash set) // — replaying it is the theft signature. await prisma.refresh_tokens.create({ data: { user_id: userId, token_hash: hashToken(rawC), expires_at: future, remember_me: false, replaced_at: new Date(), replaced_by_hash: hashToken(`${N}descendant_${stamp}`), }, }); const tokenD = await prisma.refresh_tokens.create({ data: { user_id: userId, token_hash: hashToken(rawD), expires_at: future, remember_me: false, }, }); tokenDId = tokenD.id; }); afterAll(async () => { await cleanup(); await prisma.$disconnect(); }); describe("refreshAccessToken vs terminated sessions (audit H1)", () => { it("rejects a manually terminated token with 401 WITHOUT revoking other sessions", async () => { const result = await refreshAccessToken(rawB, fakeReq); expect(result.type).toBe("error"); if (result.type === "error") expect(result.status).toBe(401); // The user's OTHER session must survive — termination is not theft. const tokenA = await prisma.refresh_tokens.findUnique({ where: { id: tokenAId }, }); expect(tokenA).not.toBeNull(); }); it("still revokes the whole family on replay of a ROTATED token (theft)", async () => { const result = await refreshAccessToken(rawC, fakeReq); expect(result.type).toBe("error"); // Breach containment must keep working: every session of the user is // gone, including the otherwise-valid token D. const tokenD = await prisma.refresh_tokens.findUnique({ where: { id: tokenDId }, }); expect(tokenD).toBeNull(); }); });