Stack: Node.js/Express + React/Vite + tRPC + MySQL (Drizzle ORM) Features: Gestion de podcasts, établissements, mots-clés, upload audio S3 Migrations: 0000-0002 (users, etablissements, mots_cles, podcasts, podcast_mots_cles)
245 lines
8.9 KiB
TypeScript
245 lines
8.9 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
import { appRouter } from "./routers";
|
|
import type { TrpcContext } from "./_core/context";
|
|
|
|
// Mock the db module
|
|
vi.mock("./db", () => ({
|
|
getAllEtablissements: vi.fn().mockResolvedValue([
|
|
{ id: 1, nom: "Établissement Test", description: null, logoUrl: null, actif: true, createdAt: new Date(), updatedAt: new Date() },
|
|
]),
|
|
getEtablissementById: vi.fn().mockResolvedValue({ id: 1, nom: "Établissement Test", description: null }),
|
|
createEtablissement: vi.fn().mockResolvedValue(undefined),
|
|
updateEtablissement: vi.fn().mockResolvedValue(undefined),
|
|
deleteEtablissement: vi.fn().mockResolvedValue(undefined),
|
|
getAllMotsCles: vi.fn().mockResolvedValue([
|
|
{ id: 1, label: "Bien-être", createdAt: new Date() },
|
|
{ id: 2, label: "Formation", createdAt: new Date() },
|
|
]),
|
|
createMotCle: vi.fn().mockResolvedValue(undefined),
|
|
deleteMotCle: vi.fn().mockResolvedValue(undefined),
|
|
getPodcasts: vi.fn().mockResolvedValue([
|
|
{
|
|
id: 1,
|
|
titre: "Podcast Test",
|
|
resume: "Un résumé de test",
|
|
etablissementId: 1,
|
|
etablissementNom: "Établissement Test",
|
|
audioUrl: "https://example.com/audio.mp3",
|
|
audioKey: "podcasts/audio/test.mp3",
|
|
dureeSecondes: 300,
|
|
statut: "publie",
|
|
auteurId: 1,
|
|
auteurNom: "Test User",
|
|
imageUrl: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
motsCles: [{ id: 1, label: "Bien-être" }],
|
|
},
|
|
]),
|
|
getPodcastById: vi.fn().mockResolvedValue({
|
|
id: 1,
|
|
titre: "Podcast Test",
|
|
resume: "Un résumé de test",
|
|
etablissementId: 1,
|
|
etablissementNom: "Établissement Test",
|
|
audioUrl: "https://example.com/audio.mp3",
|
|
audioKey: "podcasts/audio/test.mp3",
|
|
dureeSecondes: 300,
|
|
statut: "publie",
|
|
auteurId: 1,
|
|
auteurNom: "Test User",
|
|
imageUrl: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
motsCles: [{ id: 1, label: "Bien-être" }],
|
|
}),
|
|
createPodcast: vi.fn().mockResolvedValue(42),
|
|
updatePodcast: vi.fn().mockResolvedValue(undefined),
|
|
deletePodcast: vi.fn().mockResolvedValue(undefined),
|
|
getPodcastStats: vi.fn().mockResolvedValue({ total: 5, publies: 3, brouillons: 2, etablissements: 2 }),
|
|
getAllUsers: vi.fn().mockResolvedValue([]),
|
|
updateUserRole: vi.fn().mockResolvedValue(undefined),
|
|
upsertUser: vi.fn().mockResolvedValue(undefined),
|
|
getUserByOpenId: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
function makePublicCtx(): TrpcContext {
|
|
return {
|
|
user: null,
|
|
req: { protocol: "https", headers: {} } as TrpcContext["req"],
|
|
res: { clearCookie: vi.fn() } as unknown as TrpcContext["res"],
|
|
};
|
|
}
|
|
|
|
function makeUserCtx(role: "user" | "admin" = "user"): TrpcContext {
|
|
return {
|
|
user: {
|
|
id: 1,
|
|
openId: "test-user",
|
|
name: "Test User",
|
|
email: "test@example.com",
|
|
loginMethod: "email",
|
|
role,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
lastSignedIn: new Date(),
|
|
},
|
|
req: { protocol: "https", headers: {} } as TrpcContext["req"],
|
|
res: { clearCookie: vi.fn() } as unknown as TrpcContext["res"],
|
|
};
|
|
}
|
|
|
|
// ─── Établissements ────────────────────────────────────────────────────────────
|
|
|
|
describe("etablissements.list", () => {
|
|
it("retourne la liste des établissements sans authentification", async () => {
|
|
const caller = appRouter.createCaller(makePublicCtx());
|
|
const result = await caller.etablissements.list();
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result.length).toBeGreaterThan(0);
|
|
expect(result[0]).toHaveProperty("nom");
|
|
});
|
|
});
|
|
|
|
describe("etablissements.create", () => {
|
|
it("refuse la création sans authentification admin", async () => {
|
|
const caller = appRouter.createCaller(makePublicCtx());
|
|
await expect(
|
|
caller.etablissements.create({ nom: "Test" })
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it("refuse la création pour un utilisateur standard", async () => {
|
|
const caller = appRouter.createCaller(makeUserCtx("user"));
|
|
await expect(
|
|
caller.etablissements.create({ nom: "Test" })
|
|
).rejects.toMatchObject({ code: "FORBIDDEN" });
|
|
});
|
|
|
|
it("permet la création pour un administrateur", async () => {
|
|
const caller = appRouter.createCaller(makeUserCtx("admin"));
|
|
await expect(
|
|
caller.etablissements.create({ nom: "Nouvel Établissement" })
|
|
).resolves.not.toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── Mots-clés ─────────────────────────────────────────────────────────────────
|
|
|
|
describe("motsCles.list", () => {
|
|
it("retourne la liste des mots-clés publiquement", async () => {
|
|
const caller = appRouter.createCaller(makePublicCtx());
|
|
const result = await caller.motsCles.list();
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result.length).toBe(2);
|
|
expect(result[0]).toHaveProperty("label");
|
|
});
|
|
});
|
|
|
|
describe("motsCles.create", () => {
|
|
it("refuse la création sans rôle admin", async () => {
|
|
const caller = appRouter.createCaller(makeUserCtx("user"));
|
|
await expect(
|
|
caller.motsCles.create({ label: "Test" })
|
|
).rejects.toMatchObject({ code: "FORBIDDEN" });
|
|
});
|
|
});
|
|
|
|
// ─── Podcasts ──────────────────────────────────────────────────────────────────
|
|
|
|
describe("podcasts.list", () => {
|
|
it("retourne la liste des podcasts publiquement", async () => {
|
|
const caller = appRouter.createCaller(makePublicCtx());
|
|
const result = await caller.podcasts.list({});
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result[0]).toHaveProperty("titre");
|
|
expect(result[0]).toHaveProperty("motsCles");
|
|
});
|
|
|
|
it("accepte un filtre par établissement", async () => {
|
|
const caller = appRouter.createCaller(makePublicCtx());
|
|
const result = await caller.podcasts.list({ etablissementId: 1 });
|
|
expect(Array.isArray(result)).toBe(true);
|
|
});
|
|
|
|
it("accepte un filtre par statut", async () => {
|
|
const caller = appRouter.createCaller(makePublicCtx());
|
|
const result = await caller.podcasts.list({ statut: "publie" });
|
|
expect(Array.isArray(result)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("podcasts.getById", () => {
|
|
it("retourne un podcast par son id", async () => {
|
|
const caller = appRouter.createCaller(makePublicCtx());
|
|
const result = await caller.podcasts.getById({ id: 1 });
|
|
expect(result).toHaveProperty("id", 1);
|
|
expect(result).toHaveProperty("titre", "Podcast Test");
|
|
});
|
|
});
|
|
|
|
describe("podcasts.create", () => {
|
|
it("refuse la création sans authentification", async () => {
|
|
const caller = appRouter.createCaller(makePublicCtx());
|
|
await expect(
|
|
caller.podcasts.create({
|
|
titre: "Test",
|
|
resume: "Résumé",
|
|
etablissementId: 1,
|
|
statut: "brouillon",
|
|
motCleIds: [],
|
|
})
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it("permet la création pour un utilisateur authentifié", async () => {
|
|
const caller = appRouter.createCaller(makeUserCtx("user"));
|
|
const result = await caller.podcasts.create({
|
|
titre: "Nouveau Podcast",
|
|
resume: "Un résumé complet",
|
|
etablissementId: 1,
|
|
statut: "brouillon",
|
|
motCleIds: [1],
|
|
});
|
|
expect(result).toBe(42);
|
|
});
|
|
});
|
|
|
|
describe("podcasts.delete", () => {
|
|
it("refuse la suppression pour un utilisateur non propriétaire", async () => {
|
|
const caller = appRouter.createCaller(makeUserCtx("user"));
|
|
// auteurId est 1, user.id est 1 → devrait réussir
|
|
await expect(
|
|
caller.podcasts.delete({ id: 1 })
|
|
).resolves.not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("podcasts.stats", () => {
|
|
it("retourne les statistiques pour un utilisateur authentifié", async () => {
|
|
const caller = appRouter.createCaller(makeUserCtx("user"));
|
|
const result = await caller.podcasts.stats();
|
|
expect(result).toHaveProperty("total");
|
|
expect(result).toHaveProperty("publies");
|
|
expect(result).toHaveProperty("brouillons");
|
|
expect(result).toHaveProperty("etablissements");
|
|
});
|
|
});
|
|
|
|
// ─── Auth ──────────────────────────────────────────────────────────────────────
|
|
|
|
describe("auth.me", () => {
|
|
it("retourne null pour un utilisateur non connecté", async () => {
|
|
const caller = appRouter.createCaller(makePublicCtx());
|
|
const result = await caller.auth.me();
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("retourne l'utilisateur connecté", async () => {
|
|
const caller = appRouter.createCaller(makeUserCtx("user"));
|
|
const result = await caller.auth.me();
|
|
expect(result).toHaveProperty("id", 1);
|
|
expect(result).toHaveProperty("role", "user");
|
|
});
|
|
});
|