batchCooking/apps/api/test/profile.test.ts
Nicolas acab18ac4a feat(recipes): catalogue v2 - visibilité, favoris, régimes et catalogue d'ingrédients exhaustif
Recipe catalog v2:
- Recipe gagne visibility (PERSONAL/HOUSE/PUBLIC), authorId, authorHouseId
- Favoris par utilisateur (RecipeFavorite), régimes associés (RecipeDiet)
- Aliments "pas aimés" par utilisateur (UserProfileDislikedIngredient),
  distinct des allergies médicales
- API: GET /recipes?tab=favoris|perso|foyer|publique avec contrôle d'accès
  complet, POST/DELETE /recipes/:id/favorite, édition/suppression réservées
  à l'auteur (403 NOT_RECIPE_AUTHOR), GET/PATCH /profile/disliked-ingredients
- Frontend: vue maître-détail (onglets + tableau + panneau détail),
  formulaire enrichi (visibilité, régimes), section préférences pour les
  aliments pas aimés

Catalogue d'ingrédients de référence:
- Extension du seed de 39 à ~430 ingrédients (viandes, poissons/fruits de
  mer, légumes, fruits, féculents, condiments/sauces, épices/herbes, pains
  à sandwich, cuisines italienne/asiatique/mexicaine/maghrébine, liquides
  et boissons de cuisine, bouillons/fonds)
- Chaque ingrédient lié à ses allergènes UE (IngredientAllergy) — les 14
  allergènes réglementaires restent tous couverts
- Seeding optimisé en requêtes groupées (createMany/diff ciblé) plutôt
  qu'un upsert par ligne, pour garder resetDatabase() rapide en test

Tests: 102 tests Mocha + 32 scénarios BDD, tous verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 09:29:11 +02:00

188 lines
7.5 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: { name: "Végétarien" } });
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: { name: "Végan" } });
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.name === "Arachides");
const gluten = allergies.find((a) => a.category.name === "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.name === "Arachides");
const gluten = allergies.find((a) => a.category.name === "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: { name: "Tomate" } });
const oignon = await prisma.ingredient.findFirstOrThrow({ where: { name: "Oignon" } });
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: { name: "Tomate" } });
const oignon = await prisma.ingredient.findFirstOrThrow({ where: { name: "Oignon" } });
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);
});
});
});