452 lines
14 KiB
TypeScript
452 lines
14 KiB
TypeScript
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é à 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);
|
||
});
|
||
});
|