import { createHash, randomUUID } from "node:crypto"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import request from "supertest"; import type { FastifyInstance } from "fastify"; import { PrismaClient } from "@prisma/client"; import argon2 from "argon2"; import { buildApp } from "../src/server"; const prisma = new PrismaClient(); let app: FastifyInstance; beforeAll(async () => { app = await buildApp({ AUTH_DISABLED: true, SEED_DEFAULT_BUDGET: false }); await app.ready(); }); afterAll(async () => { if (app) { await app.close(); } await prisma.$disconnect(); }); describe("Account delete access control", () => { it("rejects confirm-delete when payload email targets another user", async () => { const attackerEmail = `attacker-${Date.now()}@test.dev`; const victimEmail = `victim-${Date.now()}@test.dev`; const victimPassword = "VictimPass123!"; const deleteCode = "654321"; const [attacker, victim] = await Promise.all([ prisma.user.create({ data: { email: attackerEmail, passwordHash: await argon2.hash("AttackerPass123!"), emailVerified: true, }, }), prisma.user.create({ data: { email: victimEmail, passwordHash: await argon2.hash(victimPassword), emailVerified: true, }, }), ]); try { await prisma.emailToken.create({ data: { userId: victim.id, type: "delete", tokenHash: createHash("sha256").update(deleteCode).digest("hex"), expiresAt: new Date(Date.now() + 60_000), }, }); const csrf = randomUUID().replace(/-/g, ""); const res = await request(app.server) .post("/account/confirm-delete") .set("x-user-id", attacker.id) .set("x-csrf-token", csrf) .set("Cookie", `csrf=${csrf}`) .send({ email: victim.email, code: deleteCode, password: victimPassword, }); expect(res.status).toBe(403); const victimStillExists = await prisma.user.findUnique({ where: { id: victim.id }, select: { id: true }, }); expect(victimStillExists?.id).toBe(victim.id); } finally { await prisma.user.deleteMany({ where: { id: { in: [attacker.id, victim.id] } }, }); } }); });