test: add PurchaseOrder API tests
This commit is contained in:
@@ -0,0 +1,577 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { Mock } from "vitest";
|
||||||
|
import type { Session } from "next-auth";
|
||||||
|
import {
|
||||||
|
createPurchaseOrder,
|
||||||
|
listPurchaseOrders,
|
||||||
|
validateCreatePurchaseOrderInput,
|
||||||
|
} from "@/lib/purchase-orders";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
// Note: this file has two layers of coverage, mirroring tests/licenses.test.ts:
|
||||||
|
// - lib-level tests below exercise src/lib/purchase-orders.ts directly
|
||||||
|
// (validation, co-nullability refine, FK error handling, list ordering).
|
||||||
|
// - route-level tests (further down) exercise
|
||||||
|
// src/app/api/purchase-orders/route.ts over HTTP, including the auth() gate.
|
||||||
|
vi.mock("@/auth", () => ({ auth: vi.fn() }));
|
||||||
|
|
||||||
|
import { GET, POST } from "@/app/api/purchase-orders/route";
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
|
||||||
|
const mockedAuth = auth as unknown as Mock<() => Promise<Session | null>>;
|
||||||
|
|
||||||
|
const testSession: Session = {
|
||||||
|
user: { id: "test-user-id" },
|
||||||
|
expires: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function postRequest(body: unknown): Request {
|
||||||
|
return new Request("http://localhost/api/purchase-orders", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurchaseOrder.vendorId/receivedById are real FKs to Vendor/Employee (see
|
||||||
|
// schema.prisma / src/lib/purchase-orders.ts) — tests that need valid ids
|
||||||
|
// create real rows first and use their ids. Mirrors
|
||||||
|
// tests/licenses.test.ts's createTestVendor() and tests/requests.test.ts's
|
||||||
|
// createTestEmployee() helpers.
|
||||||
|
function createTestVendor(overrides: { name?: string } = {}) {
|
||||||
|
return prisma.vendor.create({
|
||||||
|
data: {
|
||||||
|
name: overrides.name ?? "Test Vendor",
|
||||||
|
contactEmail: "[email protected]",
|
||||||
|
contactPhone: "0281234567",
|
||||||
|
address: "123 Test Street",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestEmployee(overrides: { name?: string; department?: string } = {}) {
|
||||||
|
return prisma.employee.create({
|
||||||
|
data: {
|
||||||
|
name: overrides.name ?? "Test Employee",
|
||||||
|
department: overrides.department ?? "IT",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Each test starts from empty tables so listing/order assertions are
|
||||||
|
// deterministic. purchase_orders must be cleared before vendors/employees
|
||||||
|
// since purchase_orders has FKs (ON DELETE RESTRICT to vendors, ON DELETE
|
||||||
|
// SET NULL to employees) to both.
|
||||||
|
await prisma.purchaseOrder.deleteMany();
|
||||||
|
await prisma.vendor.deleteMany();
|
||||||
|
await prisma.employee.deleteMany();
|
||||||
|
// Default to a logged-in session — the lib-level tests below don't go
|
||||||
|
// through the route at all (so this is a no-op for them), and the
|
||||||
|
// route-level tests exercise business logic, not auth gating (that's
|
||||||
|
// covered by the "auth gating" describe block further down).
|
||||||
|
mockedAuth.mockResolvedValue(testSession);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateCreatePurchaseOrderInput", () => {
|
||||||
|
it("accepts valid input with no receivedAt/receivedById (not yet received)", () => {
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: "11111111-1111-1111-1111-111111111111",
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.value.vendorId).toBe("11111111-1111-1111-1111-111111111111");
|
||||||
|
expect(result.value.orderDate).toBeInstanceOf(Date);
|
||||||
|
expect(result.value.expectedDate).toBeInstanceOf(Date);
|
||||||
|
expect(result.value.receivedAt).toBeUndefined();
|
||||||
|
expect(result.value.receivedById).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts valid input with both receivedAt and receivedById explicitly null (not yet received)", () => {
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: "11111111-1111-1111-1111-111111111111",
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
receivedAt: null,
|
||||||
|
receivedById: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts valid input with both receivedAt and receivedById set (received)", () => {
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: "11111111-1111-1111-1111-111111111111",
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
receivedAt: "2026-08-12",
|
||||||
|
receivedById: "22222222-2222-2222-2222-222222222222",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.value.receivedAt).toBeInstanceOf(Date);
|
||||||
|
expect(result.value.receivedById).toBe("22222222-2222-2222-2222-222222222222");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects receivedAt set without receivedById", () => {
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: "11111111-1111-1111-1111-111111111111",
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
receivedAt: "2026-08-12",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error).toMatch(/receivedAt/i);
|
||||||
|
expect(result.error).toMatch(/receivedById/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects receivedById set without receivedAt", () => {
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: "11111111-1111-1111-1111-111111111111",
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
receivedById: "22222222-2222-2222-2222-222222222222",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error).toMatch(/receivedAt/i);
|
||||||
|
expect(result.error).toMatch(/receivedById/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a missing vendorId", () => {
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error).toMatch(/vendorId/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a vendorId that is not a valid UUID", () => {
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: "not-a-uuid",
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error).toMatch(/vendorId/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a missing orderDate", () => {
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: "11111111-1111-1111-1111-111111111111",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error).toMatch(/orderDate/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an invalid expectedDate", () => {
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: "11111111-1111-1111-1111-111111111111",
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "not-a-date",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error).toMatch(/expectedDate/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a receivedById that is not a valid UUID", () => {
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: "11111111-1111-1111-1111-111111111111",
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
receivedAt: "2026-08-12",
|
||||||
|
receivedById: "not-a-uuid",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error).toMatch(/receivedById/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createPurchaseOrder", () => {
|
||||||
|
it("creates a purchase order that is not yet received", async () => {
|
||||||
|
const vendor = await createTestVendor();
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: vendor.id,
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (!result.ok) return;
|
||||||
|
|
||||||
|
const created = await createPurchaseOrder(result.value);
|
||||||
|
expect(created.ok).toBe(true);
|
||||||
|
if (!created.ok) return;
|
||||||
|
const record = created.value;
|
||||||
|
|
||||||
|
expect(record.vendor_id).toBe(vendor.id);
|
||||||
|
expect(record.received_at).toBeNull();
|
||||||
|
expect(record.received_by).toBeNull();
|
||||||
|
expect(typeof record.id).toBe("string");
|
||||||
|
expect(record.id.length).toBeGreaterThan(0);
|
||||||
|
expect(Number.isNaN(Date.parse(record.order_date))).toBe(false);
|
||||||
|
expect(Number.isNaN(Date.parse(record.expected_date))).toBe(false);
|
||||||
|
expect(Number.isNaN(Date.parse(record.created_at))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a purchase order that has already been received", async () => {
|
||||||
|
const vendor = await createTestVendor();
|
||||||
|
const employee = await createTestEmployee();
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: vendor.id,
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
receivedAt: "2026-08-12",
|
||||||
|
receivedById: employee.id,
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (!result.ok) return;
|
||||||
|
|
||||||
|
const created = await createPurchaseOrder(result.value);
|
||||||
|
expect(created.ok).toBe(true);
|
||||||
|
if (!created.ok) return;
|
||||||
|
const record = created.value;
|
||||||
|
|
||||||
|
expect(record.received_by).toBe(employee.id);
|
||||||
|
expect(Number.isNaN(Date.parse(record.received_at as string))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a clean error when vendorId doesn't reference an existing vendor", async () => {
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: "00000000-0000-0000-0000-000000000000",
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (!result.ok) return;
|
||||||
|
|
||||||
|
const created = await createPurchaseOrder(result.value);
|
||||||
|
expect(created.ok).toBe(false);
|
||||||
|
if (created.ok) return;
|
||||||
|
expect(created.error).toMatch(/vendorId/i);
|
||||||
|
expect(created.error).toMatch(/does not reference an existing vendor/i);
|
||||||
|
|
||||||
|
const remaining = await prisma.purchaseOrder.findMany();
|
||||||
|
expect(remaining).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a clean error when receivedById doesn't reference an existing employee", async () => {
|
||||||
|
const vendor = await createTestVendor();
|
||||||
|
const result = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: vendor.id,
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
receivedAt: "2026-08-12",
|
||||||
|
receivedById: "00000000-0000-0000-0000-000000000000",
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (!result.ok) return;
|
||||||
|
|
||||||
|
const created = await createPurchaseOrder(result.value);
|
||||||
|
expect(created.ok).toBe(false);
|
||||||
|
if (created.ok) return;
|
||||||
|
expect(created.error).toMatch(/receivedById/i);
|
||||||
|
expect(created.error).toMatch(/does not reference an existing employee/i);
|
||||||
|
|
||||||
|
const remaining = await prisma.purchaseOrder.findMany();
|
||||||
|
expect(remaining).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("listPurchaseOrders", () => {
|
||||||
|
it("lists created purchase orders, newest first", async () => {
|
||||||
|
const vendor1 = await createTestVendor({ name: "Microsoft VN" });
|
||||||
|
const vendor2 = await createTestVendor({ name: "Adobe VN" });
|
||||||
|
|
||||||
|
const first = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: vendor1.id,
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
});
|
||||||
|
expect(first.ok).toBe(true);
|
||||||
|
if (first.ok) await createPurchaseOrder(first.value);
|
||||||
|
|
||||||
|
// Ensure a distinct created_at ordering even on fast filesystems/clocks.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
|
||||||
|
const second = validateCreatePurchaseOrderInput({
|
||||||
|
vendorId: vendor2.id,
|
||||||
|
orderDate: "2026-08-02",
|
||||||
|
expectedDate: "2026-08-11",
|
||||||
|
});
|
||||||
|
expect(second.ok).toBe(true);
|
||||||
|
if (second.ok) await createPurchaseOrder(second.value);
|
||||||
|
|
||||||
|
const records = await listPurchaseOrders();
|
||||||
|
|
||||||
|
expect(records).toHaveLength(2);
|
||||||
|
// newest first
|
||||||
|
expect(records[0]).toMatchObject({ vendor_id: vendor2.id });
|
||||||
|
expect(records[1]).toMatchObject({ vendor_id: vendor1.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty list when there are no purchase orders", async () => {
|
||||||
|
const records = await listPurchaseOrders();
|
||||||
|
expect(records).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("POST /api/purchase-orders", () => {
|
||||||
|
it("creates a purchase order that is not yet received on valid input", async () => {
|
||||||
|
const vendor = await createTestVendor();
|
||||||
|
const res = await POST(
|
||||||
|
postRequest({
|
||||||
|
vendorId: vendor.id,
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(body).toMatchObject({ vendor_id: vendor.id, received_at: null, received_by: null });
|
||||||
|
expect(typeof body.id).toBe("string");
|
||||||
|
expect(body.id.length).toBeGreaterThan(0);
|
||||||
|
expect(Number.isNaN(Date.parse(body.order_date))).toBe(false);
|
||||||
|
expect(Number.isNaN(Date.parse(body.expected_date))).toBe(false);
|
||||||
|
expect(Number.isNaN(Date.parse(body.created_at))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a purchase order that has already been received", async () => {
|
||||||
|
const vendor = await createTestVendor();
|
||||||
|
const employee = await createTestEmployee();
|
||||||
|
const res = await POST(
|
||||||
|
postRequest({
|
||||||
|
vendorId: vendor.id,
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
receivedAt: "2026-08-12",
|
||||||
|
receivedById: employee.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.received_by).toBe(employee.id);
|
||||||
|
expect(Number.isNaN(Date.parse(body.received_at))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a missing vendorId", async () => {
|
||||||
|
const res = await POST(
|
||||||
|
postRequest({ orderDate: "2026-08-01", expectedDate: "2026-08-10" })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toMatch(/vendorId/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a vendorId that is a well-formed UUID but doesn't reference an existing vendor", async () => {
|
||||||
|
const res = await POST(
|
||||||
|
postRequest({
|
||||||
|
vendorId: "00000000-0000-0000-0000-000000000000",
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toMatch(/vendorId/i);
|
||||||
|
expect(body.error).toMatch(/does not reference an existing vendor/i);
|
||||||
|
|
||||||
|
// And it must not have created anything.
|
||||||
|
const remaining = await prisma.purchaseOrder.findMany();
|
||||||
|
expect(remaining).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a receivedById that is a well-formed UUID but doesn't reference an existing employee", async () => {
|
||||||
|
const vendor = await createTestVendor();
|
||||||
|
const res = await POST(
|
||||||
|
postRequest({
|
||||||
|
vendorId: vendor.id,
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
receivedAt: "2026-08-12",
|
||||||
|
receivedById: "00000000-0000-0000-0000-000000000000",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toMatch(/receivedById/i);
|
||||||
|
expect(body.error).toMatch(/does not reference an existing employee/i);
|
||||||
|
|
||||||
|
// And it must not have created anything.
|
||||||
|
const remaining = await prisma.purchaseOrder.findMany();
|
||||||
|
expect(remaining).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects receivedAt set without receivedById", async () => {
|
||||||
|
const vendor = await createTestVendor();
|
||||||
|
const res = await POST(
|
||||||
|
postRequest({
|
||||||
|
vendorId: vendor.id,
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
receivedAt: "2026-08-12",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toMatch(/receivedAt/i);
|
||||||
|
expect(body.error).toMatch(/receivedById/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects receivedById set without receivedAt", async () => {
|
||||||
|
const vendor = await createTestVendor();
|
||||||
|
const employee = await createTestEmployee();
|
||||||
|
const res = await POST(
|
||||||
|
postRequest({
|
||||||
|
vendorId: vendor.id,
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "2026-08-10",
|
||||||
|
receivedById: employee.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toMatch(/receivedAt/i);
|
||||||
|
expect(body.error).toMatch(/receivedById/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an invalid expectedDate", async () => {
|
||||||
|
const vendor = await createTestVendor();
|
||||||
|
const res = await POST(
|
||||||
|
postRequest({
|
||||||
|
vendorId: vendor.id,
|
||||||
|
orderDate: "2026-08-01",
|
||||||
|
expectedDate: "not-a-date",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toMatch(/expectedDate/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an invalid JSON body", async () => {
|
||||||
|
const req = new Request("http://localhost/api/purchase-orders", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: "not json",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await POST(req);
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toMatch(/JSON/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a non-object JSON body", async () => {
|
||||||
|
// Note: arrays are `typeof "object"` in JS, so they pass the route's
|
||||||
|
// object check (same behavior as /api/licenses) — use a JSON string
|
||||||
|
// primitive here to actually exercise the "not an object" branch.
|
||||||
|
const res = await POST(postRequest("not an object"));
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toMatch(/JSON object/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /api/purchase-orders", () => {
|
||||||
|
it("lists created purchase orders, newest first", async () => {
|
||||||
|
const vendor1 = await createTestVendor({ name: "Microsoft VN" });
|
||||||
|
const vendor2 = await createTestVendor({ name: "Adobe VN" });
|
||||||
|
|
||||||
|
await POST(
|
||||||
|
postRequest({ vendorId: vendor1.id, orderDate: "2026-08-01", expectedDate: "2026-08-10" })
|
||||||
|
);
|
||||||
|
// Ensure a distinct created_at ordering even on fast filesystems/clocks.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
await POST(
|
||||||
|
postRequest({ vendorId: vendor2.id, orderDate: "2026-08-02", expectedDate: "2026-08-11" })
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await GET();
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const body = await res.json();
|
||||||
|
expect(Array.isArray(body)).toBe(true);
|
||||||
|
expect(body).toHaveLength(2);
|
||||||
|
|
||||||
|
// newest first
|
||||||
|
expect(body[0]).toMatchObject({ vendor_id: vendor2.id });
|
||||||
|
expect(body[1]).toMatchObject({ vendor_id: vendor1.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty list when there are no purchase orders", async () => {
|
||||||
|
const res = await GET();
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("auth gating on /api/purchase-orders", () => {
|
||||||
|
// Mirrors tests/licenses.test.ts's "auth gating" block — only checks
|
||||||
|
// session presence, per the flat permission model (no role/permission
|
||||||
|
// logic).
|
||||||
|
it("POST /api/purchase-orders returns 401 without a session", async () => {
|
||||||
|
const vendor = await createTestVendor();
|
||||||
|
mockedAuth.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const res = await POST(
|
||||||
|
postRequest({ vendorId: vendor.id, orderDate: "2026-08-01", expectedDate: "2026-08-10" })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body).toEqual({ error: "Unauthorized" });
|
||||||
|
|
||||||
|
// And it must not have created anything.
|
||||||
|
const remaining = await prisma.purchaseOrder.findMany();
|
||||||
|
expect(remaining).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("GET /api/purchase-orders returns 401 without a session", async () => {
|
||||||
|
mockedAuth.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const res = await GET();
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body).toEqual({ error: "Unauthorized" });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user