batchCooking/apps/api/test/planning.test.ts
Nicolas dfeb6fd1ac API: GET /planning/current (home planning + sidebar, step 1/5)
- packages/shared: PlanningView/PlanningItemView, exported.
- apps/api: planning module (service + route), mounted at /planning.
  GET /planning/current returns the authenticated user's household's
  planning covering today, or null (no error) when there isn't one yet —
  the expected state until planning creation exists.
- Tests: Mocha (apps/api/test/planning.test.ts) + Cucumber
  (features/planning.feature), same conventions as auth.
- packages/express-tools: fixed AsyncRequestHandler/wrapAsyncHandler's
  Locals generic constraint (Record<string, unknown> -> Record<string,
  any>, matching Express's own Response<ResBody, LocalsObj>) — the first
  endpoint combining requireAuth/AuthLocals with an async handler exposed
  that the stricter constraint rejected plain interfaces Response itself
  accepts fine.
- Docs: README.md ("Planning" section) + specs/backend-architecture.md.

First commit of the home-page-after-login feature (see plan discussed in
chat) — frontend layout/routing/HomePage follow in subsequent commits on
this same branch/PR.
2026-08-16 20:46:44 +02:00

101 lines
3.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";
/** 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 }),
};
}
describe("Planning", () => {
const app = createApp();
beforeEach(async () => {
await resetDatabase();
});
after(async () => {
await prisma.$disconnect();
});
describe("GET /planning/current", () => {
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
const res = await request(app).get("/planning/current");
expect(res.status).to.equal(401);
expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
});
it("returns null when the household has no planning covering today", async () => {
const agent = request.agent(app);
await agent.post("/auth/signup").send(buildSignupPayload());
const res = await agent.get("/planning/current");
expect(res.status).to.equal(200);
expect(res.body).to.equal(null);
});
it("returns the household's planning covering today, with recipes resolved", async () => {
const agent = request.agent(app);
const signupRes = await agent.post("/auth/signup").send(buildSignupPayload());
const houseId: number = signupRes.body.houseId;
const recipe = await prisma.recipe.create({ data: { name: "Ratatouille" } });
const today = new Date();
const planning = await prisma.planning.create({
data: {
houseId,
startDate: new Date(
Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() - 2),
),
finishDate: new Date(
Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() + 2),
),
},
});
await prisma.planningItem.create({
data: { planningId: planning.id, weekDay: "monday", meal: "dinner", recipeId: recipe.id },
});
const res = await agent.get("/planning/current");
expect(res.status).to.equal(200);
expect(res.body.id).to.equal(planning.id);
expect(res.body.items).to.have.length(1);
expect(res.body.items[0]).to.include({ weekDay: "monday", meal: "dinner" });
expect(res.body.items[0].recipe).to.include({ id: recipe.id, name: "Ratatouille" });
});
it("returns null when the household's planning does not cover today", async () => {
const agent = request.agent(app);
const signupRes = await agent.post("/auth/signup").send(buildSignupPayload());
const houseId: number = signupRes.body.houseId;
// A planning entirely in the past — shouldn't be picked up as "current".
await prisma.planning.create({
data: {
houseId,
startDate: new Date(Date.UTC(2000, 0, 1)),
finishDate: new Date(Date.UTC(2000, 0, 7)),
},
});
const res = await agent.get("/planning/current");
expect(res.status).to.equal(200);
expect(res.body).to.equal(null);
});
});
});