expria-backend/src/controllers/__tests__/presentationGenerate.test.ts
Hermann_Kitio 7cac057062 feat(eo): align correction EO on 3.6a format + Deepgram token + T1 presentation generation
Sprint 4a:
- correctEO aligned on CorrectionRapport format (revelation, diagnostic, criteres, conseil_nclc, erreurs_codes)
- nclc_cible parameter (default 9, accepts 9|10)
- Fire-and-forget modele + exercices jobs (same pattern as EE)
- EO-specific DeepSeek prompt (oral transcript tolerance, 4 TCF criteria)
- Gemini transcribeAudio: 30s timeout + 1 retry
- POST /presentations/generate: 5-field questionnaire → DeepSeek generates oral presentation (~220-260 words, NCLC 7-8)
- Migration 006_sprint_4a_eo.sql (documentation only — no audio storage)

Sprint 4b:
- POST /transcriptions/token: Deepgram temporary API key (600s TTL)
- Removed audio storage pipeline (audioStorage.ts, XOR validation, 14MB limit)
- Backend receives transcript text only, no audio files
- TD-10/TD-11 resolved (Sprint 3.6c), TD-16/17/18 resolved (4b cleanup)

Typecheck: OK · Tests: 241/241 

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 05:04:26 +03:00

148 lines
4.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
// Validation pure (pas de fetch).
describe("presentationController.validateReponses", () => {
beforeEach(() => {
vi.resetModules();
});
it("accepte les 5 champs requis non vides", async () => {
const { validateReponses } = await import("../presentationController");
const result = validateReponses({
prenom_age_ville: "Pierre, 30 ans, Alger",
formation_metier: "Ingénieur",
situation_familiale: "Marié, deux enfants",
loisirs: "Lecture, randonnée",
motivation_canada: "Opportunités professionnelles",
});
expect("ok" in result && result.ok).toBe(true);
});
it("rejette si reponses non objet", async () => {
const { validateReponses } = await import("../presentationController");
const result = validateReponses("string");
expect("error" in result).toBe(true);
if ("error" in result) expect(result.code).toBe("VALIDATION_ERROR");
});
it.each([
"prenom_age_ville",
"formation_metier",
"situation_familiale",
"loisirs",
"motivation_canada",
])("rejette si %s manquant", async (field) => {
const { validateReponses } = await import("../presentationController");
const all: Record<string, string> = {
prenom_age_ville: "a",
formation_metier: "b",
situation_familiale: "c",
loisirs: "d",
motivation_canada: "e",
};
delete all[field];
const result = validateReponses(all);
expect("error" in result).toBe(true);
});
it("rejette les champs vides ou whitespace", async () => {
const { validateReponses } = await import("../presentationController");
const result = validateReponses({
prenom_age_ville: " ",
formation_metier: "b",
situation_familiale: "c",
loisirs: "d",
motivation_canada: "e",
});
expect("error" in result).toBe(true);
});
});
// Pipeline complet — fetch mocké.
describe("presentationController.generate", () => {
beforeEach(() => {
vi.resetModules();
vi.restoreAllMocks();
});
const VALID_REPONSES = {
prenom_age_ville: "Pierre, 30 ans, Alger",
formation_metier: "Ingénieur",
situation_familiale: "Marié",
loisirs: "Lecture",
motivation_canada: "Travail",
};
it("succès → renvoie { presentation: string }", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
choices: [
{ message: { content: "Bonjour, je m'appelle Pierre. Voilà." } },
],
}),
}),
);
const { generate } = await import("../presentationController");
const result = await generate(VALID_REPONSES);
expect("data" in result).toBe(true);
if ("data" in result) {
expect(result.data.presentation).toContain("Pierre");
}
});
it("DeepSeek non-OK → INTERNAL_ERROR 500", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({ ok: false, status: 500, statusText: "I" }),
);
const { generate } = await import("../presentationController");
const result = await generate(VALID_REPONSES);
expect("error" in result).toBe(true);
if ("error" in result) {
expect(result.code).toBe("INTERNAL_ERROR");
expect(result.status).toBe(500);
}
});
it("réponse vide → INTERNAL_ERROR 500", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ choices: [{ message: { content: "" } }] }),
}),
);
const { generate } = await import("../presentationController");
const result = await generate(VALID_REPONSES);
expect("error" in result).toBe(true);
});
it("fetch throw (timeout) → INTERNAL_ERROR 500", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockRejectedValue(new Error("network down")),
);
const { generate } = await import("../presentationController");
const result = await generate(VALID_REPONSES);
expect("error" in result).toBe(true);
if ("error" in result) expect(result.code).toBe("INTERNAL_ERROR");
});
it("rejette les body invalides en court-circuitant fetch", async () => {
const fetchSpy = vi.fn();
vi.stubGlobal("fetch", fetchSpy);
const { generate } = await import("../presentationController");
const result = await generate({ prenom_age_ville: "" });
expect("error" in result).toBe(true);
expect(fetchSpy).not.toHaveBeenCalled();
});
});