import { ErrorCode } 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 { hashAdminPassword } from "../src/modules/admin/admin-auth.service.js"; import { resetDatabase } from "../test-support/reset-db.js"; async function seedAdmin(): Promise<{ email: string; password: string }> { const email = faker.internet.email().toLowerCase(); const password = faker.internet.password({ length: 16 }); await prisma.adminUser.create({ data: { email, name: faker.person.fullName(), passwordHash: await hashAdminPassword(password) }, }); return { email, password }; } /** A fresh, fake (never real-looking) create-user payload — same rationale as `auth.test.ts`'s `buildSignupPayload`. */ function buildCreateUserPayload() { const firstName = faker.person.firstName(); const lastName = faker.person.lastName(); return { firstName, lastName, email: faker.internet.email({ firstName, lastName }).toLowerCase(), }; } /** * `/admin/api/users/*` — the admin surface for provisioning end-user * accounts by hand (`/admin/utilisateurs` page). Every route behind * `requireAdmin`. Creation generates a one-time temporary password and * returns it in the response; the account starts with no household, exactly * like a self-service signup. */ describe("Admin users — account provisioning", () => { const app = createApp(); beforeEach(async () => { await resetDatabase(); }); after(async () => { await prisma.$disconnect(); }); async function adminAgent() { const { email, password } = await seedAdmin(); const agent = request.agent(app); await agent.post("/admin/api/auth/login").send({ email, password }); return agent; } it("rejects every route without an admin session", async () => { expect((await request(app).get("/admin/api/users")).status).to.equal(401); expect( (await request(app).post("/admin/api/users").send(buildCreateUserPayload())).status, ).to.equal(401); }); it("creates a profile with no household and returns a one-time temporary password", async () => { const agent = await adminAgent(); const payload = buildCreateUserPayload(); const res = await agent.post("/admin/api/users").send(payload); expect(res.status).to.equal(201); expect(res.body.user).to.include({ firstName: payload.firstName, lastName: payload.lastName, email: payload.email, householdName: null, }); expect(res.body.user).to.not.have.property("passwordHash"); expect(res.body.user).to.not.have.property("tokenVersion"); expect(res.body.temporaryPassword).to.be.a("string").with.length.greaterThan(8); const stored = await prisma.userProfile.findUnique({ where: { email: payload.email } }); expect(stored?.houseId).to.equal(null); expect(stored?.dietId).to.equal(null); }); it("issues a password the new account can actually log in with", async () => { const agent = await adminAgent(); const payload = buildCreateUserPayload(); const created = await agent.post("/admin/api/users").send(payload); const login = await request(app) .post("/auth/login") .send({ email: payload.email, password: created.body.temporaryPassword }); expect(login.status).to.equal(200); expect(login.body.email).to.equal(payload.email); }); it("rejects a duplicate email with 409 EMAIL_ALREADY_IN_USE", async () => { const agent = await adminAgent(); const payload = buildCreateUserPayload(); await agent.post("/admin/api/users").send(payload); const res = await agent .post("/admin/api/users") .send({ ...buildCreateUserPayload(), email: payload.email }); expect(res.status).to.equal(409); expect(res.body.code).to.equal(ErrorCode.EMAIL_ALREADY_IN_USE); }); it("rejects an invalid body with 400 VALIDATION_ERROR", async () => { const agent = await adminAgent(); const badEmail = await agent .post("/admin/api/users") .send({ ...buildCreateUserPayload(), email: "not-an-email" }); expect(badEmail.status).to.equal(400); expect(badEmail.body.code).to.equal(ErrorCode.VALIDATION_ERROR); const noFirstName = await agent .post("/admin/api/users") .send({ ...buildCreateUserPayload(), firstName: "" }); expect(noFirstName.status).to.equal(400); }); it("lists accounts newest-first with their household name resolved, never the password", async () => { const agent = await adminAgent(); const first = buildCreateUserPayload(); const second = buildCreateUserPayload(); const firstRes = await agent.post("/admin/api/users").send(first); await agent.post("/admin/api/users").send(second); // Put the first account in a household so `householdName` has something to // resolve — `House.adminId` is required, so the account is its own admin. const house = await prisma.house.create({ data: { name: "Le Foyer Test", adminId: firstRes.body.user.id, inviteCode: faker.string.alphanumeric(8).toUpperCase(), }, }); await prisma.userProfile.update({ where: { email: first.email }, data: { houseId: house.id }, }); const res = await agent.get("/admin/api/users"); expect(res.status).to.equal(200); expect(res.body).to.have.length(2); // Newest first: `second` was created after `first`. expect(res.body[0].email).to.equal(second.email); expect(res.body[1].email).to.equal(first.email); expect(res.body[1].householdName).to.equal("Le Foyer Test"); expect(res.body[0].householdName).to.equal(null); for (const row of res.body) { expect(row).to.not.have.property("temporaryPassword"); expect(row).to.not.have.property("passwordHash"); } }); });