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