Tests API: couverture Mocha + Cucumber pour le foyer et la suppression de compte (step 4/8)
- 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
This commit is contained in:
parent
7af98756dc
commit
3363cfad75
9 changed files with 516 additions and 23 deletions
|
|
@ -33,3 +33,18 @@ Feature: Account creation and login
|
|||
When I log in with email "alice@example.com" and password "wrong-password"
|
||||
Then the response status should be 401
|
||||
And the response error code should be "INVALID_CREDENTIALS"
|
||||
|
||||
Scenario: A signed-in user cannot delete their account with the wrong password
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I delete my account with password "wrong-password"
|
||||
Then the response status should be 401
|
||||
And the response error code should be "INVALID_CREDENTIALS"
|
||||
And I am authenticated as "alice@example.com"
|
||||
|
||||
Scenario: A signed-in user deletes their account
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I delete my account with password "correct-horse-battery-staple"
|
||||
Then the response status should be 204
|
||||
And I am no longer authenticated
|
||||
|
|
|
|||
|
|
@ -1,16 +1,67 @@
|
|||
Feature: Household name
|
||||
Feature: Household
|
||||
As a signed-in user
|
||||
I want to name my household
|
||||
So that it's recognizable as ours, not the auto-generated default
|
||||
I want to name my household, invite others to it, and manage its members
|
||||
So that my whole household can share the same planning
|
||||
|
||||
Scenario: A visitor without a session cannot read the household
|
||||
When I send a GET request to "/house/current"
|
||||
Then the response status should be 401
|
||||
And the response error code should be "NOT_AUTHENTICATED"
|
||||
|
||||
Scenario: A signed-in user creates a household
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I create a household named "Chez Alice"
|
||||
Then the response status should be 201
|
||||
And my household should be named "Chez Alice"
|
||||
|
||||
Scenario: A signed-in user renames their household
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I have a household named "Foyer de test"
|
||||
When I rename my household to "Chez les Martin"
|
||||
Then the response status should be 200
|
||||
And my household should be named "Chez les Martin"
|
||||
|
||||
Scenario: A second user joins a household using its invite code
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I have a household named "Chez Alice"
|
||||
And a profile already exists with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
When the second user logs in with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user joins my household using its invite code
|
||||
Then the second user's response status should be 200
|
||||
And the second user should be a member of my household
|
||||
|
||||
Scenario: A non-admin member cannot delete the household
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I have a household named "Chez Alice"
|
||||
And a profile already exists with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user logs in with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user joins my household using its invite code
|
||||
When the second user tries to delete the household
|
||||
Then the second user's response status should be 403
|
||||
And the second user's response error code should be "NOT_HOUSE_ADMIN"
|
||||
|
||||
Scenario: Adminship transfers to the remaining member when the admin leaves
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I have a household named "Chez Alice"
|
||||
And a profile already exists with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user logs in with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user joins my household using its invite code
|
||||
When I leave the household
|
||||
Then the response status should be 204
|
||||
And the second user should be the household's admin
|
||||
|
||||
Scenario: The admin removes a member
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I have a household named "Chez Alice"
|
||||
And a profile already exists with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user logs in with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user joins my household using its invite code
|
||||
When I remove the second user from my household
|
||||
Then the response status should be 200
|
||||
And the second user should have no household
|
||||
|
|
|
|||
|
|
@ -48,8 +48,20 @@ When(
|
|||
},
|
||||
);
|
||||
|
||||
When(
|
||||
"I delete my account with password {string}",
|
||||
async function (this: CustomWorld, password: string) {
|
||||
this.response = await this.agent.delete("/auth/me").send({ password });
|
||||
},
|
||||
);
|
||||
|
||||
Then("I am authenticated as {string}", async function (this: CustomWorld, email: string) {
|
||||
const res = await this.agent.get("/auth/me");
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.email, email);
|
||||
});
|
||||
|
||||
Then("I am no longer authenticated", async function (this: CustomWorld) {
|
||||
const res = await this.agent.get("/auth/me");
|
||||
assert.equal(res.status, 401);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,90 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { Then, When } from "@cucumber/cucumber";
|
||||
import { ErrorCode } from "@batch-cooking/shared";
|
||||
import { Given, Then, When } from "@cucumber/cucumber";
|
||||
import type { CustomWorld } from "../support/world.js";
|
||||
|
||||
Given("I have a household named {string}", async function (this: CustomWorld, name: string) {
|
||||
const res = await this.agent.post("/house").send({ name });
|
||||
assert.equal(res.status, 201, JSON.stringify(res.body));
|
||||
});
|
||||
|
||||
When("I create a household named {string}", async function (this: CustomWorld, name: string) {
|
||||
this.response = await this.agent.post("/house").send({ name });
|
||||
});
|
||||
|
||||
When("I rename my household to {string}", async function (this: CustomWorld, name: string) {
|
||||
this.response = await this.agent.patch("/house/current").send({ name });
|
||||
});
|
||||
|
||||
When("I leave the household", async function (this: CustomWorld) {
|
||||
this.response = await this.agent.post("/house/leave");
|
||||
});
|
||||
|
||||
When("I remove the second user from my household", async function (this: CustomWorld) {
|
||||
const secondMe = await this.secondAgent.get("/auth/me");
|
||||
this.response = await this.agent.delete(`/house/members/${secondMe.body.id}`);
|
||||
});
|
||||
|
||||
Then("my household should be named {string}", async function (this: CustomWorld, name: string) {
|
||||
const res = await this.agent.get("/house/current");
|
||||
assert.equal(res.body.name, name);
|
||||
});
|
||||
|
||||
// --- Steps involving a second, independently signed-in user ---------------
|
||||
// The first ("a profile already exists with email ...") step is reused
|
||||
// as-is for the second user too — it just inserts a row, agent-agnostic.
|
||||
|
||||
When(
|
||||
"the second user logs in with email {string} and password {string}",
|
||||
async function (this: CustomWorld, email: string, password: string) {
|
||||
this.secondResponse = await this.secondAgent.post("/auth/login").send({ email, password });
|
||||
},
|
||||
);
|
||||
|
||||
When(
|
||||
"the second user joins my household using its invite code",
|
||||
async function (this: CustomWorld) {
|
||||
const house = await this.agent.get("/house/current");
|
||||
this.secondResponse = await this.secondAgent
|
||||
.post("/house/join")
|
||||
.send({ inviteCode: house.body.inviteCode });
|
||||
},
|
||||
);
|
||||
|
||||
When("the second user tries to delete the household", async function (this: CustomWorld) {
|
||||
this.secondResponse = await this.secondAgent.delete("/house/current");
|
||||
});
|
||||
|
||||
Then(
|
||||
"the second user's response status should be {int}",
|
||||
function (this: CustomWorld, status: number) {
|
||||
assert.equal(this.secondResponse.status, status);
|
||||
},
|
||||
);
|
||||
|
||||
Then(
|
||||
"the second user's response error code should be {string}",
|
||||
function (this: CustomWorld, code: string) {
|
||||
const expected = ErrorCode[code as keyof typeof ErrorCode];
|
||||
assert.notEqual(expected, undefined, `Unknown ErrorCode member: "${code}"`);
|
||||
assert.equal(this.secondResponse.body.code, expected);
|
||||
},
|
||||
);
|
||||
|
||||
Then("the second user should be a member of my household", async function (this: CustomWorld) {
|
||||
const house = await this.agent.get("/house/current");
|
||||
const secondMe = await this.secondAgent.get("/auth/me");
|
||||
const memberIds = (house.body.members as Array<{ id: number }>).map((member) => member.id);
|
||||
assert.ok(memberIds.includes(secondMe.body.id));
|
||||
});
|
||||
|
||||
Then("the second user should be the household's admin", async function (this: CustomWorld) {
|
||||
const secondMe = await this.secondAgent.get("/auth/me");
|
||||
const house = await this.secondAgent.get("/house/current");
|
||||
assert.equal(house.body.adminId, secondMe.body.id);
|
||||
});
|
||||
|
||||
Then("the second user should have no household", async function (this: CustomWorld) {
|
||||
const secondMe = await this.secondAgent.get("/auth/me");
|
||||
assert.equal(secondMe.body.houseId, null);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ Then("the current planning response should be empty", function (this: CustomWorl
|
|||
// the API — there's no "create a planning" endpoint yet (see
|
||||
// specs/batch-cooking-architecture.md, "Calcul batch-cooking" is still
|
||||
// TODO), so this is the only way to get a household into a state where it
|
||||
// has one. Reads the household off the already-authenticated agent (via
|
||||
// `GET /auth/me`) rather than taking it as a step argument, since the
|
||||
// scenario never names it explicitly.
|
||||
// has one. A household is no longer created implicitly at signup, so this
|
||||
// step creates one via `POST /house` first — the scenario never names it
|
||||
// explicitly, its name doesn't matter here.
|
||||
Given(
|
||||
"my household has a planning covering today with recipe {string} on {string} for {string}",
|
||||
async function (this: CustomWorld, recipeName: string, weekDay: string, meal: string) {
|
||||
const me = await this.agent.get("/auth/me");
|
||||
const houseId: number = me.body.houseId;
|
||||
const houseRes = await this.agent.post("/house").send({ name: "Foyer de test" });
|
||||
const houseId: number = houseRes.body.id;
|
||||
|
||||
const recipe = await prisma.recipe.create({ data: { name: recipeName } });
|
||||
const today = new Date();
|
||||
|
|
|
|||
|
|
@ -11,11 +11,15 @@ export class CustomWorld extends World {
|
|||
app: Express;
|
||||
agent: ReturnType<typeof request.agent>;
|
||||
response!: request.Response;
|
||||
/** A second, independent session (own cookie jar) — only used by scenarios needing two distinct signed-in users, e.g. household invites/admin transfer/member removal. */
|
||||
secondAgent: ReturnType<typeof request.agent>;
|
||||
secondResponse!: request.Response;
|
||||
|
||||
constructor(options: IWorldOptions) {
|
||||
super(options);
|
||||
this.app = createApp();
|
||||
this.agent = request.agent(this.app);
|
||||
this.secondAgent = request.agent(this.app);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ describe("Auth", () => {
|
|||
});
|
||||
|
||||
describe("POST /auth/signup", () => {
|
||||
it("creates a profile and its house, and sets a session cookie", async () => {
|
||||
it("creates a profile without a household yet, and sets a session cookie", async () => {
|
||||
const payload = buildSignupPayload();
|
||||
const res = await request(app).post("/auth/signup").send(payload);
|
||||
|
||||
|
|
@ -49,7 +49,9 @@ describe("Auth", () => {
|
|||
email: payload.email,
|
||||
});
|
||||
expect(res.body).to.not.have.property("passwordHash");
|
||||
expect(res.body.houseId).to.be.a("number");
|
||||
// No household is created at signup anymore — it's an optional
|
||||
// onboarding step (create/join/skip), see house.test.ts.
|
||||
expect(res.body.houseId).to.equal(null);
|
||||
expect(res.headers["set-cookie"]?.[0]).to.include("session=");
|
||||
});
|
||||
|
||||
|
|
@ -131,4 +133,75 @@ describe("Auth", () => {
|
|||
expect(res.body.email).to.equal(payload.email);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /auth/me", () => {
|
||||
it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => {
|
||||
const res = await request(app).delete("/auth/me").send({ password: "whatever" });
|
||||
|
||||
expect(res.status).to.equal(401);
|
||||
expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
|
||||
});
|
||||
|
||||
it("rejects a wrong password with 401 INVALID_CREDENTIALS, without deleting the profile", async () => {
|
||||
const payload = buildSignupPayload();
|
||||
const agent = request.agent(app);
|
||||
const signupRes = await agent.post("/auth/signup").send(payload);
|
||||
|
||||
const res = await agent.delete("/auth/me").send({ password: "wrong-password" });
|
||||
|
||||
expect(res.status).to.equal(401);
|
||||
expect(res.body.code).to.equal(ErrorCode.INVALID_CREDENTIALS);
|
||||
expect(
|
||||
await prisma.userProfile.findUnique({ where: { id: signupRes.body.id } }),
|
||||
).to.not.equal(null);
|
||||
});
|
||||
|
||||
it("deletes the profile and clears the session cookie", async () => {
|
||||
const payload = buildSignupPayload();
|
||||
const agent = request.agent(app);
|
||||
const signupRes = await agent.post("/auth/signup").send(payload);
|
||||
|
||||
const res = await agent.delete("/auth/me").send({ password: payload.password });
|
||||
|
||||
expect(res.status).to.equal(204);
|
||||
expect(await prisma.userProfile.findUnique({ where: { id: signupRes.body.id } })).to.equal(
|
||||
null,
|
||||
);
|
||||
|
||||
const meRes = await agent.get("/auth/me");
|
||||
expect(meRes.status).to.equal(401);
|
||||
});
|
||||
|
||||
it("deletes the household along with the account when it's the sole member", async () => {
|
||||
const payload = buildSignupPayload();
|
||||
const agent = request.agent(app);
|
||||
await agent.post("/auth/signup").send(payload);
|
||||
const houseRes = await agent.post("/house").send({ name: "Chez moi" });
|
||||
|
||||
const res = await agent.delete("/auth/me").send({ password: payload.password });
|
||||
|
||||
expect(res.status).to.equal(204);
|
||||
expect(await prisma.house.findUnique({ where: { id: houseRes.body.id } })).to.equal(null);
|
||||
});
|
||||
|
||||
it("transfers adminship to another member before deleting an admin's account", async () => {
|
||||
const adminPayload = buildSignupPayload();
|
||||
const adminAgent = request.agent(app);
|
||||
const houseRes = await adminAgent
|
||||
.post("/auth/signup")
|
||||
.send(adminPayload)
|
||||
.then(() => adminAgent.post("/house").send({ name: "Chez nous" }));
|
||||
|
||||
const memberPayload = buildSignupPayload();
|
||||
const memberAgent = request.agent(app);
|
||||
const memberSignupRes = await memberAgent.post("/auth/signup").send(memberPayload);
|
||||
await memberAgent.post("/house/join").send({ inviteCode: houseRes.body.inviteCode });
|
||||
|
||||
const res = await adminAgent.delete("/auth/me").send({ password: adminPayload.password });
|
||||
|
||||
expect(res.status).to.equal(204);
|
||||
const house = await prisma.house.findUnique({ where: { id: houseRes.body.id } });
|
||||
expect(house?.adminId).to.equal(memberSignupRes.body.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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";
|
||||
|
|
@ -17,6 +18,13 @@ function buildSignupPayload(): SignupInput {
|
|||
};
|
||||
}
|
||||
|
||||
/** 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();
|
||||
|
||||
|
|
@ -36,14 +44,28 @@ describe("Household", () => {
|
|||
expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED);
|
||||
});
|
||||
|
||||
it("returns the household created at signup", async () => {
|
||||
const agent = request.agent(app);
|
||||
const signupRes = await agent.post("/auth/signup").send(buildSignupPayload());
|
||||
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.deep.equal({ id: signupRes.body.houseId, name: res.body.name });
|
||||
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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -55,9 +77,18 @@ describe("Household", () => {
|
|||
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 = request.agent(app);
|
||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||
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" });
|
||||
|
||||
|
|
@ -69,8 +100,8 @@ describe("Household", () => {
|
|||
});
|
||||
|
||||
it("rejects an empty name with 400 VALIDATION_ERROR", async () => {
|
||||
const agent = request.agent(app);
|
||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||
const { agent } = await signupAgent(app);
|
||||
await agent.post("/house").send({ name: "Chez Alice" });
|
||||
|
||||
const res = await agent.patch("/house/current").send({ name: "" });
|
||||
|
||||
|
|
@ -78,4 +109,231 @@ describe("Household", () => {
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,8 +49,9 @@ describe("Planning", () => {
|
|||
|
||||
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;
|
||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||
const houseRes = await agent.post("/house").send({ name: "Chez moi" });
|
||||
const houseId: number = houseRes.body.id;
|
||||
|
||||
const recipe = await prisma.recipe.create({ data: { name: "Ratatouille" } });
|
||||
const today = new Date();
|
||||
|
|
@ -80,8 +81,9 @@ describe("Planning", () => {
|
|||
|
||||
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;
|
||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||
const houseRes = await agent.post("/house").send({ name: "Chez moi" });
|
||||
const houseId: number = houseRes.body.id;
|
||||
|
||||
// A planning entirely in the past — shouldn't be picked up as "current".
|
||||
await prisma.planning.create({
|
||||
|
|
|
|||
Loading…
Reference in a new issue