portail-associations/server/admin.test.ts

452 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<TrpcContext["user"]>;
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é à laccueil, 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);
});
});