diff --git a/prisma/migrations/20260802234029_retrofit_license_vendor_id/migration.sql b/prisma/migrations/20260802234029_retrofit_license_vendor_id/migration.sql new file mode 100644 index 0000000..c80d572 --- /dev/null +++ b/prisma/migrations/20260802234029_retrofit_license_vendor_id/migration.sql @@ -0,0 +1,13 @@ +/* + Warnings: + + - You are about to drop the column `vendor` on the `licenses` table. All the data in the column will be lost. + - Added the required column `vendor_id` to the `licenses` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "licenses" DROP COLUMN "vendor", +ADD COLUMN "vendor_id" UUID NOT NULL; + +-- AddForeignKey +ALTER TABLE "licenses" ADD CONSTRAINT "licenses_vendor_id_fkey" FOREIGN KEY ("vendor_id") REFERENCES "vendors"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 02d35a0..7a3919c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -102,17 +102,20 @@ enum LicenseType { // Software license inventory — see docs/asset-license-management.md's "Data // model (sơ bộ)" table (tên, vendor, tổng seat, seat đã dùng, ngày hết hạn, -// loại). `vendor` is deliberately a plain String for now, not a separate -// Vendor entity — no Vendor entity exists yet, same "plain string for now" -// precedent as Employee.department. `seatsUsed` is a stored counter -// (defaults to 0 at creation), not a computed value — it will only ever be -// set to 0 here since Assignment (the thing that would consume seats) -// doesn't exist yet. `expiryDate` is nullable: perpetual licenses never -// expire, only subscription licenses usually have one. +// loại). `vendorId` was a free-text `vendor` String until the "## Vendor +// (chốt 2026-08-02)" retrofit — same precedent as Request.employeeId +// (string -> FK once Employee existed). No production `License` data +// existed at the time of this change, so this was a plain column-type +// migration, not a backfill. `seatsUsed` is a stored counter (defaults to 0 +// at creation), not a computed value — it will only ever be set to 0 here +// since Assignment (the thing that would consume seats) doesn't exist yet. +// `expiryDate` is nullable: perpetual licenses never expire, only +// subscription licenses usually have one. model License { id String @id @default(uuid()) @db.Uuid name String - vendor String + vendorId String @map("vendor_id") @db.Uuid + vendor Vendor @relation(fields: [vendorId], references: [id]) seatsTotal Int @map("seats_total") seatsUsed Int @default(0) @map("seats_used") expiryDate DateTime? @map("expiry_date") @db.Date @@ -126,20 +129,21 @@ model License { // (chốt 2026-08-02)". Deliberately independent of AssetModel.manufacturer // (OEM, e.g. "Dell") — Vendor is the reseller/distributor actually bought // from (e.g. "FPT Trading"), a different concept that isn't always the same -// company. No FKs on this entity yet: it's the starting point for later -// FKs from PurchaseOrder/License (License.vendor string -> License.vendorId -// FK retrofit is noted in the doc as a separate follow-up, not done here). -// taxCode/notes are nullable — not every vendor record has a known VN tax -// code or extra notes at creation time. +// company. `licenses` is the back-relation from the License.vendor -> +// License.vendorId retrofit (same doc section) — PurchaseOrder is a future +// FK from this entity, not built yet. taxCode/notes are nullable — not +// every vendor record has a known VN tax code or extra notes at creation +// time. model Vendor { - id String @id @default(uuid()) @db.Uuid + id String @id @default(uuid()) @db.Uuid name String - contactEmail String @map("contact_email") - contactPhone String @map("contact_phone") + contactEmail String @map("contact_email") + contactPhone String @map("contact_phone") address String - taxCode String? @map("tax_code") + taxCode String? @map("tax_code") notes String? - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + licenses License[] @@map("vendors") } diff --git a/src/app/api/licenses/route.ts b/src/app/api/licenses/route.ts index 8f92ac6..59d56f2 100644 --- a/src/app/api/licenses/route.ts +++ b/src/app/api/licenses/route.ts @@ -22,10 +22,10 @@ export async function POST(request: Request): Promise { return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 }); } - const { name, vendor, seatsTotal, expiryDate, licenseType } = body as Record; + const { name, vendorId, seatsTotal, expiryDate, licenseType } = body as Record; const validation = validateCreateLicenseInput({ name, - vendor, + vendorId, seatsTotal, expiryDate, licenseType, @@ -36,7 +36,10 @@ export async function POST(request: Request): Promise { } const created = await createLicense(validation.value); - return NextResponse.json(created, { status: 201 }); + if (!created.ok) { + return NextResponse.json({ error: created.error }, { status: 400 }); + } + return NextResponse.json(created.value, { status: 201 }); } export async function GET(): Promise { diff --git a/src/lib/licenses.ts b/src/lib/licenses.ts index 75ac356..160ed22 100644 --- a/src/lib/licenses.ts +++ b/src/lib/licenses.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { Prisma } from "@prisma/client"; import { prisma } from "./prisma"; export type LicenseType = "perpetual" | "subscription"; @@ -6,7 +7,7 @@ export type LicenseType = "perpetual" | "subscription"; export interface LicenseRecord { id: string; name: string; - vendor: string; + vendor_id: string; seats_total: number; seats_used: number; expiry_date: string | null; @@ -16,7 +17,7 @@ export interface LicenseRecord { export interface CreateLicenseInput { name: string; - vendor: string; + vendorId: string; seatsTotal: number; expiryDate?: Date | null; licenseType: LicenseType; @@ -46,7 +47,14 @@ function dateString(message: string) { const createLicenseSchema = z.object({ name: nonEmptyString("name is required and must be a non-empty string"), - vendor: nonEmptyString("vendor is required and must be a non-empty string"), + // Now a real FK to Vendor (see schema.prisma) — only a UUID *format* + // check here — whether it references a real vendor is left to Prisma's + // own FK constraint at insert time (see createLicense()'s P2003 handling + // below), rather than an extra pre-check query. Mirrors the same pattern + // used for Request.employeeId in src/lib/requests.ts. + vendorId: z + .string({ required_error: "vendorId is required and must be a valid UUID string" }) + .uuid("vendorId is required and must be a valid UUID string"), seatsTotal: z .number({ required_error: "seatsTotal is required and must be a positive integer", invalid_type_error: "seatsTotal is required and must be a positive integer" }) .int({ message: "seatsTotal is required and must be a positive integer" }) @@ -60,17 +68,17 @@ const createLicenseSchema = z.object({ }); /** - * Validates raw (untyped) license-creation input. name/vendor must be - * non-empty strings, seatsTotal must be a positive integer, expiryDate is - * optional/nullable but must be a valid date string if present, licenseType - * must be one of the two enum values. seatsUsed is not accepted here — new - * licenses always start at 0 (same pattern as AssetItem always starting - * "in_stock"), seats are only ever consumed via other operations (once - * Assignment exists), not at creation. + * Validates raw (untyped) license-creation input. name must be a non-empty + * string, vendorId must be a valid UUID string, seatsTotal must be a + * positive integer, expiryDate is optional/nullable but must be a valid + * date string if present, licenseType must be one of the two enum values. + * seatsUsed is not accepted here — new licenses always start at 0 (same + * pattern as AssetItem always starting "in_stock"), seats are only ever + * consumed via other operations (once Assignment exists), not at creation. */ export function validateCreateLicenseInput(input: { name?: unknown; - vendor?: unknown; + vendorId?: unknown; seatsTotal?: unknown; expiryDate?: unknown; licenseType?: unknown; @@ -86,7 +94,7 @@ export function validateCreateLicenseInput(input: { function toRecord(row: { id: string; name: string; - vendor: string; + vendorId: string; seatsTotal: number; seatsUsed: number; expiryDate: Date | null; @@ -96,7 +104,7 @@ function toRecord(row: { return { id: row.id, name: row.name, - vendor: row.vendor, + vendor_id: row.vendorId, seats_total: row.seatsTotal, seats_used: row.seatsUsed, expiry_date: row.expiryDate ? row.expiryDate.toISOString() : null, @@ -105,22 +113,35 @@ function toRecord(row: { }; } +export type CreateLicenseResult = + | { ok: true; value: LicenseRecord } + | { ok: false; error: string }; + /** - * Creates a License. No unique constraint exists on `name` (unlike - * AssetItem.serialNumber), so unlike createAssetItem() there's no known - * Prisma error code to catch/translate here. + * Creates a License. If vendorId doesn't reference an existing Vendor, + * Prisma raises a known error (code P2003, FK violation) — caught here and + * translated into the same `{ok:false,error}` shape as input validation + * failures, rather than letting a raw Prisma error leak to the API layer. + * Mirrors src/lib/requests.ts's createRequest() pattern for employeeId. */ -export async function createLicense(input: CreateLicenseInput): Promise { - const created = await prisma.license.create({ - data: { - name: input.name, - vendor: input.vendor, - seatsTotal: input.seatsTotal, - expiryDate: input.expiryDate ?? null, - licenseType: input.licenseType, - }, - }); - return toRecord(created); +export async function createLicense(input: CreateLicenseInput): Promise { + try { + const created = await prisma.license.create({ + data: { + name: input.name, + vendorId: input.vendorId, + seatsTotal: input.seatsTotal, + expiryDate: input.expiryDate ?? null, + licenseType: input.licenseType, + }, + }); + return { ok: true, value: toRecord(created) }; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2003") { + return { ok: false, error: "vendorId does not reference an existing vendor" }; + } + throw error; + } } export async function listLicenses(): Promise { diff --git a/tests/licenses.test.ts b/tests/licenses.test.ts index 3fcd22b..bf0c805 100644 --- a/tests/licenses.test.ts +++ b/tests/licenses.test.ts @@ -34,10 +34,27 @@ function postRequest(body: unknown): Request { }); } +// License.vendorId is a real FK to Vendor (see schema.prisma / +// src/lib/licenses.ts) — tests that need a valid vendorId create a real +// Vendor row first and use its id, rather than an arbitrary string. Mirrors +// tests/requests.test.ts's createTestEmployee() helper. +function createTestVendor(overrides: { name?: string } = {}) { + return prisma.vendor.create({ + data: { + name: overrides.name ?? "Test Vendor", + contactEmail: "sales@testvendor.example", + contactPhone: "0281234567", + address: "123 Test Street", + }, + }); +} + beforeEach(async () => { - // Each test starts from an empty table so listing/order assertions are - // deterministic. + // Each test starts from empty tables so listing/order assertions are + // deterministic. licenses must be cleared before vendors since + // licenses.vendor_id has an FK (ON DELETE RESTRICT) to vendors. await prisma.license.deleteMany(); + await prisma.vendor.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 @@ -49,7 +66,7 @@ describe("validateCreateLicenseInput", () => { it("accepts valid input with an expiryDate", () => { const result = validateCreateLicenseInput({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: "11111111-1111-1111-1111-111111111111", seatsTotal: 10, expiryDate: "2027-01-15", licenseType: "subscription", @@ -59,7 +76,7 @@ describe("validateCreateLicenseInput", () => { if (result.ok) { expect(result.value).toMatchObject({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: "11111111-1111-1111-1111-111111111111", seatsTotal: 10, licenseType: "subscription", }); @@ -70,7 +87,7 @@ describe("validateCreateLicenseInput", () => { it("accepts valid input with no expiryDate (perpetual license)", () => { const result = validateCreateLicenseInput({ name: "Windows Server 2022", - vendor: "Microsoft", + vendorId: "11111111-1111-1111-1111-111111111111", seatsTotal: 5, licenseType: "perpetual", }); @@ -84,7 +101,7 @@ describe("validateCreateLicenseInput", () => { it("rejects an empty name", () => { const result = validateCreateLicenseInput({ name: "", - vendor: "Adobe", + vendorId: "11111111-1111-1111-1111-111111111111", seatsTotal: 10, licenseType: "subscription", }); @@ -95,24 +112,37 @@ describe("validateCreateLicenseInput", () => { } }); - it("rejects an empty vendor", () => { + it("rejects a vendorId that is not a valid UUID", () => { const result = validateCreateLicenseInput({ name: "Adobe Creative Cloud", - vendor: "", + vendorId: "not-a-uuid", seatsTotal: 10, licenseType: "subscription", }); expect(result.ok).toBe(false); if (!result.ok) { - expect(result.error).toMatch(/vendor/i); + expect(result.error).toMatch(/vendorId/i); + } + }); + + it("rejects a missing vendorId", () => { + const result = validateCreateLicenseInput({ + name: "Adobe Creative Cloud", + seatsTotal: 10, + licenseType: "subscription", + }); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toMatch(/vendorId/i); } }); it("rejects a zero seatsTotal", () => { const result = validateCreateLicenseInput({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: "11111111-1111-1111-1111-111111111111", seatsTotal: 0, licenseType: "subscription", }); @@ -126,7 +156,7 @@ describe("validateCreateLicenseInput", () => { it("rejects a negative seatsTotal", () => { const result = validateCreateLicenseInput({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: "11111111-1111-1111-1111-111111111111", seatsTotal: -5, licenseType: "subscription", }); @@ -140,7 +170,7 @@ describe("validateCreateLicenseInput", () => { it("rejects a non-integer seatsTotal", () => { const result = validateCreateLicenseInput({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: "11111111-1111-1111-1111-111111111111", seatsTotal: 3.5, licenseType: "subscription", }); @@ -154,7 +184,7 @@ describe("validateCreateLicenseInput", () => { it("rejects an invalid expiryDate", () => { const result = validateCreateLicenseInput({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: "11111111-1111-1111-1111-111111111111", seatsTotal: 10, expiryDate: "not-a-date", licenseType: "subscription", @@ -169,7 +199,7 @@ describe("validateCreateLicenseInput", () => { it("rejects an invalid licenseType", () => { const result = validateCreateLicenseInput({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: "11111111-1111-1111-1111-111111111111", seatsTotal: 10, licenseType: "trial", }); @@ -183,7 +213,7 @@ describe("validateCreateLicenseInput", () => { it("rejects a missing licenseType", () => { const result = validateCreateLicenseInput({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: "11111111-1111-1111-1111-111111111111", seatsTotal: 10, }); @@ -196,9 +226,10 @@ describe("validateCreateLicenseInput", () => { describe("createLicense", () => { it("creates a license with seatsUsed always 0, regardless of client input", async () => { + const vendor = await createTestVendor(); const result = validateCreateLicenseInput({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: vendor.id, seatsTotal: 10, // @ts-expect-error -- seatsUsed is intentionally not part of // CreateLicenseInput; this simulates a client trying to sneak it in. @@ -209,12 +240,15 @@ describe("createLicense", () => { expect(result.ok).toBe(true); if (!result.ok) return; - const record = await createLicense(result.value); + const created = await createLicense(result.value); + expect(created.ok).toBe(true); + if (!created.ok) return; + const record = created.value; expect(record.seats_used).toBe(0); expect(record).toMatchObject({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendor_id: vendor.id, seats_total: 10, license_type: "subscription", }); @@ -225,28 +259,55 @@ describe("createLicense", () => { }); it("creates a perpetual license with no expiryDate", async () => { + const vendor = await createTestVendor(); const result = validateCreateLicenseInput({ name: "Windows Server 2022", - vendor: "Microsoft", + vendorId: vendor.id, seatsTotal: 5, licenseType: "perpetual", }); expect(result.ok).toBe(true); if (!result.ok) return; - const record = await createLicense(result.value); + const created = await createLicense(result.value); + expect(created.ok).toBe(true); + if (!created.ok) return; + const record = created.value; expect(record.expiry_date).toBeNull(); expect(record.seats_used).toBe(0); expect(record.license_type).toBe("perpetual"); }); + + it("returns a clean error when vendorId doesn't reference an existing vendor", async () => { + const result = validateCreateLicenseInput({ + name: "Adobe Creative Cloud", + vendorId: "00000000-0000-0000-0000-000000000000", + seatsTotal: 10, + licenseType: "subscription", + }); + expect(result.ok).toBe(true); + if (!result.ok) return; + + const created = await createLicense(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.license.findMany(); + expect(remaining).toHaveLength(0); + }); }); describe("listLicenses", () => { it("lists created licenses, newest first", async () => { + const vendor1 = await createTestVendor({ name: "Microsoft VN" }); + const vendor2 = await createTestVendor({ name: "Adobe VN" }); + const first = validateCreateLicenseInput({ name: "Windows Server 2022", - vendor: "Microsoft", + vendorId: vendor1.id, seatsTotal: 5, licenseType: "perpetual", }); @@ -258,7 +319,7 @@ describe("listLicenses", () => { const second = validateCreateLicenseInput({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: vendor2.id, seatsTotal: 10, licenseType: "subscription", }); @@ -269,8 +330,8 @@ describe("listLicenses", () => { expect(records).toHaveLength(2); // newest first - expect(records[0]).toMatchObject({ name: "Adobe Creative Cloud", vendor: "Adobe" }); - expect(records[1]).toMatchObject({ name: "Windows Server 2022", vendor: "Microsoft" }); + expect(records[0]).toMatchObject({ name: "Adobe Creative Cloud", vendor_id: vendor2.id }); + expect(records[1]).toMatchObject({ name: "Windows Server 2022", vendor_id: vendor1.id }); }); it("returns an empty list when there are no licenses", async () => { @@ -281,10 +342,11 @@ describe("listLicenses", () => { describe("POST /api/licenses", () => { it("creates a license with seatsUsed always 0 on valid input", async () => { + const vendor = await createTestVendor(); const res = await POST( postRequest({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: vendor.id, seatsTotal: 10, expiryDate: "2027-01-15", licenseType: "subscription", @@ -296,7 +358,7 @@ describe("POST /api/licenses", () => { expect(body).toMatchObject({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendor_id: vendor.id, seats_total: 10, seats_used: 0, license_type: "subscription", @@ -308,10 +370,11 @@ describe("POST /api/licenses", () => { }); it("creates a perpetual license with no expiryDate", async () => { + const vendor = await createTestVendor(); const res = await POST( postRequest({ name: "Windows Server 2022", - vendor: "Microsoft", + vendorId: vendor.id, seatsTotal: 5, licenseType: "perpetual", }) @@ -324,8 +387,9 @@ describe("POST /api/licenses", () => { }); it("rejects an empty name", async () => { + const vendor = await createTestVendor(); const res = await POST( - postRequest({ name: "", vendor: "Adobe", seatsTotal: 10, licenseType: "subscription" }) + postRequest({ name: "", vendorId: vendor.id, seatsTotal: 10, licenseType: "subscription" }) ); expect(res.status).toBe(400); @@ -333,19 +397,45 @@ describe("POST /api/licenses", () => { expect(body.error).toMatch(/name/i); }); - it("rejects an empty vendor", async () => { + it("rejects a vendorId that is not a valid UUID", async () => { const res = await POST( - postRequest({ name: "Adobe Creative Cloud", vendor: "", seatsTotal: 10, licenseType: "subscription" }) + postRequest({ + name: "Adobe Creative Cloud", + vendorId: "not-a-uuid", + seatsTotal: 10, + licenseType: "subscription", + }) ); expect(res.status).toBe(400); const body = await res.json(); - expect(body.error).toMatch(/vendor/i); + 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({ + name: "Adobe Creative Cloud", + vendorId: "00000000-0000-0000-0000-000000000000", + seatsTotal: 10, + licenseType: "subscription", + }) + ); + + 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.license.findMany(); + expect(remaining).toHaveLength(0); }); it("rejects a zero seatsTotal", async () => { + const vendor = await createTestVendor(); const res = await POST( - postRequest({ name: "Adobe Creative Cloud", vendor: "Adobe", seatsTotal: 0, licenseType: "subscription" }) + postRequest({ name: "Adobe Creative Cloud", vendorId: vendor.id, seatsTotal: 0, licenseType: "subscription" }) ); expect(res.status).toBe(400); @@ -354,10 +444,11 @@ describe("POST /api/licenses", () => { }); it("rejects an invalid expiryDate", async () => { + const vendor = await createTestVendor(); const res = await POST( postRequest({ name: "Adobe Creative Cloud", - vendor: "Adobe", + vendorId: vendor.id, seatsTotal: 10, expiryDate: "not-a-date", licenseType: "subscription", @@ -370,8 +461,9 @@ describe("POST /api/licenses", () => { }); it("rejects an invalid licenseType", async () => { + const vendor = await createTestVendor(); const res = await POST( - postRequest({ name: "Adobe Creative Cloud", vendor: "Adobe", seatsTotal: 10, licenseType: "trial" }) + postRequest({ name: "Adobe Creative Cloud", vendorId: vendor.id, seatsTotal: 10, licenseType: "trial" }) ); expect(res.status).toBe(400); @@ -406,13 +498,16 @@ describe("POST /api/licenses", () => { describe("GET /api/licenses", () => { it("lists created licenses, newest first", async () => { + const vendor1 = await createTestVendor({ name: "Microsoft VN" }); + const vendor2 = await createTestVendor({ name: "Adobe VN" }); + await POST( - postRequest({ name: "Windows Server 2022", vendor: "Microsoft", seatsTotal: 5, licenseType: "perpetual" }) + postRequest({ name: "Windows Server 2022", vendorId: vendor1.id, seatsTotal: 5, licenseType: "perpetual" }) ); // Ensure a distinct created_at ordering even on fast filesystems/clocks. await new Promise((resolve) => setTimeout(resolve, 5)); await POST( - postRequest({ name: "Adobe Creative Cloud", vendor: "Adobe", seatsTotal: 10, licenseType: "subscription" }) + postRequest({ name: "Adobe Creative Cloud", vendorId: vendor2.id, seatsTotal: 10, licenseType: "subscription" }) ); const res = await GET(); @@ -423,8 +518,8 @@ describe("GET /api/licenses", () => { expect(body).toHaveLength(2); // newest first - expect(body[0]).toMatchObject({ name: "Adobe Creative Cloud", vendor: "Adobe" }); - expect(body[1]).toMatchObject({ name: "Windows Server 2022", vendor: "Microsoft" }); + expect(body[0]).toMatchObject({ name: "Adobe Creative Cloud", vendor_id: vendor2.id }); + expect(body[1]).toMatchObject({ name: "Windows Server 2022", vendor_id: vendor1.id }); }); it("returns an empty list when there are no licenses", async () => { @@ -440,10 +535,11 @@ describe("auth gating on /api/licenses", () => { // session presence, per the flat permission model (no role/permission // logic). it("POST /api/licenses returns 401 without a session", async () => { + const vendor = await createTestVendor(); mockedAuth.mockResolvedValue(null); const res = await POST( - postRequest({ name: "Adobe Creative Cloud", vendor: "Adobe", seatsTotal: 10, licenseType: "subscription" }) + postRequest({ name: "Adobe Creative Cloud", vendorId: vendor.id, seatsTotal: 10, licenseType: "subscription" }) ); expect(res.status).toBe(401);