import { describe, expect, it, vi, beforeEach } from "vitest"; import { appRouter } from "./routers"; import { buildDataPrivacyConsent } from "../shared/privacyCompliance"; import type { TrpcContext } from "./_core/context"; // Mock the database functions with all required exports vi.mock("./db", () => ({ getDb: vi.fn(() => Promise.resolve({})), upsertUser: vi.fn(), getUserByEmail: vi.fn(() => Promise.resolve(undefined)), getUserByOpenId: vi.fn(), getUserById: vi.fn(), getAllUsers: vi.fn(() => Promise.resolve([])), getAdminUsers: vi.fn(() => Promise.resolve([])), updateUser: vi.fn(), // Association functions getAssociationByUserId: vi.fn(() => Promise.resolve(null)), getAssociationById: vi.fn(() => Promise.resolve(null)), getAssociationBySourceDirectoryEntryId: vi.fn(() => Promise.resolve(null)), createAssociation: vi.fn(() => Promise.resolve(1)), updateAssociation: vi.fn(() => Promise.resolve()), getAllAssociations: vi.fn(() => Promise.resolve([])), searchAssociations: vi.fn(() => Promise.resolve({ data: [], total: 0 })), getAssociationCommuneCounts: vi.fn(() => Promise.resolve({ all: 0, kourou: 0, sinnamary: 0, iracoubo: 0, saint_elie: 0, })), toggleAssociationStatus: vi.fn(() => Promise.resolve()), getAssociationDirectoryEntryByNormalizedEmail: vi.fn(() => Promise.resolve(null)), getAssociationDirectoryEntryById: vi.fn(() => Promise.resolve(null)), getAssociationDirectoryEntriesSummary: vi.fn(() => Promise.resolve({ total: 0, withEmail: 0, withoutEmail: 0, lastImportAt: null })), listAssociationDirectoryEntriesWithStatus: vi.fn(() => Promise.resolve({ data: [], total: 0 })), listAssociationDirectoryMapEntries: vi.fn(() => Promise.resolve({ data: [], total: 0 })), getAssociationInvitationSummariesForDirectoryEntryIds: vi.fn(() => Promise.resolve({})), listAssociationDirectoryEntries: vi.fn(() => Promise.resolve([])), listAssociationDirectoryReviews: vi.fn(() => Promise.resolve([])), findAssociationDirectoryMatch: vi.fn(() => Promise.resolve({ status: "none", candidates: [], reason: "no match" })), upsertAssociationDirectoryEntry: vi.fn(() => Promise.resolve("created")), createAssociationDirectoryEntry: vi.fn(() => Promise.resolve(1)), updateAssociationDirectoryEntry: vi.fn(() => Promise.resolve()), getAssociationDirectoryEntryDetails: vi.fn(() => Promise.resolve(null)), createAssociationDirectoryReview: vi.fn(() => Promise.resolve(1)), getAssociationDirectoryReviewById: vi.fn(() => Promise.resolve(null)), getPendingAssociationDirectoryReviewByUserId: vi.fn(() => Promise.resolve(null)), updateAssociationDirectoryReview: vi.fn(() => Promise.resolve()), createAssociationInvitation: vi.fn(() => Promise.resolve(1)), getAssociationInvitationByToken: vi.fn(() => Promise.resolve(null)), getActiveAssociationInvitationByDirectoryEntryId: vi.fn(() => Promise.resolve(null)), getLatestAssociationInvitationByDirectoryEntryId: vi.fn(() => Promise.resolve(null)), revokeAssociationInvitationsByDirectoryEntryId: vi.fn(() => Promise.resolve()), markAssociationInvitationUsed: vi.fn(() => Promise.resolve()), // Document functions getDocumentsByAssociationId: vi.fn(() => Promise.resolve([])), getDocumentById: vi.fn(() => Promise.resolve(null)), createDocument: vi.fn(() => Promise.resolve(1)), deleteDocument: vi.fn(() => Promise.resolve()), // Request functions getRequestsByAssociationId: vi.fn(() => Promise.resolve([])), getRequestById: vi.fn(() => Promise.resolve(null)), createRequest: vi.fn(() => Promise.resolve(1)), updateRequest: vi.fn(() => Promise.resolve()), getAllRequests: vi.fn(() => Promise.resolve([])), getRequestsByStatus: vi.fn(() => Promise.resolve([])), searchRequests: vi.fn(() => Promise.resolve({ data: [], total: 0 })), getPendingRequests: vi.fn(() => Promise.resolve([])), getOverdueRequests: vi.fn(() => Promise.resolve([])), assignRequest: vi.fn(() => Promise.resolve()), // Request history createRequestHistory: vi.fn(() => Promise.resolve(1)), getRequestHistoryByRequestId: vi.fn(() => Promise.resolve([])), // Templates getAllRequestTemplates: vi.fn(() => Promise.resolve([])), getRequestTemplateById: vi.fn(() => Promise.resolve(null)), getRequestTemplateByType: vi.fn(() => Promise.resolve(null)), createRequestTemplate: vi.fn(() => Promise.resolve(1)), updateRequestTemplate: vi.fn(() => Promise.resolve()), getAllResponseTemplates: vi.fn(() => Promise.resolve([])), getResponseTemplateById: vi.fn(() => Promise.resolve(null)), createResponseTemplate: vi.fn(() => Promise.resolve(1)), updateResponseTemplate: vi.fn(() => Promise.resolve()), deleteResponseTemplate: vi.fn(() => Promise.resolve()), // Audit & Notifications createAuditLog: vi.fn(() => Promise.resolve(1)), getAuditLogs: vi.fn(() => Promise.resolve({ data: [], total: 0 })), createAdminNotification: vi.fn(() => Promise.resolve(1)), getAdminNotifications: vi.fn(() => Promise.resolve([])), markNotificationAsRead: vi.fn(() => Promise.resolve()), markAllNotificationsAsRead: vi.fn(() => Promise.resolve()), getUnreadNotificationCount: vi.fn(() => Promise.resolve(0)), // Settings getPortalSetting: vi.fn(() => Promise.resolve(null)), setPortalSetting: vi.fn(() => Promise.resolve()), getAllPortalSettings: vi.fn(() => Promise.resolve([])), // Stats getDashboardStats: vi.fn(() => Promise.resolve({ totalAssociations: 0, activeAssociations: 0, totalRequests: 0, pendingRequests: 0, validatedRequests: 0, rejectedRequests: 0, newAssociationsThisMonth: 0, newRequestsThisMonth: 0, requestsThisWeek: 0, overdueRequests: 0, acceptanceRate: 0, })), getRequestsPerMonth: vi.fn(() => Promise.resolve([])), getAssociationsPerMonth: vi.fn(() => Promise.resolve([])), getRequestsByTypeStats: vi.fn(() => Promise.resolve([])), getAverageProcessingTime: vi.fn(() => Promise.resolve(null)), })); // Mock storage vi.mock("./storage", () => ({ storagePut: vi.fn(() => Promise.resolve({ url: "https://example.com/file.pdf", key: "test-key" })), storageGet: vi.fn(() => Promise.resolve({ url: "https://example.com/file.pdf", key: "test-key" })), })); // Mock notification vi.mock("./_core/notification", () => ({ notifyOwner: vi.fn(() => Promise.resolve(true)), })); vi.mock("./mailer", () => ({ canSendOperationalEmails: vi.fn(() => true), sendOperationalEmail: vi.fn(() => Promise.resolve({ sent: true })), })); vi.mock("./_core/auth", () => ({ clearSessionCookie: vi.fn(), createSessionToken: vi.fn(() => Promise.resolve("session-token")), loginLocalUser: vi.fn(), registerLocalUser: vi.fn((input) => Promise.resolve({ id: 99, openId: input.email, email: input.email, name: input.name, loginMethod: "local_jwt", role: "user", isActive: true, createdAt: new Date(), updatedAt: new Date(), lastSignedIn: new Date(), })), setSessionCookie: vi.fn(), })); type AuthenticatedUser = NonNullable; function createAuthContext(role: "user" | "admin" | "directrice" | "super_admin" = "user"): TrpcContext { const user: AuthenticatedUser = { id: 1, openId: "test-user-123", email: "test@example.com", name: "Test User", loginMethod: "manus", role, canManageLogistics: false, canSignSalle: role === "directrice" || role === "super_admin", delegatedSalleSignerUserId: null, salleSignatureDelegatedByUserIds: [], isActive: true, createdAt: new Date(), updatedAt: new Date(), lastSignedIn: new Date(), }; return { user, req: { protocol: "https", headers: {}, } as TrpcContext["req"], res: { clearCookie: vi.fn(), } as unknown as TrpcContext["res"], }; } function createUnauthContext(): TrpcContext { return { user: null, req: { protocol: "https", headers: {}, } as TrpcContext["req"], res: { clearCookie: vi.fn(), } as unknown as TrpcContext["res"], }; } describe("association router", () => { beforeEach(() => { vi.clearAllMocks(); }); it("getMyProfile returns null when no association exists", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.association.getMyProfile(); expect(result).toBeNull(); }); it("exposes a protected portal directory listing", async () => { const db = await import("./db"); const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); (db.listAssociationDirectoryEntriesWithStatus as any).mockResolvedValue({ total: 1, data: [ { id: 12, nomAssociation: "Association des Savanes", emailOfficiel: "contact@example.com", ville: "Kourou", importedAt: new Date(), registered: false, registeredAt: null, invitationStatus: undefined, }, ], }); const result = await caller.associationDirectory.listPortal({ commune: "kourou", registrationStatus: "unregistered", limit: 20, }); expect(result.total).toBe(1); expect(result.data[0]?.nomAssociation).toBe("Association des Savanes"); }); it("returns a public secure invitation when the token is still valid", async () => { const db = await import("./db"); const caller = appRouter.createCaller(createUnauthContext()); (db.getAssociationInvitationByToken as any).mockResolvedValue({ token: "invite-token", directoryEntryId: 42, emailOfficiel: "contact@example.com", emailOfficielNormalise: "contact@example.com", sentByUserId: 1, deliveryMode: "email", emailSent: true, sentAt: new Date(), expiresAt: new Date(Date.now() + 60_000), usedAt: null, revokedAt: null, acceptedByUserId: null, createdAt: new Date(), updatedAt: new Date(), }); (db.getAssociationDirectoryEntryById as any).mockResolvedValue({ id: 42, nomAssociation: "Association des Savanes", emailOfficiel: "contact@example.com", nomRepresentant: "Mme Test", ville: "Kourou", }); const result = await caller.associationInvitation.getPublic({ token: "invite-token" }); expect(result.valid).toBe(true); expect(result.association.nomAssociation).toBe("Association des Savanes"); }); it("registers a user from a valid invitation and marks it as used", async () => { const db = await import("./db"); const ctx = createUnauthContext(); const caller = appRouter.createCaller(ctx); (db.getAssociationInvitationByToken as any).mockResolvedValue({ token: "invite-token", directoryEntryId: 42, emailOfficiel: "contact@example.com", emailOfficielNormalise: "contact@example.com", sentByUserId: 1, deliveryMode: "email", emailSent: true, sentAt: new Date(), expiresAt: new Date(Date.now() + 60_000), usedAt: null, revokedAt: null, acceptedByUserId: null, createdAt: new Date(), updatedAt: new Date(), }); (db.getAssociationDirectoryEntryById as any).mockResolvedValue({ id: 42, nomAssociation: "Association Importée", emailOfficiel: "contact@example.com", emailOfficielNormalise: "contact@example.com", siret: "12345678901234", rna: "W123456789", adresse: "1 rue des Savanes", codePostal: "97310", ville: "Kourou", telephone: "0594000000", siteWeb: null, dateCreation: null, objetAssociation: "Culture", statutJuridique: "association_loi_1901", nomRepresentant: "Mme Test", fonctionRepresentant: "Présidente", sourceFileName: "bordereau.xlsx", sourceRowNumber: 2, sourceFingerprint: "abc", isActive: true, importedAt: new Date(), updatedAt: new Date(), }); (db.getAssociationBySourceDirectoryEntryId as any).mockResolvedValue(null); (db.getAssociationByUserId as any).mockResolvedValue(null); const result = await caller.auth.register({ name: "Mme Test", email: "contact@example.com", password: "motdepasse", thematique: "culture_loisirs", invitationToken: "invite-token", privacyConsent: buildDataPrivacyConsent("register_account"), }); expect(result.email).toBe("contact@example.com"); expect(db.createAssociation).toHaveBeenCalled(); expect(db.markAssociationInvitationUsed).toHaveBeenCalledWith("invite-token", 99); }); it("getMyProfile auto-creates profile from imported directory when email matches", async () => { const db = await import("./db"); const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); (db.getAssociationDirectoryEntryByNormalizedEmail as any).mockResolvedValue({ id: 42, nomAssociation: "Association Importée", emailOfficiel: "test@example.com", emailOfficielNormalise: "test@example.com", siret: "12345678901234", rna: "W123456789", adresse: "1 rue des Savanes", codePostal: "97310", ville: "Kourou", telephone: "0594000000", siteWeb: null, dateCreation: null, objetAssociation: "Culture", statutJuridique: "association_loi_1901", nomRepresentant: "Mme Test", fonctionRepresentant: "Présidente", sourceFileName: "bordereau.xlsx", sourceRowNumber: 2, sourceFingerprint: "abc", isActive: true, importedAt: new Date(), updatedAt: new Date(), }); (db.getAssociationById as any).mockResolvedValue({ id: 1, userId: 1, sourceDirectoryEntryId: 42, nomAssociation: "Association Importée", siret: "12345678901234", rna: "W123456789", adresse: "1 rue des Savanes", codePostal: "97310", ville: "Kourou", telephone: "0594000000", emailContact: "test@example.com", siteWeb: null, dateCreation: null, objetAssociation: "Culture", statutJuridique: "association_loi_1901", nomRepresentant: "Mme Test", fonctionRepresentant: "Présidente", profileComplete: true, isActive: true, createdAt: new Date(), updatedAt: new Date(), }); const result = await caller.association.getMyProfile(); expect(db.createAssociation).toHaveBeenCalled(); expect(result?.sourceDirectoryEntryId).toBe(42); expect(result?.nomAssociation).toBe("Association Importée"); }); it("upsertProfile requires authentication", async () => { const ctx = createUnauthContext(); const caller = appRouter.createCaller(ctx); await expect( caller.association.upsertProfile({ nomAssociation: "Test Association", }) ).rejects.toThrow(); }); it("upsertProfile creates association for authenticated user", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.association.upsertProfile({ nomAssociation: "Test Association", siret: "12345678901234", adresse: "123 rue Test", codePostal: "75001", ville: "Paris", gouvernance: { representantLegal: { prenom: "Jean", nom: "Dupont", fonction: "president", email: "", telephone: "", }, membres: [], }, privacyConsent: buildDataPrivacyConsent("save_profile"), }); expect(result).toBeDefined(); expect(result).toHaveProperty('id'); expect(result.updated).toBe(false); }); }); describe("document router", () => { beforeEach(() => { vi.clearAllMocks(); }); it("getMyDocuments requires authentication", async () => { const ctx = createUnauthContext(); const caller = appRouter.createCaller(ctx); await expect(caller.document.getMyDocuments()).rejects.toThrow(); }); it("getMyDocuments returns empty array when no documents exist", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.document.getMyDocuments(); expect(result).toEqual([]); }); }); describe("request router", () => { beforeEach(() => { vi.clearAllMocks(); }); it("getMyRequests requires authentication", async () => { const ctx = createUnauthContext(); const caller = appRouter.createCaller(ctx); await expect(caller.request.getMyRequests()).rejects.toThrow(); }); it("getMyRequests returns empty array when no requests exist", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.request.getMyRequests(); expect(result).toEqual([]); }); }); describe("stats router", () => { beforeEach(() => { vi.clearAllMocks(); }); it("getDashboard requires admin role", async () => { const ctx = createAuthContext("user"); const caller = appRouter.createCaller(ctx); await expect(caller.stats.getDashboard()).rejects.toThrow(); }); it("getDashboard returns stats for admin", async () => { const ctx = createAuthContext("admin"); const caller = appRouter.createCaller(ctx); const result = await caller.stats.getDashboard(); expect(result).toBeDefined(); expect(result.totalAssociations).toBe(0); expect(result.totalRequests).toBe(0); }); });