import type { DateTime } from "@batch-cooking/date-tools"; import { ErrorCode, type SignupInput } from "@batch-cooking/shared"; import { faker } from "@faker-js/faker"; import { expect } from "chai"; import request from "supertest"; import { createApp } from "../src/app.js"; import { prisma } from "../src/db/prisma.js"; import { TEST_REFERENCE_DATE } from "../test-support/reference-date.js"; import { resetDatabase } from "../test-support/reset-db.js"; /** See `auth.test.ts` — same rationale for generating rather than hardcoding. */ function buildSignupPayload(): SignupInput { const firstName = faker.person.firstName(); const lastName = faker.person.lastName(); return { firstName, lastName, email: faker.internet.email({ firstName, lastName }).toLowerCase(), password: faker.internet.password({ length: 16 }), }; } /** `toISODate()` only returns `null` for an invalid `DateTime` — never the always-valid values here. */ function isoDate(date: DateTime): string { const iso = date.toISODate(); if (iso === null) throw new Error("Unexpectedly invalid DateTime in a test helper"); return iso; } /** The fixed test "today", as the `YYYY-MM-DD` string the `?date=` query expects. */ function today(): string { return isoDate(TEST_REFERENCE_DATE); } /** Resolves a reference row's id by its `reference-seed-data.ts` uid (also its DB `key`) — same helpers as `recipe.test.ts`. */ async function ingredientId(key: string): Promise { return (await prisma.ingredient.findFirstOrThrow({ where: { key } })).id; } async function unitId(key: string): Promise { return (await prisma.unit.findFirstOrThrow({ where: { key } })).id; } async function techStepId(key: string): Promise { return (await prisma.techStep.findFirstOrThrow({ where: { key } })).id; } describe("Cooking session", () => { const app = createApp(); beforeEach(async () => { await resetDatabase(); }); after(async () => { await prisma.$disconnect(); }); describe("GET /cooking-session", () => { it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => { const res = await request(app).get("/cooking-session").query({ date: today() }); expect(res.status).to.equal(401); expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED); }); it("rejects a malformed date with 400 VALIDATION_ERROR", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const res = await agent.get("/cooking-session").query({ date: "not-a-date" }); expect(res.status).to.equal(400); expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR); }); it("returns an empty plan when the profile has no household", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const res = await agent.get("/cooking-session").query({ date: today() }); expect(res.status).to.equal(200); expect(res.body.recipes).to.deep.equal([]); expect(res.body.phases).to.deep.equal([]); }); it("returns an empty plan when no planning covers that week", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); await agent.post("/house").send({ name: "Chez moi" }); const res = await agent.get("/cooking-session").query({ date: today() }); expect(res.status).to.equal(200); expect(res.body.phases).to.deep.equal([]); }); it("pools an identical prep step from two planned recipes into one merged-prep task", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const houseRes = await agent.post("/house").send({ name: "Chez moi" }); const houseId: number = houseRes.body.id; const authorId: number = houseRes.body.adminId; const onionId = await ingredientId("onion"); const pieceId = await unitId("piece"); const chopId = await techStepId("chop"); const simmerId = await techStepId("simmer"); /** A recipe: one pure-prep "chop onion" step, then one simmer step. */ async function makeRecipe(name: string, onionQty: number) { return prisma.recipe.create({ data: { name, authorId, portions: 4, steps: { create: [ { order: 0, description: "Émincer les oignons", techSteps: { create: [ { techStepId: chopId, order: 0, ingredients: { create: [ { ingredientId: onionId, quantity: onionQty, unitId: pieceId, start: 0, end: 1, }, ], }, }, ], }, }, { order: 1, description: "Faire mijoter", techSteps: { create: [{ techStepId: simmerId, order: 0 }] }, }, ], }, }, }); } const soupe = await makeRecipe("Soupe", 2); const tarte = await makeRecipe("Tarte", 3); const planning = await prisma.planning.create({ data: { houseId, startDate: TEST_REFERENCE_DATE.minus({ days: 2 }).toJSDate(), finishDate: TEST_REFERENCE_DATE.plus({ days: 2 }).toJSDate(), }, }); await prisma.planningItem.createMany({ data: [ { planningId: planning.id, weekDay: "lundi", meal: "dejeuner", recipeId: soupe.id, portions: 4, }, { planningId: planning.id, weekDay: "mardi", meal: "diner", recipeId: tarte.id, portions: 4, }, ], }); const res = await agent.get("/cooking-session").query({ date: today() }); expect(res.status).to.equal(200); expect(res.body.recipes.map((r: { name: string }) => r.name)).to.have.members([ "Soupe", "Tarte", ]); const mise = res.body.phases[0]; expect(mise.kind).to.equal("mise-en-place"); const merged = mise.tasks.filter((t: { kind: string }) => t.kind === "merged-prep"); expect(merged).to.have.length(1); expect(merged[0].technique.key).to.equal("chop"); expect(merged[0].ingredients[0].ingredient.key).to.equal("onion"); expect(merged[0].ingredients[0].quantity).to.equal(5); expect(merged[0].sourceRecipes).to.have.length(2); // The simmer steps land in a later phase, and one shows as background. const later = res.body.phases.slice(1); const backgrounds = later.flatMap((p: { background: unknown[] }) => p.background); expect(backgrounds.length).to.be.greaterThan(0); }); }); });