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)
This commit is contained in:
244
server/podcasts.test.ts
Normal file
244
server/podcasts.test.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user