Files
roast_command_center/test/inventory.test.js
T
2026-08-09 14:30:42 -04:00

254 lines
9.0 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import { setup, signup } from "./helpers.js";
async function bootstrapPlan(agent, csrf) {
const r = await agent
.post("/api/plans")
.set("x-csrf-token", csrf)
.send({ plan: { fields: { 0.1: "Test coffee" } } });
return r.body.plan.id;
}
test("inventory: auth and CSRF are required on every route", async () => {
const { app } = await setup();
const anon = request.agent(app);
assert.equal((await anon.get("/api/inventory")).status, 401);
assert.equal((await anon.post("/api/inventory").send({})).status, 401);
const { agent, csrf } = await (async () => {
const a = request.agent(app);
const { csrf: c } = await signup(a, "[email protected]");
return { agent: a, csrf: c };
})();
assert.equal(
(await agent.post("/api/inventory").send({ origin: "X", initialWeightG: 100 })).status,
403,
);
const created = await agent
.post("/api/inventory")
.set("x-csrf-token", csrf)
.send({ origin: "Huila", initialWeightG: 1000 });
assert.equal(created.status, 201);
assert.equal(
(await agent.put(`/api/inventory/${created.body.lot.id}`).send({ origin: "Y" })).status,
403,
);
assert.equal(
(await agent.delete(`/api/inventory/${created.body.lot.id}`)).status,
403,
);
assert.equal(
(
await agent
.post(`/api/inventory/${created.body.lot.id}/consume`)
.send({ weightG: 10 })
).status,
403,
);
});
test("inventory: create, list, and ownership isolation", async () => {
const { app } = await setup();
const first = request.agent(app);
const second = request.agent(app);
const { csrf: firstCsrf } = await signup(first, "[email protected]");
await signup(second, "[email protected]");
const bad = await first
.post("/api/inventory")
.set("x-csrf-token", firstCsrf)
.send({ origin: "", initialWeightG: 100 });
assert.equal(bad.status, 400);
const badWeight = await first
.post("/api/inventory")
.set("x-csrf-token", firstCsrf)
.send({ origin: "Huila", initialWeightG: -5 });
assert.equal(badWeight.status, 400);
const created = await first
.post("/api/inventory")
.set("x-csrf-token", firstCsrf)
.send({ origin: "Huila, Colombia", variety: "Caturra", initialWeightG: 2000 });
assert.equal(created.status, 201);
assert.equal(created.body.lot.remainingWeightG, 2000);
assert.equal((await first.get("/api/inventory")).body.lots.length, 1);
assert.equal((await second.get("/api/inventory")).body.lots.length, 0);
assert.equal((await second.get(`/api/inventory/${created.body.lot.id}`)).status, 404);
});
test("inventory: consume decrements remaining, allows negative, is idempotent per plan, and logs", async () => {
const { app } = await setup();
const agent = request.agent(app);
const { csrf } = await signup(agent, "[email protected]");
const planId = await bootstrapPlan(agent, csrf);
const lot = (
await agent
.post("/api/inventory")
.set("x-csrf-token", csrf)
.send({ origin: "Huila", initialWeightG: 300 })
).body.lot;
const first = await agent
.post(`/api/inventory/${lot.id}/consume`)
.set("x-csrf-token", csrf)
.send({ weightG: 250, roastPlanId: planId });
assert.equal(first.status, 201);
assert.equal(first.body.lot.remainingWeightG, 50);
// A second draw against the SAME plan is rejected — one draw-down per roast plan, ever.
const dupe = await agent
.post(`/api/inventory/${lot.id}/consume`)
.set("x-csrf-token", csrf)
.send({ weightG: 10, roastPlanId: planId });
assert.equal(dupe.status, 409);
assert.equal(dupe.body.code, "already_consumed");
assert.equal(
(await agent.get(`/api/inventory/${lot.id}`)).body.lot.remainingWeightG,
50,
"remaining must be unchanged after the rejected duplicate",
);
// A manual (plan-less) draw is unlimited and can push remaining negative — an honest
// signal of paperwork/shelf drift, not clamped.
const manual = await agent
.post(`/api/inventory/${lot.id}/consume`)
.set("x-csrf-token", csrf)
.send({ weightG: 100 });
assert.equal(manual.status, 201);
assert.equal(manual.body.lot.remainingWeightG, -50);
const zero = await agent
.post(`/api/inventory/${lot.id}/consume`)
.set("x-csrf-token", csrf)
.send({ weightG: 0 });
assert.equal(zero.status, 400);
const withLog = await agent.get(`/api/inventory/${lot.id}`);
assert.equal(withLog.body.log.length, 2);
// Newest first: the manual (plan-less) draw has no plan title; the earlier draw does.
assert.equal(withLog.body.log[0].roastPlanId, null);
assert.equal(withLog.body.log[0].planTitle, null);
assert.equal(withLog.body.log[1].roastPlanId, planId);
assert.equal(withLog.body.log[1].planTitle, "Test coffee");
});
test("inventory: custom name is created, updated, listed, and falls back cleanly when unset", async () => {
const { app } = await setup();
const agent = request.agent(app);
const { csrf } = await signup(agent, "[email protected]");
// No name given: defaults to "" so the client's `lot.name || lot.origin` fallback applies.
const unnamed = await agent
.post("/api/inventory")
.set("x-csrf-token", csrf)
.send({ origin: "Huila, Colombia", initialWeightG: 500 });
assert.equal(unnamed.status, 201);
assert.equal(unnamed.body.lot.name, "");
assert.equal(unnamed.body.lot.origin, "Huila, Colombia");
// Named on create.
const named = await agent
.post("/api/inventory")
.set("x-csrf-token", csrf)
.send({ origin: "Yirgacheffe", name: " Spring Ethiopia ", initialWeightG: 300 });
assert.equal(named.status, 201);
assert.equal(named.body.lot.name, "Spring Ethiopia", "name is trimmed");
// Name is returned in both the list and the detail endpoints.
const list = await agent.get("/api/inventory");
const listedNamed = list.body.lots.find((l) => l.id === named.body.lot.id);
assert.equal(listedNamed.name, "Spring Ethiopia");
const listedUnnamed = list.body.lots.find((l) => l.id === unnamed.body.lot.id);
assert.equal(listedUnnamed.name, "");
const detail = await agent.get(`/api/inventory/${named.body.lot.id}`);
assert.equal(detail.body.lot.name, "Spring Ethiopia");
// Rename via PUT; other fields are left untouched (partial-update pattern).
const renamed = await agent
.put(`/api/inventory/${unnamed.body.lot.id}`)
.set("x-csrf-token", csrf)
.send({ name: "Reserve lot" });
assert.equal(renamed.status, 200);
assert.equal(renamed.body.lot.name, "Reserve lot");
assert.equal(renamed.body.lot.origin, "Huila, Colombia");
// A PUT that omits `name` leaves the existing name unchanged.
const untouched = await agent
.put(`/api/inventory/${unnamed.body.lot.id}`)
.set("x-csrf-token", csrf)
.send({ notes: "recheck moisture" });
assert.equal(untouched.body.lot.name, "Reserve lot");
// Clearing the name back out is honored explicitly.
const cleared = await agent
.put(`/api/inventory/${unnamed.body.lot.id}`)
.set("x-csrf-token", csrf)
.send({ name: "" });
assert.equal(cleared.body.lot.name, "");
});
test("inventory: consuming against another user's plan is refused", async () => {
const { app } = await setup();
const owner = request.agent(app);
const attacker = request.agent(app);
const { csrf: ownerCsrf } = await signup(owner, "[email protected]");
const { csrf: attackerCsrf } = await signup(attacker, "[email protected]");
const planId = await bootstrapPlan(owner, ownerCsrf);
const lot = (
await attacker
.post("/api/inventory")
.set("x-csrf-token", attackerCsrf)
.send({ origin: "Huila", initialWeightG: 500 })
).body.lot;
const consume = await attacker
.post(`/api/inventory/${lot.id}/consume`)
.set("x-csrf-token", attackerCsrf)
.send({ weightG: 50, roastPlanId: planId });
assert.equal(consume.status, 404);
});
test("inventory: edit never accepts remainingWeightG directly, but shifting initialWeightG shifts remaining by the delta", async () => {
const { app } = await setup();
const agent = request.agent(app);
const { csrf } = await signup(agent, "[email protected]");
const lot = (
await agent
.post("/api/inventory")
.set("x-csrf-token", csrf)
.send({ origin: "Huila", initialWeightG: 1000 })
).body.lot;
await agent
.post(`/api/inventory/${lot.id}/consume`)
.set("x-csrf-token", csrf)
.send({ weightG: 400 });
// Attempting to set remainingWeightG directly is silently ignored.
const sneaky = await agent
.put(`/api/inventory/${lot.id}`)
.set("x-csrf-token", csrf)
.send({ remainingWeightG: 999999 });
assert.equal(sneaky.body.lot.remainingWeightG, 600);
// Correcting the recorded initial weight (e.g. a scale error) shifts remaining by the delta.
const corrected = await agent
.put(`/api/inventory/${lot.id}`)
.set("x-csrf-token", csrf)
.send({ initialWeightG: 1100 });
assert.equal(corrected.body.lot.initialWeightG, 1100);
assert.equal(corrected.body.lot.remainingWeightG, 700);
const archived = await agent
.put(`/api/inventory/${lot.id}`)
.set("x-csrf-token", csrf)
.send({ archived: true });
assert.equal(archived.body.lot.archived, true);
assert.equal(
(await agent.delete(`/api/inventory/${lot.id}`).set("x-csrf-token", csrf)).status,
200,
);
assert.equal((await agent.get(`/api/inventory/${lot.id}`)).status, 404);
});