import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createTestDb } from "@parking/db/testing"; import { type Db } from "@parking/db"; import type { FastifyInstance } from "fastify"; import { buildServer } from "../server.js"; import { seedUser, login } from "../test-helpers.js"; // HTTP integration for soft delete + recycle bin: an admin DELETE soft-deletes (the user // leaves the list, can't log in), the bin lists it, restore brings it back, and a deleted // user can log in again. Drives the REAL app over a fresh in-memory DB via app.inject. let db: Db; let close: () => void; let app: FastifyInstance; beforeEach(async () => { const t = createTestDb(); db = t.db; close = t.close; app = await buildServer({ db }); await app.ready(); }); afterEach(async () => { await app.close(); close(); }); /** Log in an admin and return the auth headers for mutations. */ async function asAdmin() { const { username, password } = await seedUser(db, { username: "boss", roleId: "admin" }); const { cookie, csrf } = await login(app, username, password); return { cookie, csrf }; } describe("soft delete via the resource DELETE route", () => { it("DELETE /api/users/:id soft-deletes: user leaves the list and can't log in, but is restorable", async () => { const { cookie, csrf } = await asAdmin(); // Create a victim user to delete. await seedUser(db, { username: "victim", password: "victim-pass-123", roleId: "admin" }); const victim = (await app.inject({ method: "GET", url: "/api/users", headers: { cookie } })).json() .users.find((u: { username: string; id: string }) => u.username === "victim"); expect(victim).toBeDefined(); // Delete (soft). const del = await app.inject({ method: "DELETE", url: `/api/users/${victim.id}`, headers: { cookie, "x-csrf-token": csrf }, }); expect(del.statusCode).toBeLessThan(300); // Gone from the live list. const list = (await app.inject({ method: "GET", url: "/api/users", headers: { cookie } })).json(); expect(list.users.some((u: { username: string }) => u.username === "victim")).toBe(false); // Can't log in. const relogin = await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "victim", password: "victim-pass-123" } }); expect(relogin.statusCode).toBe(401); // Shows in the recycle bin. const bin = (await app.inject({ method: "GET", url: "/api/recycle-bin", headers: { cookie } })).json(); expect(bin.items.some((i: { kind: string; label: string }) => i.kind === "user" && i.label === "victim")).toBe(true); // Restore → reappears + can log in. const restore = await app.inject({ method: "POST", url: `/api/recycle-bin/user/${victim.id}/restore`, headers: { cookie, "x-csrf-token": csrf }, }); expect(restore.statusCode).toBeLessThan(300); const relogin2 = await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "victim", password: "victim-pass-123" } }); expect(relogin2.statusCode).toBe(200); }); it("purge permanently removes a soft-deleted user", async () => { const { cookie, csrf } = await asAdmin(); await seedUser(db, { username: "gone", password: "gone-pass-1234", roleId: "admin" }); const id = (await app.inject({ method: "GET", url: "/api/users", headers: { cookie } })).json() .users.find((u: { username: string }) => u.username === "gone").id; await app.inject({ method: "DELETE", url: `/api/users/${id}`, headers: { cookie, "x-csrf-token": csrf } }); const purge = await app.inject({ method: "DELETE", url: `/api/recycle-bin/user/${id}`, headers: { cookie, "x-csrf-token": csrf }, }); expect(purge.statusCode).toBe(204); const bin = (await app.inject({ method: "GET", url: "/api/recycle-bin", headers: { cookie } })).json(); expect(bin.items.some((i: { label: string }) => i.label === "gone")).toBe(false); }); it("the recycle bin is gated — a user without recyclebin:read is 403", async () => { const { username, password } = await seedUser(db, { username: "plain", roleId: "plain", permissions: ["user:read"], }); const { cookie } = await login(app, username, password); const res = await app.inject({ method: "GET", url: "/api/recycle-bin", headers: { cookie } }); expect(res.statusCode).toBe(403); }); it("recreating a user with a soft-deleted user's username gives a clear 409", async () => { const { cookie, csrf } = await asAdmin(); await seedUser(db, { username: "dup", password: "dup-pass-12345", roleId: "admin" }); const id = (await app.inject({ method: "GET", url: "/api/users", headers: { cookie } })).json() .users.find((u: { username: string }) => u.username === "dup").id; await app.inject({ method: "DELETE", url: `/api/users/${id}`, headers: { cookie, "x-csrf-token": csrf } }); const create = await app.inject({ method: "POST", url: "/api/users", headers: { cookie, "x-csrf-token": csrf }, payload: { username: "dup", password: "new-pass-12345", roleId: "admin" }, }); expect(create.statusCode).toBe(409); expect(create.json().error).toMatch(/recycle bin/i); }); });