import test from "node:test"; import assert from "node:assert/strict"; import request from "supertest"; import { setup, signup } from "./helpers.js"; function makeAlog(title, fcIdx = 14) { const timex = [], temp1 = [], temp2 = []; for (let i = 0; i <= 20; i++) { timex.push(i * 30); temp1.push(200 + i); temp2.push(i < 3 ? 180 - i * 30 : 90 + (i - 3) * 7); } return JSON.stringify({ title, mode: "C", weight: [250, 212, "g"], timex, temp1, temp2, timeindex: [1, 8, fcIdx, 0, 0, 0, 20, 0] }); } const stubEval = async () => ({ summary: "ok", grade: "good", highlights: [], concerns: [], suggestions: [], planComparison: null }); test("profile picture: set, serve, remove, validate", async () => { const { agent } = await setup(); const { csrf } = await signup(agent, "avatar@example.com"); assert.equal((await agent.get("/api/auth/me")).body.user.hasAvatar, false); assert.equal((await agent.get("/api/account/avatar")).status, 404); // 1x1 PNG const png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; assert.equal( (await agent.put("/api/account/avatar").set("x-csrf-token", csrf).send({ dataUrl: png })).status, 200, ); assert.equal((await agent.get("/api/auth/me")).body.user.hasAvatar, true); const served = await agent.get("/api/account/avatar"); assert.equal(served.status, 200); assert.equal(served.headers["content-type"], "image/png"); // Junk rejected for (const dataUrl of ["not-an-image", "data:image/svg+xml;base64,PHN2Zz4=", 5]) assert.equal( (await agent.put("/api/account/avatar").set("x-csrf-token", csrf).send({ dataUrl })).status, 400, ); assert.equal( (await agent.delete("/api/account/avatar").set("x-csrf-token", csrf)).status, 200, ); assert.equal((await agent.get("/api/auth/me")).body.user.hasAvatar, false); assert.equal((await agent.get("/api/account/avatar")).status, 404); }); test("roasters: CRUD, default handling, uploads attach and reassign, per-roaster profiles", async () => { const { app, agent } = await setup({}, { evaluateRoast: stubEval }); const { csrf } = await signup(agent, "machines@example.com"); // First roaster auto-defaults; second doesn't const hottop = ( await agent.post("/api/roasters").set("x-csrf-token", csrf).send({ name: "Hottop", model: "KN-8828B-2K+" }) ).body.roaster; assert.equal(hottop.isDefault, true); const aillio = ( await agent.post("/api/roasters").set("x-csrf-token", csrf).send({ name: "Aillio" }) ).body.roaster; assert.equal(aillio.isDefault, false); assert.equal( (await agent.post("/api/roasters").set("x-csrf-token", csrf).send({ name: "" })).status, 400, ); // Uploads attach to the default roaster unless told otherwise const upDefault = await agent .post("/api/roasts") .set("x-csrf-token", csrf) .send({ filename: "d.alog", content: makeAlog("On default") }); assert.equal(upDefault.body.roast.roasterId, hottop.id); const upExplicit = await agent .post("/api/roasts") .set("x-csrf-token", csrf) .send({ filename: "e.alog", content: makeAlog("On aillio", 15), roasterId: aillio.id }); assert.equal(upExplicit.body.roast.roasterId, aillio.id); const list = await agent.get("/api/roasts"); assert.equal(list.body.roasts.find((r) => r.filename === "d.alog").roasterName, "Hottop"); // Roaster list counts its roasts const roasters = (await agent.get("/api/roasters")).body.roasters; assert.equal(roasters.find((r) => r.id === hottop.id).roastCount, 1); assert.equal(roasters.find((r) => r.id === aillio.id).roastCount, 1); // Per-roaster profile only sees that machine's roasts; default profile = default roaster const hottopProfile = ( await agent.get(`/api/roaster-profile?roaster=${hottop.id}`) ).body.profile; assert.equal(hottopProfile.n, 1); assert.equal(hottopProfile.roaster.name, "Hottop"); const defaultProfile = (await agent.get("/api/roaster-profile")).body.profile; assert.equal(defaultProfile.roaster.id, hottop.id); assert.equal(defaultProfile.n, 1); // Overrides: applied on top of learned medians; bad values rejected const put = await agent .put(`/api/roasters/${hottop.id}`) .set("x-csrf-token", csrf) .send({ overrides: { firstCrackTempC: 196, paceFactor: 1.1 } }); assert.equal(put.status, 200); const tweaked = ( await agent.get(`/api/roaster-profile?roaster=${hottop.id}`) ).body.profile; assert.equal(tweaked.medians.firstCrackTempC, 196); assert.notEqual(tweaked.learnedMedians.firstCrackTempC, 196); assert.equal(tweaked.paceFactorOverride, 1.1); assert.equal( ( await agent .put(`/api/roasters/${hottop.id}`) .set("x-csrf-token", csrf) .send({ overrides: { paceFactor: 9 } }) ).status, 400, ); // Reassign a roast between machines const reassign = await agent .put(`/api/roasts/${upDefault.body.roast.id}`) .set("x-csrf-token", csrf) .send({ roasterId: aillio.id }); assert.equal(reassign.status, 200); assert.equal( (await agent.get(`/api/roaster-profile?roaster=${aillio.id}`)).body.profile.n, 2, ); // Default transfer + delete keeps roasts (detached) await agent.put(`/api/roasters/${aillio.id}`).set("x-csrf-token", csrf).send({ isDefault: true }); const after = (await agent.get("/api/roasters")).body.roasters; assert.equal(after.find((r) => r.id === hottop.id).isDefault, false); assert.equal(after.find((r) => r.id === aillio.id).isDefault, true); assert.equal( (await agent.delete(`/api/roasters/${aillio.id}`).set("x-csrf-token", csrf)).status, 200, ); const survivors = await agent.get("/api/roasts"); assert.equal(survivors.body.roasts.length, 2); assert.equal( survivors.body.roasts.every((r) => r.roasterId === null || r.roasterId === hottop.id), true, ); // Ownership: a stranger sees nothing and can't touch anything const stranger = request.agent(app); const { csrf: strangerCsrf } = await signup(stranger, "other@example.com"); assert.equal((await stranger.get("/api/roasters")).body.roasters.length, 0); assert.equal( (await stranger.get(`/api/roaster-profile?roaster=${hottop.id}`)).status, 404, ); assert.equal( ( await stranger .put(`/api/roasters/${hottop.id}`) .set("x-csrf-token", strangerCsrf) .send({ name: "hijack" }) ).status, 404, ); assert.equal( ( await stranger .post("/api/roasts") .set("x-csrf-token", strangerCsrf) .send({ filename: "x.alog", content: makeAlog("x"), roasterId: hottop.id }) ).status, 404, ); });