SONUM v7 - Évolution v6 (éditeurs/blocs CRUD, tableau de bord stats) + vue liste alternance couleurs
This commit is contained in:
249
server/sonum-v2.test.ts
Normal file
249
server/sonum-v2.test.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { appRouter } from "./routers";
|
||||
import { COOKIE_NAME } from "../shared/const";
|
||||
import type { TrpcContext } from "./_core/context";
|
||||
import type { User } from "../drizzle/schema";
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function makeUser(overrides: Partial<User> = {}): User {
|
||||
return {
|
||||
id: 1,
|
||||
openId: "test-open-id",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
loginMethod: "local",
|
||||
role: "user",
|
||||
sonumRole: "referent",
|
||||
cguAccepted: true,
|
||||
cguAcceptedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
lastSignedIn: new Date(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeCtx(user: User | null = null): TrpcContext {
|
||||
const cookies: Record<string, string> = {};
|
||||
return {
|
||||
user,
|
||||
req: {
|
||||
protocol: "https",
|
||||
headers: {},
|
||||
} as TrpcContext["req"],
|
||||
res: {
|
||||
cookie: (name: string, value: string, _opts: unknown) => {
|
||||
cookies[name] = value;
|
||||
},
|
||||
clearCookie: (_name: string, _opts: unknown) => {},
|
||||
} as unknown as TrpcContext["res"],
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Tests : auth.me ──────────────────────────────────────────────────────────
|
||||
|
||||
describe("auth.me", () => {
|
||||
it("retourne null quand non authentifié", async () => {
|
||||
const caller = appRouter.createCaller(makeCtx(null));
|
||||
const result = await caller.auth.me();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("retourne l'utilisateur quand authentifié", async () => {
|
||||
const user = makeUser({ name: "Alice" });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
const result = await caller.auth.me();
|
||||
expect(result?.name).toBe("Alice");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tests : auth.logout ──────────────────────────────────────────────────────
|
||||
|
||||
describe("auth.logout", () => {
|
||||
it("efface le cookie de session et retourne success", async () => {
|
||||
const clearedCookies: string[] = [];
|
||||
const ctx: TrpcContext = {
|
||||
user: makeUser(),
|
||||
req: { protocol: "https", headers: {} } as TrpcContext["req"],
|
||||
res: {
|
||||
clearCookie: (name: string) => clearedCookies.push(name),
|
||||
} as unknown as TrpcContext["res"],
|
||||
};
|
||||
const caller = appRouter.createCaller(ctx);
|
||||
const result = await caller.auth.logout();
|
||||
expect(result.success).toBe(true);
|
||||
expect(clearedCookies).toContain(COOKIE_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tests : auth.loginLocal ──────────────────────────────────────────────────
|
||||
|
||||
describe("auth.loginLocal", () => {
|
||||
it("rejette un email invalide", async () => {
|
||||
const caller = appRouter.createCaller(makeCtx(null));
|
||||
await expect(
|
||||
caller.auth.loginLocal({ email: "not-an-email", password: "password123" })
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("rejette un mot de passe vide", async () => {
|
||||
const caller = appRouter.createCaller(makeCtx(null));
|
||||
await expect(
|
||||
caller.auth.loginLocal({ email: "test@example.com", password: "" })
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tests : cgu ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe("cgu.status", () => {
|
||||
it("retourne le statut CGU de l'utilisateur", async () => {
|
||||
const user = makeUser({ cguAccepted: true });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
const result = await caller.cgu.status();
|
||||
expect(result.accepted).toBe(true);
|
||||
});
|
||||
|
||||
it("retourne false si CGU non acceptée", async () => {
|
||||
const user = makeUser({ cguAccepted: false, cguAcceptedAt: null });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
const result = await caller.cgu.status();
|
||||
expect(result.accepted).toBe(false);
|
||||
});
|
||||
|
||||
it("lève UNAUTHORIZED si non authentifié", async () => {
|
||||
const caller = appRouter.createCaller(makeCtx(null));
|
||||
await expect(caller.cgu.status()).rejects.toMatchObject({
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tests : gestion des rôles ────────────────────────────────────────────────
|
||||
|
||||
describe("admin.updateRole", () => {
|
||||
it("lève FORBIDDEN pour un référent", async () => {
|
||||
const user = makeUser({ sonumRole: "referent" });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
await expect(
|
||||
caller.admin.updateRole({ userId: 2, sonumRole: "gestionnaire" })
|
||||
).rejects.toMatchObject({ code: "FORBIDDEN" });
|
||||
});
|
||||
|
||||
it("lève FORBIDDEN pour un adhérent", async () => {
|
||||
const user = makeUser({ sonumRole: "adherent" });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
await expect(
|
||||
caller.admin.updateRole({ userId: 2, sonumRole: "referent" })
|
||||
).rejects.toMatchObject({ code: "FORBIDDEN" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("admin.createUser", () => {
|
||||
it("lève FORBIDDEN pour un référent", async () => {
|
||||
const user = makeUser({ sonumRole: "referent" });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
await expect(
|
||||
caller.admin.createUser({
|
||||
name: "Test",
|
||||
email: "test@test.com",
|
||||
sonumRole: "adherent",
|
||||
password: "password123",
|
||||
})
|
||||
).rejects.toMatchObject({ code: "FORBIDDEN" });
|
||||
});
|
||||
|
||||
it("valide que le mot de passe fait au moins 8 caractères", async () => {
|
||||
const user = makeUser({ sonumRole: "gestionnaire" });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
await expect(
|
||||
caller.admin.createUser({
|
||||
name: "Test",
|
||||
email: "test@test.com",
|
||||
sonumRole: "adherent",
|
||||
password: "short",
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tests : affectations ─────────────────────────────────────────────────────
|
||||
|
||||
describe("admin.setAffectations", () => {
|
||||
it("lève FORBIDDEN pour un non-gestionnaire", async () => {
|
||||
const user = makeUser({ sonumRole: "referent" });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
await expect(
|
||||
caller.admin.setAffectations({ userId: 2, etablissementIds: [1, 2] })
|
||||
).rejects.toMatchObject({ code: "FORBIDDEN" });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tests : rôles des procédures admin ──────────────────────────────────────
|
||||
|
||||
describe("admin.deleteUser", () => {
|
||||
it("lève FORBIDDEN pour un adhérent", async () => {
|
||||
const user = makeUser({ sonumRole: "adherent" });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
await expect(
|
||||
caller.admin.deleteUser({ userId: 2 })
|
||||
).rejects.toMatchObject({ code: "FORBIDDEN" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("admin.resetPassword", () => {
|
||||
it("lève FORBIDDEN pour un référent", async () => {
|
||||
const user = makeUser({ sonumRole: "referent" });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
await expect(
|
||||
caller.admin.resetPassword({ userId: 2, newPassword: "newpassword123" })
|
||||
).rejects.toMatchObject({ code: "FORBIDDEN" });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tests : filtrage adhérent ────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("etablissements.byId - contrôle accès adhérent", () => {
|
||||
it("lève FORBIDDEN si l'adhérent tente d'accéder à un établissement non affecté", async () => {
|
||||
// L'adhérent n'a aucun établissement affecté (DB vide en test)
|
||||
const user = makeUser({ sonumRole: "adherent", id: 999 });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
// L'établissement id=1 n'est pas affecté à l'utilisateur id=999
|
||||
// La procédure doit lever FORBIDDEN ou NOT_FOUND
|
||||
await expect(
|
||||
caller.etablissements.byId({ id: 1 })
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("admin.setAffectations - accès gestionnaire", () => {
|
||||
it("accepte la requête d'un gestionnaire (ne lève pas FORBIDDEN)", async () => {
|
||||
const user = makeUser({ sonumRole: "gestionnaire" });
|
||||
const caller = appRouter.createCaller(makeCtx(user));
|
||||
// setAffectations avec une liste vide est idempotent et ne doit pas lever FORBIDDEN
|
||||
// (peut échouer sur DB indisponible mais pas sur les permissions)
|
||||
try {
|
||||
await caller.admin.setAffectations({ userId: 999, etablissementIds: [] });
|
||||
} catch (err: any) {
|
||||
// Seule une erreur de permission est inacceptable
|
||||
expect(err?.code).not.toBe("FORBIDDEN");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("auth.loginLocal - validation", () => {
|
||||
it("rejette un mot de passe trop court", async () => {
|
||||
const caller = appRouter.createCaller(makeCtx(null));
|
||||
await expect(
|
||||
caller.auth.loginLocal({ email: "user@test.com", password: "" })
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("retourne UNAUTHORIZED pour des credentials inexistants", async () => {
|
||||
const caller = appRouter.createCaller(makeCtx(null));
|
||||
await expect(
|
||||
caller.auth.loginLocal({ email: "nonexistent@test.com", password: "password123" })
|
||||
).rejects.toMatchObject({ code: "UNAUTHORIZED" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user