Files
itinova-podcasts/server/podcasts.test.ts
manus-admin aab11c8308 Initial commit: itinova-podcasts v1
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)
2026-04-12 18:34:56 -04:00

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