- POST /corrections/eo accepts audioBase64 + mimeType (XOR with transcript) - Gemini transcribeAudio called server-side before correction - No audio storage (client downloads locally) - /transcriptions/token kept for future Deepgram live use Typecheck: OK · Tests: all green Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
219 lines
6.1 KiB
TypeScript
219 lines
6.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { Hono } from "hono";
|
|
|
|
// ─── Mocks ───────────────────────────────────────────────────────────────
|
|
|
|
vi.mock("../../middleware/auth", () => ({
|
|
authMiddleware: async (c: any, next: any) => {
|
|
const authHeader = c.req.header("Authorization");
|
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
return c.json({ error: true, code: "AUTH_REQUIRED" }, 401);
|
|
}
|
|
c.set("profile", {
|
|
id: "user-1",
|
|
email: "u@test.com",
|
|
plan: "standard",
|
|
simulations_used: 3,
|
|
});
|
|
await next();
|
|
},
|
|
}));
|
|
|
|
const { correctEOMock } = vi.hoisted(() => ({ correctEOMock: vi.fn() }));
|
|
vi.mock("../../controllers/correctionController", () => ({
|
|
correctEE: vi.fn(),
|
|
correctEO: correctEOMock,
|
|
}));
|
|
|
|
import correctionsRoutes from "../corrections";
|
|
|
|
function buildApp() {
|
|
const app = new Hono();
|
|
app.route("/corrections", correctionsRoutes);
|
|
return app;
|
|
}
|
|
|
|
const AUTH = { Authorization: "Bearer x" };
|
|
const JSON_HEADERS = { ...AUTH, "Content-Type": "application/json" };
|
|
|
|
describe("POST /corrections/eo — Sprint 4a", () => {
|
|
beforeEach(() => {
|
|
correctEOMock.mockReset();
|
|
});
|
|
|
|
it("401 sans Authorization", async () => {
|
|
const app = buildApp();
|
|
const res = await app.request("/corrections/eo", { method: "POST" });
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it("400 si simulationId manquant", async () => {
|
|
const app = buildApp();
|
|
const res = await app.request("/corrections/eo", {
|
|
method: "POST",
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({ tache: "EO_T1", transcript: "t" }),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
const body = await res.json();
|
|
expect(body.code).toBe("VALIDATION_ERROR");
|
|
});
|
|
|
|
it("400 si tache invalide (EO_T2 par exemple)", async () => {
|
|
const app = buildApp();
|
|
const res = await app.request("/corrections/eo", {
|
|
method: "POST",
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({
|
|
simulationId: "s1",
|
|
tache: "EO_T2",
|
|
transcript: "t",
|
|
}),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("400 si ni transcript ni audioBase64 fournis", async () => {
|
|
const app = buildApp();
|
|
const res = await app.request("/corrections/eo", {
|
|
method: "POST",
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({ simulationId: "s1", tache: "EO_T1" }),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
const body = await res.json();
|
|
expect(body.code).toBe("VALIDATION_ERROR");
|
|
});
|
|
|
|
it("400 si transcript ET audioBase64 fournis simultanément (XOR)", async () => {
|
|
const app = buildApp();
|
|
const res = await app.request("/corrections/eo", {
|
|
method: "POST",
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({
|
|
simulationId: "s1",
|
|
tache: "EO_T1",
|
|
transcript: "t",
|
|
audioBase64: "AAAA",
|
|
mimeType: "audio/webm",
|
|
}),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("400 si audioBase64 sans mimeType", async () => {
|
|
const app = buildApp();
|
|
const res = await app.request("/corrections/eo", {
|
|
method: "POST",
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({
|
|
simulationId: "s1",
|
|
tache: "EO_T1",
|
|
audioBase64: "AAAA",
|
|
}),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("400 si nclc_cible invalide", async () => {
|
|
const app = buildApp();
|
|
const res = await app.request("/corrections/eo", {
|
|
method: "POST",
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({
|
|
simulationId: "s1",
|
|
tache: "EO_T1",
|
|
transcript: "t",
|
|
nclc_cible: 8,
|
|
}),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("200 quand le controller renvoie un rapport (mode transcript)", async () => {
|
|
correctEOMock.mockResolvedValue({
|
|
data: {
|
|
score: 14,
|
|
nclc: 9,
|
|
simulation_id: "s1",
|
|
diagnostic: "d",
|
|
},
|
|
});
|
|
const app = buildApp();
|
|
const res = await app.request("/corrections/eo", {
|
|
method: "POST",
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({
|
|
simulationId: "s1",
|
|
tache: "EO_T1",
|
|
transcript: "Bonjour je m appelle Pierre",
|
|
}),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const body = await res.json();
|
|
expect(body.score).toBe(14);
|
|
expect(correctEOMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
simulationId: "s1",
|
|
tache: "EO_T1",
|
|
nclcCible: 9,
|
|
transcript: "Bonjour je m appelle Pierre",
|
|
}),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it("200 mode batch audio (transmet audioBase64 + mimeType au controller)", async () => {
|
|
correctEOMock.mockResolvedValue({
|
|
data: { score: 14, nclc: 9, simulation_id: "s-audio", diagnostic: "d" },
|
|
});
|
|
const app = buildApp();
|
|
const res = await app.request("/corrections/eo", {
|
|
method: "POST",
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({
|
|
simulationId: "s-audio",
|
|
tache: "EO_T1",
|
|
audioBase64: "AAAA",
|
|
mimeType: "audio/webm",
|
|
}),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
expect(correctEOMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
simulationId: "s-audio",
|
|
tache: "EO_T1",
|
|
nclcCible: 9,
|
|
audioBase64: "AAAA",
|
|
mimeType: "audio/webm",
|
|
}),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it("200 avec nclc_cible=10 transmis au controller", async () => {
|
|
correctEOMock.mockResolvedValue({
|
|
data: { score: 16, nclc: 10, simulation_id: "s2", diagnostic: "d" },
|
|
});
|
|
const app = buildApp();
|
|
const res = await app.request("/corrections/eo", {
|
|
method: "POST",
|
|
headers: JSON_HEADERS,
|
|
body: JSON.stringify({
|
|
simulationId: "s2",
|
|
tache: "EO_T1",
|
|
transcript: "Bonjour",
|
|
nclc_cible: 10,
|
|
}),
|
|
});
|
|
expect(res.status).toBe(200);
|
|
expect(correctEOMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
simulationId: "s2",
|
|
nclcCible: 10,
|
|
transcript: "Bonjour",
|
|
}),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
});
|