Files
sonum/server/sonum-v2.test.ts

250 lines
9.5 KiB
TypeScript

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" });
});
});