Replaces the free-text "Place of Delivery" with a dropdown sourced from a new admin-managed Delivery Locations list (each = a Company FK + free-text address). - schema + migration: new DeliveryLocation model (companyId, address, isActive). - permission: manage_delivery_locations granted to Manager + SuperUser + Admin (Manager-accessible, not admin-only, per the issue). - admin screen /admin/delivery-locations: table + Add/Edit dialogs + activate/deactivate + delete (mirrors /admin/sites); sidebar link under Administration for Manager/SuperUser/Admin. - PO forms (new / edit / manager-edit): shared <DeliveryLocationField> native select populated from active locations, formatted "Company — address". - PurchaseOrder.placeOfDelivery stays a free-text SNAPSHOT (no FK) — the dropdown only changes how the value is picked, so export/import/historical POs are unchanged, and an edit preserves a current value not in the list as a "(current)" option. Deleting a location is therefore always safe. - tests: delivery-location CRUD + permission guard (6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
3.8 KiB
TypeScript
89 lines
3.8 KiB
TypeScript
/**
|
|
* Integration tests for the Delivery Locations admin CRUD (issue #19).
|
|
* Covers create/update/toggle/delete + the manage_delivery_locations guard.
|
|
*/
|
|
import { vi, describe, it, expect, beforeAll, 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 {
|
|
createDeliveryLocation,
|
|
updateDeliveryLocation,
|
|
toggleDeliveryLocationActive,
|
|
deleteDeliveryLocation,
|
|
} from "@/app/(portal)/admin/delivery-locations/actions";
|
|
import { makeSession, fd } from "./helpers";
|
|
|
|
const mockedAuth = vi.mocked(auth);
|
|
const PREFIX = "INTTEST_DELLOC_";
|
|
let companyId: string;
|
|
|
|
const asManager = () => mockedAuth.mockResolvedValue(makeSession("u-mgr", "MANAGER") as never);
|
|
|
|
beforeAll(async () => {
|
|
const company = await db.company.create({ data: { name: `${PREFIX}Co`, code: "ZZDELLOC" } });
|
|
companyId = company.id;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await db.deliveryLocation.deleteMany({ where: { companyId } });
|
|
await db.company.deleteMany({ where: { name: { startsWith: PREFIX } } });
|
|
});
|
|
|
|
describe("createDeliveryLocation", () => {
|
|
it("persists a location tied to its company", async () => {
|
|
asManager();
|
|
const result = await createDeliveryLocation(fd({ companyId, address: "Dock 4, Mumbai" }));
|
|
expect(result).toEqual({ ok: true });
|
|
|
|
const loc = await db.deliveryLocation.findFirstOrThrow({ where: { companyId, address: "Dock 4, Mumbai" } });
|
|
expect(loc.isActive).toBe(true);
|
|
expect(loc.companyId).toBe(companyId);
|
|
});
|
|
|
|
it("requires both a company and an address", async () => {
|
|
asManager();
|
|
expect("error" in (await createDeliveryLocation(fd({ companyId, address: " " })))).toBe(true);
|
|
expect("error" in (await createDeliveryLocation(fd({ companyId: "", address: "x" })))).toBe(true);
|
|
});
|
|
|
|
it("rejects a company that no longer exists", async () => {
|
|
asManager();
|
|
const result = await createDeliveryLocation(fd({ companyId: "nonexistent", address: "x" }));
|
|
expect("error" in result).toBe(true);
|
|
});
|
|
|
|
it("refuses callers without manage_delivery_locations", async () => {
|
|
mockedAuth.mockResolvedValue(makeSession("u-tech", "TECHNICAL") as never);
|
|
expect(await createDeliveryLocation(fd({ companyId, address: "x" }))).toEqual({ error: "Forbidden" });
|
|
mockedAuth.mockResolvedValue(makeSession("u-acc", "ACCOUNTS") as never);
|
|
expect(await createDeliveryLocation(fd({ companyId, address: "x" }))).toEqual({ error: "Forbidden" });
|
|
});
|
|
});
|
|
|
|
describe("updateDeliveryLocation / toggle / delete", () => {
|
|
it("edits, toggles active, then deletes a location", async () => {
|
|
asManager();
|
|
await createDeliveryLocation(fd({ companyId, address: "Old Address" }));
|
|
const loc = await db.deliveryLocation.findFirstOrThrow({ where: { companyId, address: "Old Address" } });
|
|
|
|
expect(await updateDeliveryLocation(loc.id, fd({ companyId, address: "New Address" }))).toEqual({ ok: true });
|
|
expect((await db.deliveryLocation.findUniqueOrThrow({ where: { id: loc.id } })).address).toBe("New Address");
|
|
|
|
expect(await toggleDeliveryLocationActive(loc.id)).toEqual({ ok: true });
|
|
expect((await db.deliveryLocation.findUniqueOrThrow({ where: { id: loc.id } })).isActive).toBe(false);
|
|
|
|
expect(await deleteDeliveryLocation(loc.id)).toEqual({ ok: true });
|
|
expect(await db.deliveryLocation.findUnique({ where: { id: loc.id } })).toBeNull();
|
|
});
|
|
|
|
it("guards update/toggle/delete behind the permission", async () => {
|
|
mockedAuth.mockResolvedValue(makeSession("u-tech", "TECHNICAL") as never);
|
|
expect(await updateDeliveryLocation("x", fd({ companyId, address: "y" }))).toEqual({ error: "Forbidden" });
|
|
expect(await toggleDeliveryLocationActive("x")).toEqual({ error: "Forbidden" });
|
|
expect(await deleteDeliveryLocation("x")).toEqual({ error: "Forbidden" });
|
|
});
|
|
});
|