feat(vehicles): PATCH /api/vehicles/[id] accepts unit_type
Lets the inline-edit flow on /dashboard/plant set the vehicle's unit_type (fuel|plant) so the plant-hours import route's type-mismatch warnings can be resolved by reclassifying the vehicle. Adds a route.test.ts mirroring the attention-dismiss precedent: 11 cases covering happy paths for unit_type='plant'/'fuel', invalid unit_type dropped (with/without other valid fields), existing description+project_code behaviour preserved, JSON parse errors, empty-payload errors, and PB write failures.
This commit is contained in:
parent
b22badb4f0
commit
c9789d89f4
2 changed files with 153 additions and 4 deletions
146
src/app/api/vehicles/[id]/route.test.ts
Normal file
146
src/app/api/vehicles/[id]/route.test.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from "vitest";
|
||||
|
||||
const updateMock = vi.fn();
|
||||
|
||||
vi.mock("@/lib/pb", () => ({
|
||||
pbAdmin: vi.fn(async () => ({
|
||||
collection: () => ({
|
||||
update: updateMock,
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
import { PATCH } from "./route";
|
||||
|
||||
const ID = "abcdefghijklmno"; // 15 chars, matches PB ID pattern
|
||||
|
||||
function patch(id: string, body: Record<string, unknown>) {
|
||||
return PATCH(
|
||||
new Request("http://x", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
{ params: Promise.resolve({ id }) },
|
||||
);
|
||||
}
|
||||
|
||||
describe("PATCH /api/vehicles/[id]", () => {
|
||||
beforeAll(() => {
|
||||
vi.stubEnv("POCKETBASE_ADMIN_EMAIL", "test@example.com");
|
||||
vi.stubEnv("POCKETBASE_ADMIN_PASSWORD", "test-password");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
updateMock.mockReset();
|
||||
});
|
||||
|
||||
it("returns 400 for non-JSON body", async () => {
|
||||
const res = await PATCH(
|
||||
new Request("http://x", {
|
||||
method: "PATCH",
|
||||
body: "not-json{",
|
||||
}),
|
||||
{ params: Promise.resolve({ id: ID }) },
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
expect(await res.json()).toEqual({ error: "Body must be JSON" });
|
||||
expect(updateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 400 when no editable fields are supplied", async () => {
|
||||
const res = await patch(ID, { foo: "bar" });
|
||||
expect(res.status).toBe(400);
|
||||
expect(await res.json()).toEqual({ error: "No editable fields supplied" });
|
||||
expect(updateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts unit_type='plant' and forwards it to PB", async () => {
|
||||
updateMock.mockResolvedValueOnce({ id: ID, unit_type: "plant" });
|
||||
const res = await patch(ID, { unit_type: "plant" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ vehicle: { id: ID, unit_type: "plant" } });
|
||||
expect(updateMock).toHaveBeenCalledWith(ID, { unit_type: "plant" });
|
||||
});
|
||||
|
||||
it("accepts unit_type='fuel' and forwards it to PB", async () => {
|
||||
updateMock.mockResolvedValueOnce({ id: ID, unit_type: "fuel" });
|
||||
const res = await patch(ID, { unit_type: "fuel" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(updateMock).toHaveBeenCalledWith(ID, { unit_type: "fuel" });
|
||||
});
|
||||
|
||||
it("drops an invalid unit_type (not 'fuel' or 'plant') and 400s if it's the only field", async () => {
|
||||
const res = await patch(ID, { unit_type: "truck" });
|
||||
expect(res.status).toBe(400);
|
||||
expect(await res.json()).toEqual({ error: "No editable fields supplied" });
|
||||
expect(updateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("drops an invalid unit_type but still writes other valid fields", async () => {
|
||||
updateMock.mockResolvedValueOnce({ id: ID, description: "New" });
|
||||
const res = await patch(ID, { description: "New", unit_type: "truck" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(updateMock).toHaveBeenCalledWith(ID, { description: "New" });
|
||||
});
|
||||
|
||||
it("continues to support description + project_code updates", async () => {
|
||||
updateMock.mockResolvedValueOnce({ id: ID, description: "D", project_code: "P" });
|
||||
const res = await patch(ID, { description: "D", project_code: "P" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(updateMock).toHaveBeenCalledWith(ID, {
|
||||
description: "D",
|
||||
project_code: "P",
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts all three editable fields together", async () => {
|
||||
updateMock.mockResolvedValueOnce({
|
||||
id: ID,
|
||||
description: "D",
|
||||
project_code: "P",
|
||||
unit_type: "plant",
|
||||
});
|
||||
const res = await patch(ID, {
|
||||
description: "D",
|
||||
project_code: "P",
|
||||
unit_type: "plant",
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(updateMock).toHaveBeenCalledWith(ID, {
|
||||
description: "D",
|
||||
project_code: "P",
|
||||
unit_type: "plant",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 500 when PB update throws", async () => {
|
||||
updateMock.mockRejectedValueOnce(new Error("write conflict"));
|
||||
const res = await patch(ID, { unit_type: "plant" });
|
||||
expect(res.status).toBe(500);
|
||||
expect(await res.json()).toEqual({ error: "write conflict" });
|
||||
});
|
||||
|
||||
it("trims description + project_code before forwarding", async () => {
|
||||
updateMock.mockResolvedValueOnce({ id: ID });
|
||||
const res = await patch(ID, {
|
||||
description: " Padded ",
|
||||
project_code: " P-001 ",
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(updateMock).toHaveBeenCalledWith(ID, {
|
||||
description: "Padded",
|
||||
project_code: "P-001",
|
||||
});
|
||||
});
|
||||
|
||||
it("drops an empty description (after trimming) but keeps project_code", async () => {
|
||||
updateMock.mockResolvedValueOnce({ id: ID, project_code: "P" });
|
||||
const res = await patch(ID, { description: " ", project_code: "P" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(updateMock).toHaveBeenCalledWith(ID, { project_code: "P" });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { pbAdmin } from "@/lib/pb";
|
||||
import type { UnitType } from "@/lib/pb-types";
|
||||
|
||||
/**
|
||||
* PATCH /api/vehicles/[id]
|
||||
*
|
||||
* Updates one or more editable fields on a vehicle record. Currently
|
||||
* supports `description` and `project_code`; both are optional and only
|
||||
* present keys are written. Unknown / immutable fields are silently
|
||||
* dropped (project_code locked for now would be a stricter change).
|
||||
* Updates editable fields on a vehicle record. Currently supports
|
||||
* `description`, `project_code`, and `unit_type`. Only present keys
|
||||
* are written; unknown fields are silently dropped.
|
||||
*/
|
||||
export async function PATCH(
|
||||
request: Request,
|
||||
|
|
@ -29,6 +29,9 @@ export async function PATCH(
|
|||
if (typeof body.project_code === "string") {
|
||||
payload.project_code = body.project_code.trim();
|
||||
}
|
||||
if (body.unit_type === "fuel" || body.unit_type === "plant") {
|
||||
payload.unit_type = body.unit_type as UnitType;
|
||||
}
|
||||
|
||||
if (Object.keys(payload).length === 0) {
|
||||
return NextResponse.json(
|
||||
|
|
|
|||
Loading…
Reference in a new issue