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:
manus-admin
2026-04-12 18:34:56 -04:00
commit aab11c8308
138 changed files with 27782 additions and 0 deletions

198
server/auth.local.test.ts Normal file
View File

@@ -0,0 +1,198 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { appRouter } from "./routers";
import type { TrpcContext } from "./_core/context";
import bcrypt from "bcryptjs";
// ─── Mocks ─────────────────────────────────────────────────────────────────────
const mockPasswordHash = bcrypt.hashSync("Itinova69!", 10);
vi.mock("./db", () => ({
getUserByUsername: vi.fn().mockImplementation(async (username: string) => {
if (username === "adminServPodcast") {
return {
id: 99,
openId: "local-adminServPodcast",
username: "adminServPodcast",
name: "Administrateur Service Podcast",
email: null,
loginMethod: "local",
role: "admin",
passwordHash: mockPasswordHash,
immutable: true,
createdAt: new Date(),
updatedAt: new Date(),
lastSignedIn: new Date(),
};
}
return undefined;
}),
getUserByOpenId: vi.fn().mockImplementation(async (openId: string) => {
if (openId === "local-adminServPodcast") {
return {
id: 99,
openId: "local-adminServPodcast",
username: "adminServPodcast",
name: "Administrateur Service Podcast",
email: null,
loginMethod: "local",
role: "admin",
passwordHash: mockPasswordHash,
immutable: true,
createdAt: new Date(),
updatedAt: new Date(),
lastSignedIn: new Date(),
};
}
return undefined;
}),
upsertUser: vi.fn().mockResolvedValue(undefined),
getAllEtablissements: vi.fn().mockResolvedValue([]),
getAllMotsCles: vi.fn().mockResolvedValue([]),
getPodcasts: vi.fn().mockResolvedValue([]),
getPodcastById: vi.fn().mockResolvedValue(null),
createPodcast: vi.fn().mockResolvedValue(1),
updatePodcast: vi.fn().mockResolvedValue(undefined),
deletePodcast: vi.fn().mockResolvedValue(undefined),
getPodcastStats: vi.fn().mockResolvedValue({ total: 0, publies: 0, brouillons: 0, etablissements: 0 }),
getAllUsers: vi.fn().mockResolvedValue([]),
updateUserRole: vi.fn().mockResolvedValue(undefined),
createLocalUser: vi.fn().mockResolvedValue(undefined),
getEtablissementById: vi.fn().mockResolvedValue(null),
createEtablissement: vi.fn().mockResolvedValue(undefined),
updateEtablissement: vi.fn().mockResolvedValue(undefined),
deleteEtablissement: vi.fn().mockResolvedValue(undefined),
createMotCle: vi.fn().mockResolvedValue(undefined),
deleteMotCle: vi.fn().mockResolvedValue(undefined),
}));
// ─── Helpers ───────────────────────────────────────────────────────────────────
function makePublicCtx(): TrpcContext {
const cookies: Record<string, string> = {};
return {
user: null,
req: {
protocol: "https",
headers: {},
} as TrpcContext["req"],
res: {
clearCookie: vi.fn(),
cookie: vi.fn((name: string, value: string) => {
cookies[name] = value;
}),
} as unknown as TrpcContext["res"],
};
}
// ─── Tests ─────────────────────────────────────────────────────────────────────
describe("auth.loginLocal", () => {
it("réussit avec les bonnes credentials adminServPodcast", async () => {
const ctx = makePublicCtx();
const caller = appRouter.createCaller(ctx);
const result = await caller.auth.loginLocal({
username: "adminServPodcast",
password: "Itinova69!",
});
expect(result.success).toBe(true);
expect(result.user).toBeDefined();
expect(result.user.username).toBe("adminServPodcast");
expect(result.user.role).toBe("admin");
expect(result.user.immutable).toBe(true);
});
it("échoue avec un mauvais mot de passe", async () => {
const ctx = makePublicCtx();
const caller = appRouter.createCaller(ctx);
await expect(
caller.auth.loginLocal({
username: "adminServPodcast",
password: "mauvais-mot-de-passe",
})
).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Identifiant ou mot de passe incorrect",
});
});
it("échoue avec un utilisateur inexistant", async () => {
const ctx = makePublicCtx();
const caller = appRouter.createCaller(ctx);
await expect(
caller.auth.loginLocal({
username: "utilisateurInexistant",
password: "n'importe-quoi",
})
).rejects.toMatchObject({
code: "UNAUTHORIZED",
});
});
it("définit un cookie de session après connexion réussie", async () => {
const setCookieCalls: Array<{ name: string; value: string }> = [];
const ctx: TrpcContext = {
user: null,
req: { protocol: "https", headers: {} } as TrpcContext["req"],
res: {
clearCookie: vi.fn(),
cookie: vi.fn((name: string, value: string) => {
setCookieCalls.push({ name, value });
}),
} as unknown as TrpcContext["res"],
};
const caller = appRouter.createCaller(ctx);
await caller.auth.loginLocal({
username: "adminServPodcast",
password: "Itinova69!",
});
expect(setCookieCalls.length).toBeGreaterThan(0);
expect(setCookieCalls[0].name).toBeDefined();
expect(typeof setCookieCalls[0].value).toBe("string");
expect(setCookieCalls[0].value.length).toBeGreaterThan(10);
});
});
describe("compte adminServPodcast - propriétés immuables", () => {
it("le compte est marqué immutable = true", async () => {
const ctx = makePublicCtx();
const caller = appRouter.createCaller(ctx);
const result = await caller.auth.loginLocal({
username: "adminServPodcast",
password: "Itinova69!",
});
expect(result.user.immutable).toBe(true);
});
it("le compte a le rôle admin", async () => {
const ctx = makePublicCtx();
const caller = appRouter.createCaller(ctx);
const result = await caller.auth.loginLocal({
username: "adminServPodcast",
password: "Itinova69!",
});
expect(result.user.role).toBe("admin");
});
it("le compte utilise la méthode de connexion locale", async () => {
const ctx = makePublicCtx();
const caller = appRouter.createCaller(ctx);
const result = await caller.auth.loginLocal({
username: "adminServPodcast",
password: "Itinova69!",
});
expect(result.user.loginMethod).toBe("local");
});
});