shoc-frontend-new/src/test/domain/vendors/api/vendor-company-roster-api.test.ts
Alexandre Brandizzi 7fdef2a46a
Some checks are pending
Frontend checks / Build and test (push) Waiting to run
Frontend checks / governance (push) Waiting to run
Frontend checks / Visual regression (push) Waiting to run
Deploy / deploy (push) Waiting to run
fix: classify duplicate vendor conflicts from parsed body (#149)
Co-authored-by: Codex Review Integration <codex-review@local.invalid>
2026-08-27 14:33:17 -04:00

249 lines
8.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { API_PATHS } from "@/api/api-paths";
const apiGet = vi.fn();
const apiPost = vi.fn();
const apiPut = vi.fn();
const apiPatch = vi.fn();
vi.mock("@/api/api", () => ({
apiGet: (...args: unknown[]) => apiGet(...args),
apiPost: (...args: unknown[]) => apiPost(...args),
apiPut: (...args: unknown[]) => apiPut(...args),
apiPatch: (...args: unknown[]) => apiPatch(...args),
}));
vi.mock("ky", () => ({
isHTTPError: (error: unknown) =>
error !== null && typeof error === "object" && "__kyHTTPError" in error,
}));
import { vendorCompanyRosterApi } from "@/domain/vendors/api/vendor-company-roster-api";
import { isVendorRosterConflictError } from "@/domain/vendors/lib/vendor-roster-conflict";
function httpError(status: number, body: unknown): Error {
const error = new Error("HTTPError") as Error & {
__kyHTTPError: true;
response: { status: number; json: () => Promise<unknown>; text: () => Promise<string> };
};
error.__kyHTTPError = true;
error.response = {
status,
json: () => Promise.resolve(body),
text: () => Promise.resolve(JSON.stringify(body)),
};
return error;
}
describe("vendorCompanyRosterApi", () => {
beforeEach(() => {
apiGet.mockReset();
apiPost.mockReset();
apiPut.mockReset();
apiPatch.mockReset();
});
it("fetches by vendorId or companyId with the correct query params", async () => {
apiGet.mockResolvedValueOnce({ data: { companyId: 5, name: "Solo Co" } });
await vendorCompanyRosterApi.get({ vendorId: 7 });
expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendorCompanyRoster.base, {
searchParams: { vendorId: "7" },
});
apiGet.mockResolvedValueOnce({ data: { companyId: 5, name: "Solo Co" } });
await vendorCompanyRosterApi.get({ companyId: 5 });
expect(apiGet).toHaveBeenLastCalledWith(API_PATHS.vendorCompanyRoster.base, {
searchParams: { companyId: "5" },
});
});
it("posts the canonicalized create payload", async () => {
apiPost.mockResolvedValueOnce({ data: { companyId: 9, name: "Solo Co" } });
await vendorCompanyRosterApi.create({
name: "Solo Co",
companyPhone: "314-555-0100",
technicians: [{ contactName: "Adam", phone: "3145550198", preferredContact: "Phone" }],
});
expect(apiPost).toHaveBeenCalledWith(
API_PATHS.vendorCompanyRoster.base,
expect.objectContaining({
name: "Solo Co",
companyPhone: "(314) 555-0100",
technicians: [expect.objectContaining({ phone: "(314) 555-0198" })],
}),
);
expect(apiPost.mock.calls[0]?.[1]).not.toHaveProperty("rowVersion");
});
it("puts the rowVersion on the update payload", async () => {
apiPut.mockResolvedValueOnce({ data: { companyId: 5, name: "Solo Co" } });
await vendorCompanyRosterApi.update(5, { name: "Solo Co" }, "rv-1");
expect(apiPut).toHaveBeenCalledWith(
API_PATHS.vendorCompanyRoster.byCompany(5),
expect.objectContaining({ name: "Solo Co", rowVersion: "rv-1" }),
);
});
it("throws an open-work-order conflict on a 409 with blocked work orders", async () => {
apiPut.mockRejectedValueOnce(
httpError(409, {
blockedWorkOrders: [
{ workOrderId: 501, workOrderNumber: "WO-501", workOrderTitle: "Boiler" },
],
}),
);
await expect(vendorCompanyRosterApi.update(5, { name: "Solo Co" }, "rv-1")).rejects.toSatisfy(
(error: unknown) => {
if (!isVendorRosterConflictError(error)) return false;
return error.conflict.kind === "open-work-orders";
},
);
});
it("throws a stale conflict on a 409 without blocked work orders", async () => {
apiPut.mockRejectedValueOnce(httpError(409, { message: "rowversion mismatch" }));
await expect(vendorCompanyRosterApi.update(5, { name: "Solo Co" }, "rv-1")).rejects.toSatisfy(
(error: unknown) => {
if (!isVendorRosterConflictError(error)) return false;
return error.conflict.kind === "stale";
},
);
});
it("throws a duplicate conflict for the stable create response", async () => {
apiPost.mockRejectedValueOnce(
httpError(409, {
code: "duplicate_vendor_company_name",
message: "Another vendor company already uses that name.",
}),
);
await expect(vendorCompanyRosterApi.create({ name: "Solo Co" })).rejects.toSatisfy(
(error: unknown) => {
if (!isVendorRosterConflictError(error)) return false;
return (
error.conflict.kind === "duplicate" &&
error.conflict.message === "Another vendor company already uses that name."
);
},
);
});
it("classifies a duplicate 409 from parsed error.data when the response body is consumed", async () => {
const conflictError = new Error("HTTPError") as Error & {
__kyHTTPError: true;
data: unknown;
response: { status: number; json: () => Promise<unknown>; text: () => Promise<string> };
};
conflictError.__kyHTTPError = true;
conflictError.data = {
code: "duplicate_vendor_company_name",
message: "Another vendor company already uses that name.",
};
const consumedBody = () => {
const rejection = Promise.reject(new TypeError("Body has already been consumed"));
rejection.catch(() => {});
return rejection;
};
conflictError.response = {
status: 409,
json: () => consumedBody(),
text: () => consumedBody(),
};
apiPost.mockRejectedValueOnce(conflictError);
await expect(vendorCompanyRosterApi.create({ name: "Solo Co" })).rejects.toSatisfy(
(error: unknown) => {
if (!isVendorRosterConflictError(error)) return false;
return (
error.conflict.kind === "duplicate" &&
error.conflict.message === "Another vendor company already uses that name."
);
},
);
});
it("rethrows non-conflict errors untouched", async () => {
const generic = new Error("boom");
apiPost.mockRejectedValueOnce(generic);
await expect(vendorCompanyRosterApi.create({ name: "Solo Co" })).rejects.toBe(generic);
});
it("patches the additive payload without a technician id and without PUT", async () => {
apiPatch.mockResolvedValueOnce({ data: { companyId: 5, name: "Solo Co" } });
await vendorCompanyRosterApi.addTechnicians(5, {
rowVersion: "rv-1",
addTechnicians: [
{
id: 99,
contactName: "Adam",
phone: "3145550198",
email: "",
preferredContact: "Phone",
tradeSpecialties: "Plumbing",
isActive: true,
},
],
});
expect(apiPatch).toHaveBeenCalledWith(
API_PATHS.vendorCompanyRoster.byCompany(5),
expect.objectContaining({
rowVersion: "rv-1",
addTechnicians: [expect.objectContaining({ contactName: "Adam", phone: "(314) 555-0198" })],
}),
);
const body = apiPatch.mock.calls[0]?.[1] as Record<string, unknown>;
expect(body).not.toHaveProperty("companyFields");
expect((body.addTechnicians as Array<Record<string, unknown>>)[0]).not.toHaveProperty("id");
expect(apiPut).not.toHaveBeenCalled();
expect(apiPost).not.toHaveBeenCalled();
});
it("includes companyFields on the additive patch when provided", async () => {
apiPatch.mockResolvedValueOnce({ data: { companyId: 5, name: "Solo Co" } });
await vendorCompanyRosterApi.addTechnicians(5, {
rowVersion: "rv-2",
addTechnicians: [
{ contactName: "Adam", phone: "", email: "", tradeSpecialties: "", isActive: true },
],
companyFields: { notes: "Updated notes" },
});
expect(apiPatch).toHaveBeenCalledWith(
API_PATHS.vendorCompanyRoster.byCompany(5),
expect.objectContaining({
rowVersion: "rv-2",
companyFields: { notes: "Updated notes" },
}),
);
});
it("throws a stale conflict on a 409 from the additive patch", async () => {
apiPatch.mockRejectedValueOnce(httpError(409, { message: "rowversion mismatch" }));
await expect(
vendorCompanyRosterApi.addTechnicians(5, {
rowVersion: "rv-1",
addTechnicians: [
{ contactName: "Adam", phone: "", email: "", tradeSpecialties: "", isActive: true },
],
}),
).rejects.toSatisfy((error: unknown) => {
if (!isVendorRosterConflictError(error)) return false;
return error.conflict.kind === "stale";
});
});
});