Replaces the hardcoded PROJECT_CODES array with an admin-managed `ProjectCode` model, mirroring the Delivery Locations pattern (PR #100): - ProjectCode model (unique `code` + isActive) + migration seeding the five previously-hardcoded codes; PO.projectCode stays a free-text snapshot (no FK) so history/exports/imports are unchanged. - manage_project_codes permission (Manager + SuperUser + Admin). - /admin/project-codes CRUD screen (table + Add/Edit + activate/delete) and an Administration sidebar link. - ProjectCodeField now takes `options` from the active codes; the three PO forms + pages fetch them from the DB. Static list removed. - Unit test reworked to the options API; CRUD integration test added; documented in App/CLAUDE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
3.2 KiB
TypeScript
81 lines
3.2 KiB
TypeScript
/**
|
|
* Integration tests for the Project Codes admin CRUD (issue #124).
|
|
* Covers create/update/toggle/delete + the manage_project_codes guard.
|
|
*/
|
|
import { vi, describe, it, expect, afterAll } from "vitest";
|
|
|
|
vi.mock("@/auth", () => ({ auth: vi.fn() }));
|
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
|
|
|
import { auth } from "@/auth";
|
|
import { db } from "@/lib/db";
|
|
import {
|
|
createProjectCode,
|
|
updateProjectCode,
|
|
toggleProjectCodeActive,
|
|
deleteProjectCode,
|
|
} from "@/app/(portal)/admin/project-codes/actions";
|
|
import { makeSession, fd } from "./helpers";
|
|
|
|
const mockedAuth = vi.mocked(auth);
|
|
const PREFIX = "INTTEST_PROJCODE_";
|
|
|
|
const asManager = () => mockedAuth.mockResolvedValue(makeSession("u-mgr", "MANAGER") as never);
|
|
|
|
afterAll(async () => {
|
|
await db.projectCode.deleteMany({ where: { code: { startsWith: PREFIX } } });
|
|
});
|
|
|
|
describe("createProjectCode", () => {
|
|
it("persists an active project code", async () => {
|
|
asManager();
|
|
const result = await createProjectCode(fd({ code: `${PREFIX}Alpha` }));
|
|
expect(result).toEqual({ ok: true });
|
|
|
|
const code = await db.projectCode.findFirstOrThrow({ where: { code: `${PREFIX}Alpha` } });
|
|
expect(code.isActive).toBe(true);
|
|
});
|
|
|
|
it("requires a non-empty code", async () => {
|
|
asManager();
|
|
expect("error" in (await createProjectCode(fd({ code: " " })))).toBe(true);
|
|
});
|
|
|
|
it("rejects a duplicate code", async () => {
|
|
asManager();
|
|
await createProjectCode(fd({ code: `${PREFIX}Dup` }));
|
|
const result = await createProjectCode(fd({ code: `${PREFIX}Dup` }));
|
|
expect("error" in result).toBe(true);
|
|
});
|
|
|
|
it("refuses callers without manage_project_codes", async () => {
|
|
mockedAuth.mockResolvedValue(makeSession("u-tech", "TECHNICAL") as never);
|
|
expect(await createProjectCode(fd({ code: `${PREFIX}X` }))).toEqual({ error: "Forbidden" });
|
|
mockedAuth.mockResolvedValue(makeSession("u-acc", "ACCOUNTS") as never);
|
|
expect(await createProjectCode(fd({ code: `${PREFIX}X` }))).toEqual({ error: "Forbidden" });
|
|
});
|
|
});
|
|
|
|
describe("updateProjectCode / toggle / delete", () => {
|
|
it("edits, toggles active, then deletes a project code", async () => {
|
|
asManager();
|
|
await createProjectCode(fd({ code: `${PREFIX}Old` }));
|
|
const code = await db.projectCode.findFirstOrThrow({ where: { code: `${PREFIX}Old` } });
|
|
|
|
expect(await updateProjectCode(code.id, fd({ code: `${PREFIX}New` }))).toEqual({ ok: true });
|
|
expect((await db.projectCode.findUniqueOrThrow({ where: { id: code.id } })).code).toBe(`${PREFIX}New`);
|
|
|
|
expect(await toggleProjectCodeActive(code.id)).toEqual({ ok: true });
|
|
expect((await db.projectCode.findUniqueOrThrow({ where: { id: code.id } })).isActive).toBe(false);
|
|
|
|
expect(await deleteProjectCode(code.id)).toEqual({ ok: true });
|
|
expect(await db.projectCode.findUnique({ where: { id: code.id } })).toBeNull();
|
|
});
|
|
|
|
it("guards update/toggle/delete behind the permission", async () => {
|
|
mockedAuth.mockResolvedValue(makeSession("u-tech", "TECHNICAL") as never);
|
|
expect(await updateProjectCode("x", fd({ code: "y" }))).toEqual({ error: "Forbidden" });
|
|
expect(await toggleProjectCodeActive("x")).toEqual({ error: "Forbidden" });
|
|
expect(await deleteProjectCode("x")).toEqual({ error: "Forbidden" });
|
|
});
|
|
});
|