import { expect } from "chai"; import request from "supertest"; import { createApp } from "../src/app.js"; import { prisma } from "../src/db/prisma.js"; import { syncRecipeSources } from "../src/db/recipe-source-sync.js"; import { seedReferenceData } from "../src/db/reference-seed-data.js"; import type { RecipeSourceAdapter } from "../src/lib/recipe-sources/recipe-source-adapter.js"; import { clearRecipeSources, registerRecipeSource, } from "../src/lib/recipe-sources/recipe-source-registry.js"; import { resetDatabase } from "../test-support/reset-db.js"; /** A minimal `RecipeSourceAdapter` — only `key`/`name`/`official`/`iconUrl` matter for `syncRecipeSources` fixtures here. */ function buildFakeAdapter( key: string, name: string, official: boolean, iconUrl: string | null = null, ): RecipeSourceAdapter { return { key, name, official, iconUrl, locale: "fr", async list() { return { items: [], nextCursor: null }; }, async fetchDetail() { throw new Error("not implemented"); }, parse() { throw new Error("not implemented"); }, }; } describe("Reference data", () => { const app = createApp(); beforeEach(async () => { await resetDatabase(); }); after(async () => { await prisma.$disconnect(); }); describe("GET /reference/diets", () => { it("returns the seeded regimes, no session required", async () => { const res = await request(app).get("/reference/diets"); expect(res.status).to.equal(200); expect(res.body).to.have.length(5); expect(res.body.map((d: { key: string }) => d.key)).to.include("vegetarian"); expect(res.body[0]).to.have.keys(["id", "key"]); }); }); describe("GET /reference/allergies", () => { it("returns the seeded allergens with their key resolved, no session required", async () => { const res = await request(app).get("/reference/allergies"); expect(res.status).to.equal(200); expect(res.body).to.have.length(14); expect(res.body.map((a: { key: string }) => a.key)).to.include("peanuts"); expect(res.body[0]).to.have.keys(["id", "key", "kind"]); }); it("classifies Gluten and Sulfites as intolerances, the rest as allergies", async () => { const res = await request(app).get("/reference/allergies"); const byKey = (key: string) => res.body.find((a: { key: string }) => a.key === key); expect(byKey("gluten").kind).to.equal("INTOLERANCE"); expect(byKey("sulfites").kind).to.equal("INTOLERANCE"); expect(byKey("peanuts").kind).to.equal("ALLERGY"); expect(res.body.filter((a: { kind: string }) => a.kind === "INTOLERANCE")).to.have.length(2); }); }); describe("GET /reference/ingredients", () => { it("returns the seeded ingredients, no session required", async () => { const res = await request(app).get("/reference/ingredients"); expect(res.status).to.equal(200); expect(res.body.length).to.be.greaterThan(0); expect(res.body.map((i: { key: string }) => i.key)).to.include("tomato"); expect(res.body[0]).to.have.keys([ "id", "key", "icon", "category", "subcategory", "reproducible", "allergens", "diets", ]); }); it("resolves each ingredient's linked allergens, empty for one with none", async () => { const res = await request(app).get("/reference/ingredients"); const byKey = (key: string) => res.body.find((i: { key: string }) => i.key === key); expect(byKey("egg").allergens.map((a: { key: string }) => a.key)).to.include("eggs"); expect(byKey("tomato").allergens).to.deep.equal([]); }); }); describe("GET /reference/units", () => { it("returns the seeded units, no session required", async () => { const res = await request(app).get("/reference/units"); expect(res.status).to.equal(200); expect(res.body).to.have.length(17); expect(res.body.map((u: { key: string }) => u.key)).to.include("gram"); expect(res.body[0]).to.have.keys(["id", "key", "type", "toBaseFactor"]); }); it("resolves MASS/VOLUME toBaseFactor against their type's base unit, COUNT units all at 1", async () => { const res = await request(app).get("/reference/units"); const byKey = (key: string) => res.body.find((u: { key: string }) => u.key === key); expect(byKey("gram")).to.include({ type: "MASS", toBaseFactor: 1 }); expect(byKey("kilogram")).to.include({ type: "MASS", toBaseFactor: 1000 }); expect(byKey("liter")).to.include({ type: "VOLUME", toBaseFactor: 1000 }); expect(byKey("piece")).to.include({ type: "COUNT", toBaseFactor: 1 }); expect(byKey("pinch")).to.include({ type: "COUNT", toBaseFactor: 1 }); expect(byKey("cup")).to.include({ type: "VOLUME", toBaseFactor: 236.5882 }); expect(byKey("ounce")).to.include({ type: "MASS", toBaseFactor: 28.3495 }); expect(byKey("pound")).to.include({ type: "MASS", toBaseFactor: 453.5924 }); }); }); describe("GET /reference/tech-steps", () => { it("returns the seeded techniques, no session required", async () => { const res = await request(app).get("/reference/tech-steps"); expect(res.status).to.equal(200); expect(res.body).to.have.length(26); expect(res.body.map((t: { key: string }) => t.key)).to.include("simmer"); expect(res.body[0]).to.have.keys(["id", "key"]); }); it("orders techniques alphabetically by key", async () => { const res = await request(app).get("/reference/tech-steps"); const keys = res.body.map((t: { key: string }) => t.key); expect(keys).to.deep.equal([...keys].sort()); }); it("reseeding is idempotent — no duplicate techniques", async () => { // resetDatabase already seeded once in beforeEach; seed a second time // on top of that without truncating, the way a redeploy would. await seedReferenceData(prisma); const res = await request(app).get("/reference/tech-steps"); expect(res.body).to.have.length(26); }); }); describe("GET /reference/sources", () => { afterEach(() => { clearRecipeSources(); }); it("is empty until a concrete adapter is registered", async () => { const res = await request(app).get("/reference/sources"); expect(res.status).to.equal(200); expect(res.body).to.deep.equal([]); }); it("returns every synced adapter, official flag and icon included, no session required", async () => { registerRecipeSource(buildFakeAdapter("fakeSource", "Fake Source", false)); registerRecipeSource( buildFakeAdapter( "officialSource", "Official Source", true, "https://example.test/icon.svg", ), ); await syncRecipeSources(prisma); const res = await request(app).get("/reference/sources"); expect(res.status).to.equal(200); expect(res.body).to.have.length(2); expect(res.body[0]).to.have.keys(["id", "key", "name", "official", "iconUrl"]); const byKey = (key: string) => res.body.find((s: { key: string }) => s.key === key); expect(byKey("fakeSource").official).to.equal(false); expect(byKey("fakeSource").iconUrl).to.equal(null); expect(byKey("officialSource").official).to.equal(true); expect(byKey("officialSource").iconUrl).to.equal("https://example.test/icon.svg"); }); it("orders sources alphabetically by name", async () => { registerRecipeSource(buildFakeAdapter("bSource", "Bravo", false)); registerRecipeSource(buildFakeAdapter("aSource", "Alpha", false)); await syncRecipeSources(prisma); const res = await request(app).get("/reference/sources"); expect(res.body.map((s: { name: string }) => s.name)).to.deep.equal(["Alpha", "Bravo"]); }); }); });