/** * Tests Vitest — Authentification locale Itinova Budget SI * Couvre : login, logout, me, users.create, etablissements.list */ 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, login: "admin", email: "admin@itinova.fr", passwordHash: "$2a$10$hashedpassword", firstName: "Admin", lastName: "Itinova", role: "admin", isActive: true, createdAt: new Date(), updatedAt: new Date(), lastSignedIn: null, ...overrides, }; } type CookieCall = { name: string; options: Record }; function createPublicCtx(): { ctx: TrpcContext; setCookies: CookieCall[]; clearedCookies: CookieCall[] } { const setCookies: CookieCall[] = []; const clearedCookies: CookieCall[] = []; const ctx: TrpcContext = { user: null, req: { protocol: "https", headers: {} } as TrpcContext["req"], res: { cookie: (name: string, _val: string, options: Record) => setCookies.push({ name, options }), clearCookie: (name: string, options: Record) => clearedCookies.push({ name, options }), } as unknown as TrpcContext["res"], }; return { ctx, setCookies, clearedCookies }; } function createAuthCtx(userOverrides: Partial = {}): { ctx: TrpcContext; clearedCookies: CookieCall[] } { const clearedCookies: CookieCall[] = []; const ctx: TrpcContext = { user: makeUser(userOverrides), req: { protocol: "https", headers: {} } as TrpcContext["req"], res: { cookie: () => {}, clearCookie: (name: string, options: Record) => clearedCookies.push({ name, options }), } as unknown as TrpcContext["res"], }; return { ctx, clearedCookies }; } // ─── Mock db ───────────────────────────────────────────────────────────────── vi.mock("./db", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, getUserByLogin: vi.fn(), updateLastSignedIn: vi.fn(), createUser: vi.fn(), listUsers: vi.fn(), listEtablissements: vi.fn(), getParametres: vi.fn(), }; }); vi.mock("./_core/sdk", () => ({ sdk: { createSessionToken: vi.fn().mockResolvedValue("mock-jwt-token"), authenticateRequest: vi.fn(), }, })); // ─── Tests auth.logout ──────────────────────────────────────────────────────── describe("auth.logout", () => { it("efface le cookie de session et retourne success:true", async () => { const { ctx, clearedCookies } = createAuthCtx(); const caller = appRouter.createCaller(ctx); const result = await caller.auth.logout(); expect(result).toEqual({ success: true }); expect(clearedCookies).toHaveLength(1); expect(clearedCookies[0]?.name).toBe(COOKIE_NAME); expect(clearedCookies[0]?.options).toMatchObject({ maxAge: -1, httpOnly: true, path: "/", }); }); it("fonctionne aussi sans utilisateur connecté (public procedure)", async () => { const { ctx, clearedCookies } = createPublicCtx(); const caller = appRouter.createCaller(ctx); const result = await caller.auth.logout(); expect(result).toEqual({ success: true }); expect(clearedCookies).toHaveLength(1); }); }); // ─── Tests auth.me ──────────────────────────────────────────────────────────── describe("auth.me", () => { it("retourne null si non authentifié", async () => { const { ctx } = createPublicCtx(); const caller = appRouter.createCaller(ctx); const result = await caller.auth.me(); expect(result).toBeNull(); }); it("retourne les infos utilisateur si authentifié", async () => { const { ctx } = createAuthCtx({ login: "jdupont", email: "j.dupont@itinova.fr", role: "standard" }); const caller = appRouter.createCaller(ctx); const result = await caller.auth.me(); expect(result).not.toBeNull(); expect(result?.login).toBe("jdupont"); expect(result?.email).toBe("j.dupont@itinova.fr"); expect(result?.role).toBe("standard"); }); it("ne retourne pas le hash du mot de passe", async () => { const { ctx } = createAuthCtx(); const caller = appRouter.createCaller(ctx); const result = await caller.auth.me(); expect(result).not.toHaveProperty("passwordHash"); }); }); // ─── Tests auth.login ──────────────────────────────────────────────────────── describe("auth.login", () => { beforeEach(() => { vi.clearAllMocks(); }); it("refuse un login avec identifiants invalides (utilisateur inexistant)", async () => { const { db } = await import("./db").then(m => ({ db: m })); (db.getUserByLogin as ReturnType).mockResolvedValue(null); const { ctx } = createPublicCtx(); const caller = appRouter.createCaller(ctx); await expect(caller.auth.login({ login: "inexistant", password: "mauvais" })) .rejects.toThrow("Identifiants invalides"); }); it("refuse un compte inactif", async () => { const { db } = await import("./db").then(m => ({ db: m })); (db.getUserByLogin as ReturnType).mockResolvedValue( makeUser({ isActive: false }) ); const { ctx } = createPublicCtx(); const caller = appRouter.createCaller(ctx); await expect(caller.auth.login({ login: "admin", password: "password" })) .rejects.toThrow("Identifiants invalides"); }); }); // ─── Tests etablissements.list ──────────────────────────────────────────────── describe("etablissements.list", () => { beforeEach(() => { vi.clearAllMocks(); }); it("retourne la liste des établissements pour un utilisateur connecté", async () => { const { db } = await import("./db").then(m => ({ db: m })); const mockEtabs = [ { id: 1, code: "ETB001", nom: "EHPAD Les Pins", groupe: "Itinova", ville: "Lyon", actif: true, createdAt: new Date(), updatedAt: new Date() }, { id: 2, code: "ETB002", nom: "Résidence Soleil", groupe: "Itinova", ville: "Grenoble", actif: true, createdAt: new Date(), updatedAt: new Date() }, ]; (db.listEtablissements as ReturnType).mockResolvedValue(mockEtabs); const { ctx } = createAuthCtx(); const caller = appRouter.createCaller(ctx); const result = await caller.etablissements.list(); expect(result).toHaveLength(2); expect(result[0]?.code).toBe("ETB001"); expect(result[1]?.code).toBe("ETB002"); }); it("lève une erreur UNAUTHORIZED si non authentifié", async () => { const { ctx } = createPublicCtx(); const caller = appRouter.createCaller(ctx); await expect(caller.etablissements.list()).rejects.toThrow(); }); }); // ─── Tests users.list (admin only) ─────────────────────────────────────────── describe("users.list", () => { beforeEach(() => { vi.clearAllMocks(); }); it("retourne la liste des utilisateurs pour un admin", async () => { const { db } = await import("./db").then(m => ({ db: m })); const mockUsers = [ makeUser({ id: 1, login: "admin", role: "admin" }), makeUser({ id: 2, login: "jdupont", role: "standard" }), ]; (db.listUsers as ReturnType).mockResolvedValue(mockUsers); const { ctx } = createAuthCtx({ role: "admin" }); const caller = appRouter.createCaller(ctx); const result = await caller.users.list(); expect(result).toHaveLength(2); expect(result[0]?.role).toBe("admin"); }); it("lève FORBIDDEN pour un utilisateur standard", async () => { const { ctx } = createAuthCtx({ role: "standard" }); const caller = appRouter.createCaller(ctx); await expect(caller.users.list()).rejects.toThrow(); }); it("lève FORBIDDEN pour un utilisateur readonly", async () => { const { ctx } = createAuthCtx({ role: "readonly" }); const caller = appRouter.createCaller(ctx); await expect(caller.users.list()).rejects.toThrow(); }); }); // ─── Tests parametres.get ───────────────────────────────────────────────────── describe("parametres.get", () => { beforeEach(() => { vi.clearAllMocks(); }); it("retourne les paramètres sous forme d'objet clé-valeur", async () => { const { db } = await import("./db").then(m => ({ db: m })); (db.getParametres as ReturnType).mockResolvedValue([ { id: 1, cle: "seuil_fixes_ans", valeur: "5", updatedAt: new Date() }, { id: 2, cle: "cout_fixe", valeur: "850", updatedAt: new Date() }, ]); const { ctx } = createAuthCtx(); const caller = appRouter.createCaller(ctx); const result = await caller.parametres.get(); expect(result).toEqual({ seuil_fixes_ans: "5", cout_fixe: "850" }); }); });