import { describe, it, expect, beforeAll, afterAll } from "vitest"; import Fastify from "fastify"; import jwt from "jsonwebtoken"; import bankAccountsRoutes from "../routes/admin/bank-accounts"; import prisma from "../config/database"; import { config } from "../config/env"; // Fixture marker so cleanup never touches real data. const N = "bank_accounts_test_"; let app: ReturnType; let token: string; const createdIds: number[] = []; beforeAll(async () => { app = Fastify({ logger: false }); await app.register(bankAccountsRoutes, { prefix: "/api/admin/bank-accounts", }); await prisma.users .deleteMany({ where: { username: { startsWith: N } } }) .catch(() => {}); // Admin role bypasses the settings.banking permission guard. const adminRole = await prisma.roles.findFirst({ where: { name: "admin" } }); if (!adminRole) throw new Error("Test setup: no admin role found — seed the database"); const user = await prisma.users.create({ data: { username: `${N}user`, email: `${N}user@test.local`, password_hash: "x", first_name: "Bank", last_name: "Test", is_active: true, totp_enabled: false, role_id: adminRole.id, }, }); token = jwt.sign( { sub: user.id, username: user.username, role: "admin" }, config.jwt.secret, { expiresIn: config.jwt.accessTokenExpiry }, ); }); afterAll(async () => { if (createdIds.length) { await prisma.bank_accounts .deleteMany({ where: { id: { in: createdIds } } }) .catch(() => {}); } await prisma.users .deleteMany({ where: { username: { startsWith: N } } }) .catch(() => {}); await app.close(); }); const auth = { authorization: () => `Bearer ${token}` }; describe("bank accounts — flat-body create/update persists all fields", () => { it("creates an account from a flat top-level body and persists every field", async () => { const payload = { account_name: `${N}Hlavní účet`, bank_name: "Komerční banka", account_number: "123456789/0100", iban: "CZ6508000000192000145399", bic: "KOMBCZPP", currency: "CZK", is_default: true, }; const post = await app.inject({ method: "POST", url: "/api/admin/bank-accounts/", headers: { authorization: auth.authorization() }, payload, }); expect(post.statusCode).toBe(201); const id = post.json().data.id as number; expect(typeof id).toBe("number"); createdIds.push(id); // Read back via the list endpoint and confirm NONE of the fields are blank. const row = await prisma.bank_accounts.findUnique({ where: { id } }); expect(row?.account_name).toBe(payload.account_name); expect(row?.bank_name).toBe(payload.bank_name); expect(row?.account_number).toBe(payload.account_number); expect(row?.iban).toBe(payload.iban); expect(row?.bic).toBe(payload.bic); expect(row?.currency).toBe("CZK"); expect(row?.is_default).toBe(true); }); it("updates an account from a flat body that also carries the id", async () => { const created = await prisma.bank_accounts.create({ data: { account_name: `${N}orig`, bank_name: "Orig", currency: "CZK" }, }); createdIds.push(created.id); // Mirrors the client payload: spread fields + id (id routes the URL/method, // is declared in the schema, and is ignored by the route which uses :id). const put = await app.inject({ method: "PUT", url: `/api/admin/bank-accounts/${created.id}`, headers: { authorization: auth.authorization() }, payload: { id: created.id, account_name: `${N}upraveno`, bank_name: "Nová banka", iban: "CZ9999", currency: "EUR", is_default: false, }, }); expect(put.statusCode).toBe(200); const row = await prisma.bank_accounts.findUnique({ where: { id: created.id }, }); expect(row?.account_name).toBe(`${N}upraveno`); expect(row?.bank_name).toBe("Nová banka"); expect(row?.iban).toBe("CZ9999"); expect(row?.currency).toBe("EUR"); }); it("rejects the old nested {bank:{...}} body shape with 400 (no silent blank save)", async () => { const post = await app.inject({ method: "POST", url: "/api/admin/bank-accounts/", headers: { authorization: auth.authorization() }, payload: { bank: { account_name: `${N}nested`, bank_name: "X" } }, }); expect(post.statusCode).toBe(400); // And nothing was persisted under that name. const leaked = await prisma.bank_accounts.findFirst({ where: { account_name: `${N}nested` }, }); expect(leaked).toBeNull(); }); it("requires authentication", async () => { const res = await app.inject({ method: "GET", url: "/api/admin/bank-accounts/", }); expect(res.statusCode).toBe(401); }); });