Initial local backup snapshot

This commit is contained in:
Selecta Keke 2026-06-24 00:02:55 -03:00
commit acd9e14ba5
367 changed files with 118038 additions and 0 deletions

510
server/association.test.ts Normal file
View file

@ -0,0 +1,510 @@
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<TrpcContext["user"]>;
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);
});
});