- house.test.ts réécrit (le foyer n'est plus auto-créé) + POST /house, POST /house/join, POST /house/leave, DELETE /house/current, DELETE /house/members/:id - auth.test.ts: signup renvoie houseId=null, DELETE /auth/me (mauvais mot de passe, suppression, transfert d'admin) - planning.test.ts/steps.ts: création explicite du foyer (POST /house) - household.feature: scénarios créer/rejoindre/quitter/supprimer/ retirer un membre, via un second agent (CustomWorld.secondAgent) - auth.feature: scénarios de suppression de compte
339 lines
13 KiB
TypeScript
339 lines
13 KiB
TypeScript
import { ErrorCode, type SignupInput } from "@batch-cooking/shared";
|
|
import { faker } from "@faker-js/faker";
|
|
import { expect } from "chai";
|
|
import type { Express } from "express";
|
|
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 }),
|
|
};
|
|
}
|
|
|
|
/** Signs up a fresh profile on a brand new agent (its own cookie jar) and returns both. */
|
|
async function signupAgent(app: Express) {
|
|
const agent = request.agent(app);
|
|
const res = await agent.post("/auth/signup").send(buildSignupPayload());
|
|
return { agent, profile: res.body };
|
|
}
|
|
|
|
describe("Household", () => {
|
|
const app = createApp();
|
|
|
|
beforeEach(async () => {
|
|
await resetDatabase();
|
|
});
|
|
|
|
after(async () => {
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
describe("GET /house/current", () => {
|
|
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
|
|
const res = await request(app).get("/house/current");
|
|
|
|
expect(res.status).to.equal(401);
|
|
expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
|
|
});
|
|
|
|
it("returns null when the profile has no household yet", async () => {
|
|
const { agent } = await signupAgent(app);
|
|
|
|
const res = await agent.get("/house/current");
|
|
|
|
expect(res.status).to.equal(200);
|
|
expect(res.body).to.equal(null);
|
|
});
|
|
|
|
it("returns the household with its admin and member list, once created", async () => {
|
|
const { agent, profile } = await signupAgent(app);
|
|
await agent.post("/house").send({ name: "Chez Alice" });
|
|
|
|
const res = await agent.get("/house/current");
|
|
|
|
expect(res.status).to.equal(200);
|
|
expect(res.body.name).to.equal("Chez Alice");
|
|
expect(res.body.adminId).to.equal(profile.id);
|
|
expect(res.body.inviteCode).to.match(/^[A-Z2-9]{8}$/);
|
|
expect(res.body.members).to.deep.equal([
|
|
{ id: profile.id, firstName: profile.firstName, lastName: profile.lastName },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("PATCH /house/current", () => {
|
|
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
|
|
const res = await request(app).patch("/house/current").send({ name: "Chez nous" });
|
|
|
|
expect(res.status).to.equal(401);
|
|
expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
|
|
});
|
|
|
|
it("rejects renaming when the profile has no household yet with 404 HOUSE_NOT_FOUND", async () => {
|
|
const { agent } = await signupAgent(app);
|
|
|
|
const res = await agent.patch("/house/current").send({ name: "Chez nous" });
|
|
|
|
expect(res.status).to.equal(404);
|
|
expect(res.body.code).to.equal(ErrorCode.HOUSE_NOT_FOUND);
|
|
});
|
|
|
|
it("renames the household", async () => {
|
|
const { agent } = await signupAgent(app);
|
|
await agent.post("/house").send({ name: "Chez Alice" });
|
|
|
|
const res = await agent.patch("/house/current").send({ name: "Chez les Dupont" });
|
|
|
|
expect(res.status).to.equal(200);
|
|
expect(res.body.name).to.equal("Chez les Dupont");
|
|
|
|
const refetch = await agent.get("/house/current");
|
|
expect(refetch.body.name).to.equal("Chez les Dupont");
|
|
});
|
|
|
|
it("rejects an empty name with 400 VALIDATION_ERROR", async () => {
|
|
const { agent } = await signupAgent(app);
|
|
await agent.post("/house").send({ name: "Chez Alice" });
|
|
|
|
const res = await agent.patch("/house/current").send({ name: "" });
|
|
|
|
expect(res.status).to.equal(400);
|
|
expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR);
|
|
});
|
|
});
|
|
|
|
describe("POST /house", () => {
|
|
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
|
|
const res = await request(app).post("/house").send({ name: "Chez nous" });
|
|
|
|
expect(res.status).to.equal(401);
|
|
expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
|
|
});
|
|
|
|
it("creates a household with the caller as its admin", async () => {
|
|
const { agent, profile } = await signupAgent(app);
|
|
|
|
const res = await agent.post("/house").send({ name: "Chez Alice" });
|
|
|
|
expect(res.status).to.equal(201);
|
|
expect(res.body.name).to.equal("Chez Alice");
|
|
expect(res.body.adminId).to.equal(profile.id);
|
|
|
|
const me = await agent.get("/auth/me");
|
|
expect(me.body.houseId).to.equal(res.body.id);
|
|
});
|
|
|
|
it("rejects an empty name with 400 VALIDATION_ERROR", async () => {
|
|
const { agent } = await signupAgent(app);
|
|
|
|
const res = await agent.post("/house").send({ name: "" });
|
|
|
|
expect(res.status).to.equal(400);
|
|
expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR);
|
|
});
|
|
|
|
it("rejects creating a second household with 409 ALREADY_HAS_HOUSE", async () => {
|
|
const { agent } = await signupAgent(app);
|
|
await agent.post("/house").send({ name: "Chez Alice" });
|
|
|
|
const res = await agent.post("/house").send({ name: "Chez Alice bis" });
|
|
|
|
expect(res.status).to.equal(409);
|
|
expect(res.body.code).to.equal(ErrorCode.ALREADY_HAS_HOUSE);
|
|
});
|
|
});
|
|
|
|
describe("POST /house/join", () => {
|
|
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
|
|
const res = await request(app).post("/house/join").send({ inviteCode: "ABCDEFGH" });
|
|
|
|
expect(res.status).to.equal(401);
|
|
expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
|
|
});
|
|
|
|
it("joins an existing household by invite code", async () => {
|
|
const { agent: adminAgent } = await signupAgent(app);
|
|
const created = await adminAgent.post("/house").send({ name: "Chez Alice" });
|
|
const { agent: joinerAgent, profile: joiner } = await signupAgent(app);
|
|
|
|
const res = await joinerAgent
|
|
.post("/house/join")
|
|
.send({ inviteCode: created.body.inviteCode });
|
|
|
|
expect(res.status).to.equal(200);
|
|
expect(res.body.id).to.equal(created.body.id);
|
|
expect(res.body.members.map((m: { id: number }) => m.id)).to.include(joiner.id);
|
|
});
|
|
|
|
it("rejects an unknown invite code with 404 INVITE_CODE_NOT_FOUND", async () => {
|
|
const { agent } = await signupAgent(app);
|
|
|
|
const res = await agent.post("/house/join").send({ inviteCode: "ZZZZZZZZ" });
|
|
|
|
expect(res.status).to.equal(404);
|
|
expect(res.body.code).to.equal(ErrorCode.INVITE_CODE_NOT_FOUND);
|
|
});
|
|
|
|
it("rejects joining when the profile already belongs to a household with 409 ALREADY_HAS_HOUSE", async () => {
|
|
const { agent: adminAgent } = await signupAgent(app);
|
|
const created = await adminAgent.post("/house").send({ name: "Chez Alice" });
|
|
const { agent } = await signupAgent(app);
|
|
await agent.post("/house").send({ name: "Chez Bob" });
|
|
|
|
const res = await agent.post("/house/join").send({ inviteCode: created.body.inviteCode });
|
|
|
|
expect(res.status).to.equal(409);
|
|
expect(res.body.code).to.equal(ErrorCode.ALREADY_HAS_HOUSE);
|
|
});
|
|
});
|
|
|
|
describe("POST /house/leave", () => {
|
|
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
|
|
const res = await request(app).post("/house/leave");
|
|
|
|
expect(res.status).to.equal(401);
|
|
expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
|
|
});
|
|
|
|
it("rejects leaving when the profile has no household with 404 HOUSE_NOT_FOUND", async () => {
|
|
const { agent } = await signupAgent(app);
|
|
|
|
const res = await agent.post("/house/leave");
|
|
|
|
expect(res.status).to.equal(404);
|
|
expect(res.body.code).to.equal(ErrorCode.HOUSE_NOT_FOUND);
|
|
});
|
|
|
|
it("deletes the household when its sole member leaves", async () => {
|
|
const { agent } = await signupAgent(app);
|
|
const created = await agent.post("/house").send({ name: "Chez Alice" });
|
|
|
|
const res = await agent.post("/house/leave");
|
|
|
|
expect(res.status).to.equal(204);
|
|
expect(await prisma.house.findUnique({ where: { id: created.body.id } })).to.equal(null);
|
|
});
|
|
|
|
it("transfers adminship to the remaining member when the admin leaves", async () => {
|
|
const { agent: adminAgent } = await signupAgent(app);
|
|
const created = await adminAgent.post("/house").send({ name: "Chez Alice" });
|
|
const { agent: memberAgent, profile: member } = await signupAgent(app);
|
|
await memberAgent.post("/house/join").send({ inviteCode: created.body.inviteCode });
|
|
|
|
const res = await adminAgent.post("/house/leave");
|
|
|
|
expect(res.status).to.equal(204);
|
|
const house = await prisma.house.findUnique({ where: { id: created.body.id } });
|
|
expect(house?.adminId).to.equal(member.id);
|
|
});
|
|
});
|
|
|
|
describe("DELETE /house/current", () => {
|
|
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
|
|
const res = await request(app).delete("/house/current");
|
|
|
|
expect(res.status).to.equal(401);
|
|
expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
|
|
});
|
|
|
|
it("rejects deleting when the profile has no household with 404 HOUSE_NOT_FOUND", async () => {
|
|
const { agent } = await signupAgent(app);
|
|
|
|
const res = await agent.delete("/house/current");
|
|
|
|
expect(res.status).to.equal(404);
|
|
expect(res.body.code).to.equal(ErrorCode.HOUSE_NOT_FOUND);
|
|
});
|
|
|
|
it("rejects a non-admin member with 403 NOT_HOUSE_ADMIN", async () => {
|
|
const { agent: adminAgent } = await signupAgent(app);
|
|
const created = await adminAgent.post("/house").send({ name: "Chez Alice" });
|
|
const { agent: memberAgent } = await signupAgent(app);
|
|
await memberAgent.post("/house/join").send({ inviteCode: created.body.inviteCode });
|
|
|
|
const res = await memberAgent.delete("/house/current");
|
|
|
|
expect(res.status).to.equal(403);
|
|
expect(res.body.code).to.equal(ErrorCode.NOT_HOUSE_ADMIN);
|
|
});
|
|
|
|
it("deletes the household for every member, cascading its plannings", async () => {
|
|
const { agent: adminAgent } = await signupAgent(app);
|
|
const created = await adminAgent.post("/house").send({ name: "Chez Alice" });
|
|
const { agent: memberAgent, profile: member } = await signupAgent(app);
|
|
await memberAgent.post("/house/join").send({ inviteCode: created.body.inviteCode });
|
|
const planning = await prisma.planning.create({
|
|
data: {
|
|
houseId: created.body.id,
|
|
startDate: new Date(Date.UTC(2000, 0, 1)),
|
|
finishDate: new Date(Date.UTC(2000, 0, 7)),
|
|
},
|
|
});
|
|
|
|
const res = await adminAgent.delete("/house/current");
|
|
|
|
expect(res.status).to.equal(204);
|
|
expect(await prisma.house.findUnique({ where: { id: created.body.id } })).to.equal(null);
|
|
expect(await prisma.planning.findUnique({ where: { id: planning.id } })).to.equal(null);
|
|
const memberProfile = await prisma.userProfile.findUniqueOrThrow({
|
|
where: { id: member.id },
|
|
});
|
|
expect(memberProfile.houseId).to.equal(null);
|
|
});
|
|
});
|
|
|
|
describe("DELETE /house/members/:memberId", () => {
|
|
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
|
|
const res = await request(app).delete("/house/members/1");
|
|
|
|
expect(res.status).to.equal(401);
|
|
expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
|
|
});
|
|
|
|
it("rejects a non-admin member with 403 NOT_HOUSE_ADMIN", async () => {
|
|
const { agent: adminAgent, profile: admin } = await signupAgent(app);
|
|
const created = await adminAgent.post("/house").send({ name: "Chez Alice" });
|
|
const { agent: memberAgent } = await signupAgent(app);
|
|
await memberAgent.post("/house/join").send({ inviteCode: created.body.inviteCode });
|
|
|
|
const res = await memberAgent.delete(`/house/members/${admin.id}`);
|
|
|
|
expect(res.status).to.equal(403);
|
|
expect(res.body.code).to.equal(ErrorCode.NOT_HOUSE_ADMIN);
|
|
});
|
|
|
|
it("rejects the admin trying to remove themselves with 400 VALIDATION_ERROR", async () => {
|
|
const { agent: adminAgent, profile: admin } = await signupAgent(app);
|
|
await adminAgent.post("/house").send({ name: "Chez Alice" });
|
|
|
|
const res = await adminAgent.delete(`/house/members/${admin.id}`);
|
|
|
|
expect(res.status).to.equal(400);
|
|
expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR);
|
|
});
|
|
|
|
it("removes the targeted member from the household", async () => {
|
|
const { agent: adminAgent } = await signupAgent(app);
|
|
const created = await adminAgent.post("/house").send({ name: "Chez Alice" });
|
|
const { agent: memberAgent, profile: member } = await signupAgent(app);
|
|
await memberAgent.post("/house/join").send({ inviteCode: created.body.inviteCode });
|
|
|
|
const res = await adminAgent.delete(`/house/members/${member.id}`);
|
|
|
|
expect(res.status).to.equal(200);
|
|
expect(res.body.members.map((m: { id: number }) => m.id)).to.not.include(member.id);
|
|
const memberProfile = await prisma.userProfile.findUniqueOrThrow({
|
|
where: { id: member.id },
|
|
});
|
|
expect(memberProfile.houseId).to.equal(null);
|
|
});
|
|
});
|
|
});
|