import { describe, expect, it, vi, beforeEach } from "vitest"; import { appRouter } from "./routers"; import type { TrpcContext } from "./_core/context"; import * as db from "./db"; // Mock the database module vi.mock("./db", () => ({ getDashboardStats: vi.fn().mockResolvedValue({ totalAssociations: 10, activeAssociations: 8, totalRequests: 25, pendingRequests: 5, validatedRequests: 15, rejectedRequests: 3, newAssociationsThisMonth: 2, newRequestsThisMonth: 8, requestsThisWeek: 3, overdueRequests: 1, acceptanceRate: 83, }), getRequestsPerMonth: vi.fn().mockResolvedValue([ { month: '2026-01', total: 10, validated: 5, rejected: 2, pending: 3 }, ]), getAssociationsPerMonth: vi.fn().mockResolvedValue([ { month: '2026-01', total: 3 }, ]), getRequestsByTypeStats: vi.fn().mockResolvedValue([ { type: 'subvention_fonctionnement', total: 10, validated: 5, totalMontantAccorde: 50000 }, ]), getAverageProcessingTime: vi.fn().mockResolvedValue(7.5), getPendingRequests: vi.fn().mockResolvedValue([]), getOverdueRequests: vi.fn().mockResolvedValue([]), getAdminUsers: vi.fn().mockResolvedValue([ { id: 1, name: 'Admin', email: 'admin@test.com', role: 'admin', lastSignedIn: new Date() }, ]), getAuditLogs: vi.fn().mockResolvedValue({ data: [], total: 0 }), getAdminNotifications: vi.fn().mockResolvedValue([]), getUnreadNotificationCount: vi.fn().mockResolvedValue(0), markNotificationAsRead: vi.fn().mockResolvedValue(undefined), markAllNotificationsAsRead: vi.fn().mockResolvedValue(undefined), searchAssociations: vi.fn().mockResolvedValue({ data: [], total: 0 }), getAssociationCommuneCounts: vi.fn().mockResolvedValue({ all: 5, kourou: 2, sinnamary: 1, iracoubo: 1, saint_elie: 1, }), searchRequests: vi.fn().mockResolvedValue({ data: [], total: 0 }), getAllPortalSettings: vi.fn().mockResolvedValue([]), getPortalSetting: vi.fn().mockResolvedValue(null), getPortalSettingRecord: vi.fn().mockResolvedValue(null), setPortalSetting: vi.fn().mockResolvedValue(undefined), getAssociationDirectoryEntriesSummary: vi.fn().mockResolvedValue({ total: 5, withEmail: 4, withoutEmail: 1, registered: 2, unregistered: 3, lastImportAt: null }), listAssociationDirectoryEntriesWithStatus: vi.fn().mockResolvedValue({ data: [], total: 0 }), listAssociationDirectoryMapEntries: vi.fn().mockResolvedValue({ data: [], total: 0 }), getAssociationInvitationSummariesForDirectoryEntryIds: vi.fn().mockResolvedValue({}), listAssociationDirectoryEntries: vi.fn().mockResolvedValue([]), listAssociationDirectoryReviews: vi.fn().mockResolvedValue([]), findAssociationDirectoryMatch: vi.fn().mockResolvedValue({ status: "none", candidates: [], reason: "no match" }), upsertAssociationDirectoryEntry: vi.fn().mockResolvedValue("created"), createAssociationDirectoryEntry: vi.fn().mockResolvedValue(1), updateAssociationDirectoryEntry: vi.fn().mockResolvedValue(undefined), getAssociationDirectoryEntryById: vi.fn().mockResolvedValue(null), getAssociationDirectoryEntryDetails: vi.fn().mockResolvedValue(null), createAssociationDirectoryReview: vi.fn().mockResolvedValue(1), getAssociationDirectoryReviewById: vi.fn().mockResolvedValue(null), getPendingAssociationDirectoryReviewByUserId: vi.fn().mockResolvedValue(null), updateAssociationDirectoryReview: vi.fn().mockResolvedValue(undefined), getAssociationBySourceDirectoryEntryId: vi.fn().mockResolvedValue(null), createAssociationInvitation: vi.fn().mockResolvedValue(1), getAssociationInvitationByToken: vi.fn().mockResolvedValue(null), getActiveAssociationInvitationByDirectoryEntryId: vi.fn().mockResolvedValue(null), getLatestAssociationInvitationByDirectoryEntryId: vi.fn().mockResolvedValue(null), revokeAssociationInvitationsByDirectoryEntryId: vi.fn().mockResolvedValue(undefined), markAssociationInvitationUsed: vi.fn().mockResolvedValue(undefined), createAuditLog: vi.fn().mockResolvedValue(1), getMaterialReturnFollowupByRequestId: vi.fn().mockResolvedValue(null), })); type AuthenticatedUser = NonNullable; function createAdminContext(): { ctx: TrpcContext } { const user: AuthenticatedUser = { id: 1, openId: "admin-user", email: "admin@example.com", name: "Admin User", loginMethod: "manus", role: "admin", canManageLogistics: false, canSignSalle: false, delegatedSalleSignerUserId: null, salleSignatureDelegatedByUserIds: [], isActive: true, createdAt: new Date(), updatedAt: new Date(), lastSignedIn: new Date(), }; const ctx: TrpcContext = { user, req: { protocol: "https", headers: {}, } as TrpcContext["req"], res: { clearCookie: vi.fn(), } as unknown as TrpcContext["res"], }; return { ctx }; } function createSuperAdminContext(): { ctx: TrpcContext } { const user: AuthenticatedUser = { id: 3, openId: "super-admin-user", email: "superadmin@example.com", name: "Super Admin User", loginMethod: "manus", role: "super_admin", canManageLogistics: true, canSignSalle: true, delegatedSalleSignerUserId: null, salleSignatureDelegatedByUserIds: [], isActive: true, createdAt: new Date(), updatedAt: new Date(), lastSignedIn: new Date(), }; const ctx: TrpcContext = { user, req: { protocol: "https", headers: {}, } as TrpcContext["req"], res: { clearCookie: vi.fn(), } as unknown as TrpcContext["res"], }; return { ctx }; } function createUserContext(): { ctx: TrpcContext } { const user: AuthenticatedUser = { id: 2, openId: "regular-user", email: "user@example.com", name: "Regular User", loginMethod: "manus", role: "user", isActive: true, createdAt: new Date(), updatedAt: new Date(), lastSignedIn: new Date(), }; const ctx: TrpcContext = { user, req: { protocol: "https", headers: {}, } as TrpcContext["req"], res: { clearCookie: vi.fn(), } as unknown as TrpcContext["res"], }; return { ctx }; } beforeEach(() => { vi.clearAllMocks(); }); describe("Admin Dashboard Stats", () => { it("returns dashboard statistics for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.stats.getDashboard(); expect(result).toBeDefined(); expect(result.totalAssociations).toBe(10); expect(result.activeAssociations).toBe(8); expect(result.totalRequests).toBe(25); expect(result.pendingRequests).toBe(5); expect(result.acceptanceRate).toBe(83); }); it("denies access to non-admin users", async () => { const { ctx } = createUserContext(); const caller = appRouter.createCaller(ctx); await expect(caller.stats.getDashboard()).rejects.toThrow( "Accès réservé à l’accueil, aux administrateurs et aux super administrateurs", ); }); }); describe("Admin Request Statistics", () => { it("returns requests per month for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.stats.getRequestsPerMonth({ months: 12 }); expect(Array.isArray(result)).toBe(true); }); it("returns requests by type for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.stats.getRequestsByType(); expect(Array.isArray(result)).toBe(true); }); it("returns average processing time for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.stats.getAverageProcessingTime(); expect(result).toBe(7.5); }); }); describe("Admin Notifications", () => { it("returns notifications for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.notifications.getAll({ unreadOnly: false }); expect(Array.isArray(result)).toBe(true); }); it("returns unread count for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.notifications.getUnreadCount(); expect(typeof result).toBe("number"); }); it("marks notification as read", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.notifications.markAsRead({ id: 1 }); expect(result.success).toBe(true); }); it("marks all notifications as read", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.notifications.markAllAsRead(); expect(result.success).toBe(true); }); }); describe("Admin User Management", () => { it("lists admin users for super admin", async () => { const { ctx } = createSuperAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.adminUsers.listAll(); expect(Array.isArray(result)).toBe(true); expect(result.length).toBeGreaterThan(0); expect(result[0].role).toBe("admin"); }); it("denies admin user listing to regular admins", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); await expect(caller.adminUsers.listAll()).rejects.toThrow("Accès réservé aux super administrateurs"); }); }); describe("Audit Log", () => { it("returns audit logs for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.auditLog.list({ limit: 10 }); expect(result).toBeDefined(); expect(result.data).toBeDefined(); expect(Array.isArray(result.data)).toBe(true); }); }); describe("Association Directory Admin", () => { it("returns directory summary for admin", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.associationDirectory.getSummary(); expect(result.total).toBe(5); expect(result.withEmail).toBe(4); expect(result.unregistered).toBe(3); }); it("returns directory entries with registration filters for admin", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.associationDirectory.listLatest({ registrationStatus: "unregistered", commune: "kourou", limit: 25, }); expect(result).toEqual({ data: [], total: 0 }); }); it("denies directory summary to regular users", async () => { const { ctx } = createUserContext(); const caller = appRouter.createCaller(ctx); await expect(caller.associationDirectory.getSummary()).rejects.toThrow("Accès réservé aux administrateurs"); }); }); describe("Association Search", () => { it("searches associations for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.association.search({ search: "test", limit: 10, }); expect(result).toBeDefined(); expect(result.data).toBeDefined(); expect(typeof result.total).toBe("number"); }); it("returns commune counts for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.association.getCommuneCounts(); expect(result.kourou).toBe(2); expect(result.all).toBe(5); }); }); describe("Request Search", () => { it("searches requests for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.request.search({ search: "test", status: "soumise", limit: 10, }); expect(result).toBeDefined(); expect(result.data).toBeDefined(); expect(typeof result.total).toBe("number"); }); it("returns pending requests for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.request.getPending(); expect(Array.isArray(result)).toBe(true); }); it("returns overdue requests for admin users", async () => { const { ctx } = createAdminContext(); const caller = appRouter.createCaller(ctx); const result = await caller.request.getOverdue(); expect(Array.isArray(result)).toBe(true); }); }); describe("Material Availability", () => { it("counts submitted and in-progress material requests before validation", async () => { vi.mocked(db.searchRequests).mockResolvedValue({ data: [ { id: 101, type: "demande_materiel_evenementiel", status: "soumise", formData: JSON.stringify({ dateDebutManifestation: "2026-06-10", dateRestitution: "2026-06-12", materielsDemandes: { tente3x3: true }, quantitesDemandees: { tente3x3: "4" }, }), }, { id: 102, type: "demande_materiel_evenementiel", status: "en_cours_traitement", formData: JSON.stringify({ dateDebutManifestation: "2026-06-10", dateRestitution: "2026-06-12", materielsDemandes: { tente3x3: true }, quantitesDemandees: { tente3x3: "3" }, }), }, { id: 103, type: "demande_materiel_evenementiel", status: "brouillon", formData: JSON.stringify({ dateDebutManifestation: "2026-06-10", dateRestitution: "2026-06-12", materielsDemandes: { tente3x3: true }, quantitesDemandees: { tente3x3: "2" }, }), }, ] as any, total: 3, }); vi.mocked(db.getMaterialReturnFollowupByRequestId).mockResolvedValue(null as any); const { ctx } = createUserContext(); const caller = appRouter.createCaller(ctx); const result = await caller.request.getMaterialAvailability({ dateDebut: "2026-06-10", dateFin: "2026-06-12", }); const tente = result.items.find((item) => item.key === "tente3x3"); expect(tente?.reserved).toBe(7); expect(tente?.available).toBe(5); }); });