import assert from "node:assert/strict"; import { Given, Then, When } from "@cucumber/cucumber"; import { getEnglishKey } from "../../src/db/catalog-en-keys.js"; import { prisma } from "../../src/db/prisma.js"; import { TEST_REFERENCE_DATE } from "../../test-support/reference-date.js"; import type { CustomWorld } from "../support/world.js"; /** * Resolves a reference ingredient by its seeded French name — every * scenario below names an ingredient by its `reference-seed-data.ts` name, * never a raw id or its slug `key` directly, so this slugifies before * matching. */ async function findIngredientId(name: string): Promise { const ingredient = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey(name) }, }); return ingredient.id; } When("I request the recipe catalog", async function (this: CustomWorld) { this.response = await this.agent.get("/recipes"); }); When("I request the recipe catalog tab {string}", async function (this: CustomWorld, tab: string) { this.response = await this.agent.get("/recipes").query({ tab }); }); Then( "the recipe catalog response should include {string}", function (this: CustomWorld, name: string) { const names = (this.response.body as Array<{ name: string }>).map((recipe) => recipe.name); assert.ok(names.includes(name), `expected ${JSON.stringify(names)} to include "${name}"`); }, ); When( "I create a recipe named {string} with ingredient {string} and step {string}", async function (this: CustomWorld, name: string, ingredientName: string, step: string) { const ingredientId = await findIngredientId(ingredientName); this.response = await this.agent.post("/recipes").send({ name, dietIds: [], ingredients: [{ ingredientId, quantity: 1, unit: "unité" }], steps: [{ description: step }], }); }, ); When( "I create a recipe named {string} with unknown ingredient id {int} and step {string}", async function (this: CustomWorld, name: string, unknownIngredientId: number, step: string) { this.response = await this.agent.post("/recipes").send({ name, dietIds: [], ingredients: [{ ingredientId: unknownIngredientId, quantity: 1, unit: "unité" }], steps: [{ description: step }], }); }, ); Then( "the created recipe should have ingredient {string} and step {string}", function (this: CustomWorld, ingredientName: string, step: string) { const body = this.response.body as { ingredients: Array<{ ingredient: { key: string } }>; steps: Array<{ description: string }>; }; const expectedKey = getEnglishKey(ingredientName); assert.ok(body.ingredients.some((line) => line.ingredient.key === expectedKey)); assert.ok(body.steps.some((s) => s.description === step)); }, ); // Created directly via Prisma (with a nested ingredient + step), not through // the API — same rationale as `planning.steps.ts`'s equivalent "already // exists" step: this is background state the scenario needs in place before // its actual `When`, not the behavior under test. `authorId` is the // currently-logged-in agent's own profile — `visibility` defaults to // `PERSONAL` (schema.prisma), matching a recipe this agent just created for // themselves. Given( "a recipe named {string} already exists with ingredient {string} and step {string}", async function (this: CustomWorld, name: string, ingredientName: string, step: string) { const ingredientId = await findIngredientId(ingredientName); const me = await this.agent.get("/auth/me"); await prisma.recipe.create({ data: { name, authorId: me.body.id, ingredients: { create: [{ ingredientId, quantity: 1, unit: "unité" }] }, steps: { create: [{ description: step, order: 0 }] }, }, }); }, ); // Same as above but `visibility: PUBLIC` — needed for scenarios where a // *second* user must be able to see (though not necessarily edit) the // recipe, e.g. the "only the author can edit" scenario: a `PERSONAL` // recipe would 404 for anyone else before the authorship check even runs // (see `recipe.service.ts`'s `canView`). Given( "a public recipe named {string} already exists with ingredient {string} and step {string}", async function (this: CustomWorld, name: string, ingredientName: string, step: string) { const ingredientId = await findIngredientId(ingredientName); const me = await this.agent.get("/auth/me"); await prisma.recipe.create({ data: { name, authorId: me.body.id, visibility: "PUBLIC", ingredients: { create: [{ ingredientId, quantity: 1, unit: "unité" }] }, steps: { create: [{ description: step, order: 0 }] }, }, }); }, ); // Distinct from `planning.steps.ts`'s "my household has a planning covering // today with recipe {string}..." — that step always creates a *new* recipe // row with the given name, which wouldn't exercise the actual `RECIPE_IN_USE` // check against a recipe this feature already created. This step instead // looks up the already-existing recipe by name and points the planning item // at its real id. Given( "my household has a planning that uses the recipe named {string}", async function (this: CustomWorld, recipeName: string) { const houseRes = await this.agent.post("/house").send({ name: "Foyer de test" }); const houseId: number = houseRes.body.id; const recipe = await prisma.recipe.findFirstOrThrow({ where: { name: recipeName } }); 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.create({ data: { planningId: planning.id, weekDay: "lundi", meal: "diner", recipeId: recipe.id }, }); }, ); When("I delete the recipe named {string}", async function (this: CustomWorld, name: string) { const recipe = await prisma.recipe.findFirstOrThrow({ where: { name } }); this.response = await this.agent.delete(`/recipes/${recipe.id}`); }); When("I favorite the recipe named {string}", async function (this: CustomWorld, name: string) { const recipe = await prisma.recipe.findFirstOrThrow({ where: { name } }); this.response = await this.agent.post(`/recipes/${recipe.id}/favorite`); }); When( "the second user tries to modify the recipe named {string}", async function (this: CustomWorld, name: string) { const ingredientId = await findIngredientId("Tomate"); const recipe = await prisma.recipe.findFirstOrThrow({ where: { name } }); this.secondResponse = await this.secondAgent.patch(`/recipes/${recipe.id}`).send({ name, dietIds: [], ingredients: [{ ingredientId, quantity: 1, unit: "unité" }], steps: [{ description: "Hack" }], }); }, );