Initial local backup snapshot
This commit is contained in:
commit
acd9e14ba5
367 changed files with 118038 additions and 0 deletions
452
server/admin.test.ts
Normal file
452
server/admin.test.ts
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue