Le catalogue de référence (diets/allergènes/ingrédients) était écrit en
français dans reference-seed-data.ts, avec une table de correspondance
séparée (catalog-en-keys.ts, 666 lignes, ~563 entrées) traduisant chaque
libellé français vers une clé anglaise snake_case, elle-même utilisée pour
peupler la colonne `key` en base et régénérer translation.json. Décision :
remplacer par un authoring 100% anglais camelCase directement dans le seed
— plus de détour, plus de table de correspondance.
- `Ingredient.name`/`allergenNames`/`dietNames` → `uid`/`allergenUids`/
`dietUids`, valeurs en camelCase directement (ex: "Tomate" → "tomato",
"Fruits à coque" → "treeNuts").
- `IngredientCategory`/`IngredientSubcategory` (enums Prisma) renommés du
français SCREAMING_SNAKE_CASE (`PRODUITS_FRAIS`, `LEGUMES`...) vers
l'anglais camelCase (`freshProduce`, `vegetables`...) — même mécanique
de migration que le renommage d'enum précédent
(20260818113250_ingredient_taxonomy_rework) : nouvelle colonne avec
valeur par défaut sûre, jamais de cast direct (aucune valeur commune
entre ancien et nouvel enum), seedReferenceData() corrige chaque ligne
au démarrage suivant.
- Migration `20260819180000_catalog_camel_case_uids` : renomme les clés
existantes (diet/category/ingredients, même mécanique que
20260818193000_catalog_keys_to_english) + swap des deux enums. Un cas
particulier corrigé à la main : "sesame_seeds" était à la fois la clé
d'un allergène (Category) et d'un ingrédient qui se référence lui-même
("Graines de sésame") — les deux tables ont besoin de leur propre
UPDATE.
- `catalog-en-keys.ts`, `slugify.ts`, `generate-catalog-i18n.ts`,
`validate-catalog-en-keys.ts` — supprimés (plus de raison d'être).
Conséquence assumée : `translation.json` n'est plus régénéré
automatiquement, c'est désormais la seule source du texte FR, tenue à
jour à la main en parallèle du uid (même clé qui les relie).
- `packages/shared/src/types/reference.ts`, `apps/web`'s
`ingredient-icons.tsx` (CATEGORY_ICON/SUBCATEGORY_ICON), fixtures
Cypress codées en dur — mis à jour avec les nouveaux noms.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
198 lines
7.6 KiB
TypeScript
198 lines
7.6 KiB
TypeScript
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 { resetDatabase } from "../test-support/reset-db.js";
|
|
|
|
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 }),
|
|
};
|
|
}
|
|
|
|
describe("Profile", () => {
|
|
const app = createApp();
|
|
|
|
beforeEach(async () => {
|
|
await resetDatabase();
|
|
});
|
|
|
|
after(async () => {
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
describe("PATCH /profile/diet", () => {
|
|
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
|
|
const res = await request(app).patch("/profile/diet").send({ dietId: 1 });
|
|
|
|
expect(res.status).to.equal(401);
|
|
expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
|
|
});
|
|
|
|
it("sets the profile's regime to a valid, seeded diet", async () => {
|
|
const agent = request.agent(app);
|
|
await agent.post("/auth/signup").send(buildSignupPayload());
|
|
const diet = await prisma.diet.findFirstOrThrow({
|
|
where: { key: "vegetarian" },
|
|
});
|
|
|
|
const res = await agent.patch("/profile/diet").send({ dietId: diet.id });
|
|
|
|
expect(res.status).to.equal(200);
|
|
expect(res.body.dietId).to.equal(diet.id);
|
|
});
|
|
|
|
it("clears the regime when dietId is null", async () => {
|
|
const agent = request.agent(app);
|
|
await agent.post("/auth/signup").send(buildSignupPayload());
|
|
const diet = await prisma.diet.findFirstOrThrow({ where: { key: "vegan" } });
|
|
await agent.patch("/profile/diet").send({ dietId: diet.id });
|
|
|
|
const res = await agent.patch("/profile/diet").send({ dietId: null });
|
|
|
|
expect(res.status).to.equal(200);
|
|
expect(res.body.dietId).to.equal(null);
|
|
});
|
|
|
|
it("rejects an unknown dietId with 404 DIET_NOT_FOUND", async () => {
|
|
const agent = request.agent(app);
|
|
await agent.post("/auth/signup").send(buildSignupPayload());
|
|
|
|
const res = await agent.patch("/profile/diet").send({ dietId: 999_999 });
|
|
|
|
expect(res.status).to.equal(404);
|
|
expect(res.body.code).to.equal(ErrorCode.DIET_NOT_FOUND);
|
|
});
|
|
});
|
|
|
|
describe("GET /profile/allergies + PATCH /profile/allergies", () => {
|
|
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
|
|
const getRes = await request(app).get("/profile/allergies");
|
|
const patchRes = await request(app).patch("/profile/allergies").send({ allergyIds: [] });
|
|
|
|
expect(getRes.status).to.equal(401);
|
|
expect(patchRes.status).to.equal(401);
|
|
});
|
|
|
|
it("starts empty, then reflects a saved selection", async () => {
|
|
const agent = request.agent(app);
|
|
await agent.post("/auth/signup").send(buildSignupPayload());
|
|
const allergies = await prisma.allergy.findMany({ include: { category: true } });
|
|
const peanuts = allergies.find((a) => a.category.key === "peanuts");
|
|
const gluten = allergies.find((a) => a.category.key === "gluten");
|
|
if (!peanuts || !gluten) throw new Error("expected seeded allergens missing");
|
|
|
|
const initial = await agent.get("/profile/allergies");
|
|
expect(initial.body).to.deep.equal([]);
|
|
|
|
const patchRes = await agent
|
|
.patch("/profile/allergies")
|
|
.send({ allergyIds: [peanuts.id, gluten.id] });
|
|
expect(patchRes.status).to.equal(200);
|
|
expect(patchRes.body.sort()).to.deep.equal([peanuts.id, gluten.id].sort());
|
|
|
|
const refetch = await agent.get("/profile/allergies");
|
|
expect(refetch.body.sort()).to.deep.equal([peanuts.id, gluten.id].sort());
|
|
});
|
|
|
|
it("replaces (not merges) the previous selection", async () => {
|
|
const agent = request.agent(app);
|
|
await agent.post("/auth/signup").send(buildSignupPayload());
|
|
const allergies = await prisma.allergy.findMany({ include: { category: true } });
|
|
const peanuts = allergies.find((a) => a.category.key === "peanuts");
|
|
const gluten = allergies.find((a) => a.category.key === "gluten");
|
|
if (!peanuts || !gluten) throw new Error("expected seeded allergens missing");
|
|
|
|
await agent.patch("/profile/allergies").send({ allergyIds: [peanuts.id] });
|
|
await agent.patch("/profile/allergies").send({ allergyIds: [gluten.id] });
|
|
|
|
const res = await agent.get("/profile/allergies");
|
|
expect(res.body).to.deep.equal([gluten.id]);
|
|
});
|
|
|
|
it("rejects an unknown allergyId with 404 ALLERGY_NOT_FOUND", async () => {
|
|
const agent = request.agent(app);
|
|
await agent.post("/auth/signup").send(buildSignupPayload());
|
|
|
|
const res = await agent.patch("/profile/allergies").send({ allergyIds: [999_999] });
|
|
|
|
expect(res.status).to.equal(404);
|
|
expect(res.body.code).to.equal(ErrorCode.ALLERGY_NOT_FOUND);
|
|
});
|
|
});
|
|
|
|
describe("GET /profile/disliked-ingredients + PATCH /profile/disliked-ingredients", () => {
|
|
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
|
|
const getRes = await request(app).get("/profile/disliked-ingredients");
|
|
const patchRes = await request(app)
|
|
.patch("/profile/disliked-ingredients")
|
|
.send({ dislikedIngredientIds: [] });
|
|
|
|
expect(getRes.status).to.equal(401);
|
|
expect(patchRes.status).to.equal(401);
|
|
});
|
|
|
|
it("starts empty, then reflects a saved selection", async () => {
|
|
const agent = request.agent(app);
|
|
await agent.post("/auth/signup").send(buildSignupPayload());
|
|
const tomate = await prisma.ingredient.findFirstOrThrow({
|
|
where: { key: "tomato" },
|
|
});
|
|
const oignon = await prisma.ingredient.findFirstOrThrow({
|
|
where: { key: "onion" },
|
|
});
|
|
|
|
const initial = await agent.get("/profile/disliked-ingredients");
|
|
expect(initial.body).to.deep.equal([]);
|
|
|
|
const patchRes = await agent
|
|
.patch("/profile/disliked-ingredients")
|
|
.send({ dislikedIngredientIds: [tomate.id, oignon.id] });
|
|
expect(patchRes.status).to.equal(200);
|
|
expect(patchRes.body.sort()).to.deep.equal([tomate.id, oignon.id].sort());
|
|
|
|
const refetch = await agent.get("/profile/disliked-ingredients");
|
|
expect(refetch.body.sort()).to.deep.equal([tomate.id, oignon.id].sort());
|
|
});
|
|
|
|
it("replaces (not merges) the previous selection", async () => {
|
|
const agent = request.agent(app);
|
|
await agent.post("/auth/signup").send(buildSignupPayload());
|
|
const tomate = await prisma.ingredient.findFirstOrThrow({
|
|
where: { key: "tomato" },
|
|
});
|
|
const oignon = await prisma.ingredient.findFirstOrThrow({
|
|
where: { key: "onion" },
|
|
});
|
|
|
|
await agent
|
|
.patch("/profile/disliked-ingredients")
|
|
.send({ dislikedIngredientIds: [tomate.id] });
|
|
await agent
|
|
.patch("/profile/disliked-ingredients")
|
|
.send({ dislikedIngredientIds: [oignon.id] });
|
|
|
|
const res = await agent.get("/profile/disliked-ingredients");
|
|
expect(res.body).to.deep.equal([oignon.id]);
|
|
});
|
|
|
|
it("rejects an unknown ingredientId with 404 INGREDIENT_NOT_FOUND", async () => {
|
|
const agent = request.agent(app);
|
|
await agent.post("/auth/signup").send(buildSignupPayload());
|
|
|
|
const res = await agent
|
|
.patch("/profile/disliked-ingredients")
|
|
.send({ dislikedIngredientIds: [999_999] });
|
|
|
|
expect(res.status).to.equal(404);
|
|
expect(res.body.code).to.equal(ErrorCode.INGREDIENT_NOT_FOUND);
|
|
});
|
|
});
|
|
});
|