import { describe, expect, it, vi, beforeEach } from "vitest"; import { appRouter } from "./routers"; import { COOKIE_NAME } from "../shared/const"; import type { TrpcContext } from "./_core/context"; import type { User } from "../drizzle/schema"; // ─── Helpers ────────────────────────────────────────────────────────────────── function makeUser(overrides: Partial = {}): User { return { id: 1, openId: "test-open-id", name: "Test User", email: "test@example.com", loginMethod: "local", role: "user", sonumRole: "referent", cguAccepted: true, cguAcceptedAt: new Date(), createdAt: new Date(), updatedAt: new Date(), lastSignedIn: new Date(), ...overrides, }; } function makeCtx(user: User | null = null): TrpcContext { const cookies: Record = {}; return { user, req: { protocol: "https", headers: {}, } as TrpcContext["req"], res: { cookie: (name: string, value: string, _opts: unknown) => { cookies[name] = value; }, clearCookie: (_name: string, _opts: unknown) => {}, } as unknown as TrpcContext["res"], }; } // ─── Tests : auth.me ────────────────────────────────────────────────────────── describe("auth.me", () => { it("retourne null quand non authentifié", async () => { const caller = appRouter.createCaller(makeCtx(null)); const result = await caller.auth.me(); expect(result).toBeNull(); }); it("retourne l'utilisateur quand authentifié", async () => { const user = makeUser({ name: "Alice" }); const caller = appRouter.createCaller(makeCtx(user)); const result = await caller.auth.me(); expect(result?.name).toBe("Alice"); }); }); // ─── Tests : auth.logout ────────────────────────────────────────────────────── describe("auth.logout", () => { it("efface le cookie de session et retourne success", async () => { const clearedCookies: string[] = []; const ctx: TrpcContext = { user: makeUser(), req: { protocol: "https", headers: {} } as TrpcContext["req"], res: { clearCookie: (name: string) => clearedCookies.push(name), } as unknown as TrpcContext["res"], }; const caller = appRouter.createCaller(ctx); const result = await caller.auth.logout(); expect(result.success).toBe(true); expect(clearedCookies).toContain(COOKIE_NAME); }); }); // ─── Tests : auth.loginLocal ────────────────────────────────────────────────── describe("auth.loginLocal", () => { it("rejette un email invalide", async () => { const caller = appRouter.createCaller(makeCtx(null)); await expect( caller.auth.loginLocal({ email: "not-an-email", password: "password123" }) ).rejects.toThrow(); }); it("rejette un mot de passe vide", async () => { const caller = appRouter.createCaller(makeCtx(null)); await expect( caller.auth.loginLocal({ email: "test@example.com", password: "" }) ).rejects.toThrow(); }); }); // ─── Tests : cgu ───────────────────────────────────────────────────────────── describe("cgu.status", () => { it("retourne le statut CGU de l'utilisateur", async () => { const user = makeUser({ cguAccepted: true }); const caller = appRouter.createCaller(makeCtx(user)); const result = await caller.cgu.status(); expect(result.accepted).toBe(true); }); it("retourne false si CGU non acceptée", async () => { const user = makeUser({ cguAccepted: false, cguAcceptedAt: null }); const caller = appRouter.createCaller(makeCtx(user)); const result = await caller.cgu.status(); expect(result.accepted).toBe(false); }); it("lève UNAUTHORIZED si non authentifié", async () => { const caller = appRouter.createCaller(makeCtx(null)); await expect(caller.cgu.status()).rejects.toMatchObject({ code: "UNAUTHORIZED", }); }); }); // ─── Tests : gestion des rôles ──────────────────────────────────────────────── describe("admin.updateRole", () => { it("lève FORBIDDEN pour un référent", async () => { const user = makeUser({ sonumRole: "referent" }); const caller = appRouter.createCaller(makeCtx(user)); await expect( caller.admin.updateRole({ userId: 2, sonumRole: "gestionnaire" }) ).rejects.toMatchObject({ code: "FORBIDDEN" }); }); it("lève FORBIDDEN pour un adhérent", async () => { const user = makeUser({ sonumRole: "adherent" }); const caller = appRouter.createCaller(makeCtx(user)); await expect( caller.admin.updateRole({ userId: 2, sonumRole: "referent" }) ).rejects.toMatchObject({ code: "FORBIDDEN" }); }); }); describe("admin.createUser", () => { it("lève FORBIDDEN pour un référent", async () => { const user = makeUser({ sonumRole: "referent" }); const caller = appRouter.createCaller(makeCtx(user)); await expect( caller.admin.createUser({ name: "Test", email: "test@test.com", sonumRole: "adherent", password: "password123", }) ).rejects.toMatchObject({ code: "FORBIDDEN" }); }); it("valide que le mot de passe fait au moins 8 caractères", async () => { const user = makeUser({ sonumRole: "gestionnaire" }); const caller = appRouter.createCaller(makeCtx(user)); await expect( caller.admin.createUser({ name: "Test", email: "test@test.com", sonumRole: "adherent", password: "short", }) ).rejects.toThrow(); }); }); // ─── Tests : affectations ───────────────────────────────────────────────────── describe("admin.setAffectations", () => { it("lève FORBIDDEN pour un non-gestionnaire", async () => { const user = makeUser({ sonumRole: "referent" }); const caller = appRouter.createCaller(makeCtx(user)); await expect( caller.admin.setAffectations({ userId: 2, etablissementIds: [1, 2] }) ).rejects.toMatchObject({ code: "FORBIDDEN" }); }); }); // ─── Tests : rôles des procédures admin ────────────────────────────────────── describe("admin.deleteUser", () => { it("lève FORBIDDEN pour un adhérent", async () => { const user = makeUser({ sonumRole: "adherent" }); const caller = appRouter.createCaller(makeCtx(user)); await expect( caller.admin.deleteUser({ userId: 2 }) ).rejects.toMatchObject({ code: "FORBIDDEN" }); }); }); describe("admin.resetPassword", () => { it("lève FORBIDDEN pour un référent", async () => { const user = makeUser({ sonumRole: "referent" }); const caller = appRouter.createCaller(makeCtx(user)); await expect( caller.admin.resetPassword({ userId: 2, newPassword: "newpassword123" }) ).rejects.toMatchObject({ code: "FORBIDDEN" }); }); }); // ─── Tests : filtrage adhérent ──────────────────────────────────────────────────────────────────────────────── describe("etablissements.byId - contrôle accès adhérent", () => { it("lève FORBIDDEN si l'adhérent tente d'accéder à un établissement non affecté", async () => { // L'adhérent n'a aucun établissement affecté (DB vide en test) const user = makeUser({ sonumRole: "adherent", id: 999 }); const caller = appRouter.createCaller(makeCtx(user)); // L'établissement id=1 n'est pas affecté à l'utilisateur id=999 // La procédure doit lever FORBIDDEN ou NOT_FOUND await expect( caller.etablissements.byId({ id: 1 }) ).rejects.toThrow(); }); }); describe("admin.setAffectations - accès gestionnaire", () => { it("accepte la requête d'un gestionnaire (ne lève pas FORBIDDEN)", async () => { const user = makeUser({ sonumRole: "gestionnaire" }); const caller = appRouter.createCaller(makeCtx(user)); // setAffectations avec une liste vide est idempotent et ne doit pas lever FORBIDDEN // (peut échouer sur DB indisponible mais pas sur les permissions) try { await caller.admin.setAffectations({ userId: 999, etablissementIds: [] }); } catch (err: any) { // Seule une erreur de permission est inacceptable expect(err?.code).not.toBe("FORBIDDEN"); } }); }); describe("auth.loginLocal - validation", () => { it("rejette un mot de passe trop court", async () => { const caller = appRouter.createCaller(makeCtx(null)); await expect( caller.auth.loginLocal({ email: "user@test.com", password: "" }) ).rejects.toThrow(); }); it("retourne UNAUTHORIZED pour des credentials inexistants", async () => { const caller = appRouter.createCaller(makeCtx(null)); await expect( caller.auth.loginLocal({ email: "nonexistent@test.com", password: "password123" }) ).rejects.toMatchObject({ code: "UNAUTHORIZED" }); }); });