250 lines
9.5 KiB
TypeScript
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" });
|
|
});
|
|
});
|