retrotoon-studio/server/routers.test.ts
Ubuntu 20a643c4ce fix: audit complet et pipeline fonctionnel RetroToon Studio
Corrections critiques:
- Fix titre HTML {{project_title}} -> %VITE_APP_TITLE%
- Suppression vitePluginManusRuntime (360KB -> 4KB index.html)
- Upload vidéo: multer au lieu du parsing binary maison (anti-corruption)
- Extraction audio ffmpeg + sauvegarde sourceAudioUrl en DB
- Page /login dédiée + correction redirect auth
- Test moteurs IA: vrai HEAD request avec latence
- Suppression spam logs [Auth] Missing session cookie
- Fix fuite passwordHash dans auth.me
- Cookie sameSite: none -> lax (CSRF)

Sécurité:
- Endpoints admin protégés par adminProcedure (role=admin requis)
- Sidebar admin masquée pour non-admins
- AdminPanel: page accès refusé pour non-admins
- Bootstrap admin optimisé (skip rehash si identique)

Fonctionnalités:
- Export vidéo MP4 réel via ffmpeg local (H.264 + AAC audio)
- Download parallèle par batch de 20 (export 10x plus rapide)
- Détection de scènes réelle via ffmpeg scene detect
- Analyse arrière-plans via Gemini Vision (remplace Math.random)
- Gemini: conservation du role system + support image_url
- Suppression thinking.budget_tokens:128 (LLM config)
- Thumbnails de frames dans la timeline
- Toast export avec bouton télécharger
- Endpoint extraction audio à la demande

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 01:37:08 +00:00

348 lines
12 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from "vitest";
import { appRouter } from "./routers";
import { COOKIE_NAME } from "../shared/const";
import type { TrpcContext } from "./_core/context";
// Mock the db module
vi.mock("./db", () => ({
listProjects: vi.fn().mockResolvedValue([
{ id: 1, name: "Test Project", status: "ready", createdAt: new Date() },
]),
getProject: vi.fn().mockResolvedValue({
id: 1,
name: "Test Project",
status: "ready",
totalFrames: 240,
fps: 24,
width: 1920,
height: 1080,
createdAt: new Date(),
updatedAt: new Date(),
}),
createProject: vi.fn().mockResolvedValue({ id: 2 }),
updateProject: vi.fn().mockResolvedValue(undefined),
listSequences: vi.fn().mockResolvedValue([
{ id: 1, projectId: 1, startFrame: 0, endFrame: 60, isStaticBackground: true, status: "detected" },
]),
createSequence: vi.fn().mockResolvedValue({ id: 1 }),
updateSequence: vi.fn().mockResolvedValue(undefined),
listLayers: vi.fn().mockResolvedValue([
{ id: 1, projectId: 1, name: "Background", type: "background", visible: true, locked: false, opacity: 100 },
]),
createLayer: vi.fn().mockResolvedValue({ id: 1 }),
updateLayer: vi.fn().mockResolvedValue(undefined),
deleteLayer: vi.fn().mockResolvedValue(undefined),
listCharacters: vi.fn().mockResolvedValue([
{ id: 1, projectId: 1, name: "Hero", modelType: "lora", color: "#4a9eff" },
]),
createCharacter: vi.fn().mockResolvedValue({ id: 1 }),
updateCharacter: vi.fn().mockResolvedValue(undefined),
listAiEngines: vi.fn().mockResolvedValue([
{ id: 1, name: "SDXL", provider: "stability-ai", taskType: "background_generation", isActive: true, isDefault: true },
]),
createAiEngine: vi.fn().mockResolvedValue({ id: 1 }),
updateAiEngine: vi.fn().mockResolvedValue(undefined),
deleteAiEngine: vi.fn().mockResolvedValue(undefined),
getDb: vi.fn().mockResolvedValue(null),
listGenerationJobs: vi.fn().mockResolvedValue([]),
createGenerationJob: vi.fn().mockResolvedValue({ id: 1 }),
listAssistantMessages: vi.fn().mockResolvedValue([
{ id: 1, projectId: 1, role: "assistant", content: "Bienvenue", createdAt: new Date() },
]),
createAssistantMessage: vi.fn().mockResolvedValue(undefined),
}));
// Mock LLM
vi.mock("./_core/llm", () => ({
invokeLLM: vi.fn().mockResolvedValue({
choices: [{ message: { content: "Réponse de l'assistant IA." } }],
}),
}));
// Mock image generation
vi.mock("./_core/imageGeneration", () => ({
generateImage: vi.fn().mockResolvedValue({ url: "/manus-storage/generated/test-result.png" }),
}));
type CookieCall = { name: string; options: Record<string, unknown> };
type AuthenticatedUser = NonNullable<TrpcContext["user"]>;
function createAuthContext(role: "user" | "admin" = "user"): { ctx: TrpcContext; clearedCookies: CookieCall[] } {
const clearedCookies: CookieCall[] = [];
const user: AuthenticatedUser = {
id: 1,
openId: "test-user-open-id",
email: "test@example.com",
name: "Test User",
loginMethod: "manus",
role,
createdAt: new Date(),
updatedAt: new Date(),
lastSignedIn: new Date(),
};
const ctx: TrpcContext = {
user,
req: { protocol: "https", headers: {} } as TrpcContext["req"],
res: {
clearCookie: (name: string, options: Record<string, unknown>) => {
clearedCookies.push({ name, options });
},
} as TrpcContext["res"],
};
return { ctx, clearedCookies };
}
function createUnauthContext(): TrpcContext {
return {
user: null,
req: { protocol: "https", headers: {} } as TrpcContext["req"],
res: {
clearCookie: () => {},
} as TrpcContext["res"],
};
}
describe("auth.logout", () => {
it("clears the session cookie and reports success", async () => {
const { ctx, clearedCookies } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.auth.logout();
expect(result).toEqual({ success: true });
expect(clearedCookies).toHaveLength(1);
expect(clearedCookies[0]?.name).toBe(COOKIE_NAME);
});
});
describe("projects", () => {
it("lists projects for authenticated user", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.projects.list();
expect(Array.isArray(result)).toBe(true);
expect(result[0]).toHaveProperty("name", "Test Project");
});
it("gets a project by id", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.projects.get({ id: 1 });
expect(result).toHaveProperty("name", "Test Project");
expect(result).toHaveProperty("totalFrames", 240);
expect(result).toHaveProperty("fps", 24);
});
it("creates a new project", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.projects.create({
name: "New Animation",
description: "Test description",
sourceVideoUrl: "/manus-storage/video.mp4",
});
expect(result).toHaveProperty("id", 2);
});
it("updates project status", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.projects.updateStatus({ id: 1, status: "ready" });
expect(result).toEqual({ success: true });
});
});
describe("sequences", () => {
it("lists sequences for a project", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.sequences.list({ projectId: 1 });
expect(Array.isArray(result)).toBe(true);
expect(result[0]).toHaveProperty("startFrame", 0);
expect(result[0]).toHaveProperty("endFrame", 60);
});
it("creates a new sequence", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.sequences.create({
projectId: 1,
startFrame: 0,
endFrame: 120,
isStaticBackground: true,
referenceFrameIndex: 30,
});
expect(result).toHaveProperty("id", 1);
});
});
describe("layers", () => {
it("lists layers for a project", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.layers.list({ projectId: 1 });
expect(Array.isArray(result)).toBe(true);
expect(result[0]).toHaveProperty("name", "Background");
expect(result[0]).toHaveProperty("type", "background");
});
it("creates a new layer", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.layers.create({
sequenceId: 1,
projectId: 1,
name: "Character Layer",
type: "character",
order: 1,
});
expect(result).toHaveProperty("id", 1);
});
it("deletes a layer", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.layers.delete({ id: 1 });
expect(result).toEqual({ success: true });
});
});
describe("characters", () => {
it("lists characters for a project", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.characters.list({ projectId: 1 });
expect(Array.isArray(result)).toBe(true);
expect(result[0]).toHaveProperty("name", "Hero");
});
it("creates a new character", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.characters.create({
projectId: 1,
name: "Villain",
description: "The antagonist",
color: "#ff4a4a",
modelType: "lora",
});
expect(result).toHaveProperty("id", 1);
});
});
describe("admin - AI engines", () => {
it("lists AI engines", async () => {
const { ctx } = createAuthContext("admin");
const caller = appRouter.createCaller(ctx);
const result = await caller.admin.listEngines();
expect(Array.isArray(result)).toBe(true);
expect(result[0]).toHaveProperty("name", "SDXL");
expect(result[0]).toHaveProperty("provider", "stability-ai");
});
it("creates a new AI engine", async () => {
const { ctx } = createAuthContext("admin");
const caller = appRouter.createCaller(ctx);
const result = await caller.admin.createEngine({
name: "SAM 2",
provider: "meta",
taskType: "segmentation",
modelName: "sam2-large",
});
expect(result).toHaveProperty("id", 1);
});
it("tests an AI engine", async () => {
const { ctx } = createAuthContext("admin");
const caller = appRouter.createCaller(ctx);
const result = await caller.admin.testEngine({ id: 1 });
expect(result).toHaveProperty("success");
});
it("deletes an AI engine", async () => {
const { ctx } = createAuthContext("admin");
const caller = appRouter.createCaller(ctx);
const result = await caller.admin.deleteEngine({ id: 1 });
expect(result).toEqual({ success: true });
});
});
describe("assistant", () => {
it("gets messages for a project", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.assistant.getMessages({ projectId: 1 });
expect(Array.isArray(result)).toBe(true);
expect(result[0]).toHaveProperty("role", "assistant");
});
it("sends a message and gets AI response", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.assistant.sendMessage({
projectId: 1,
content: "Détecte les scènes de cette vidéo",
});
expect(result).toEqual({ success: true });
});
it("runs auto compose action", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.assistant.runAutoCompose({
projectId: 1,
action: "detect_scenes",
});
expect(result).toHaveProperty("success");
expect(result).toHaveProperty("message");
});
});
describe("admin - test generation & config", () => {
it("runs a test generation", async () => {
const { ctx } = createAuthContext("admin");
const caller = appRouter.createCaller(ctx);
const result = await caller.admin.runTestGeneration({
taskType: "background_generation",
prompt: "A forest in Ghibli style",
});
expect(result).toHaveProperty("success", true);
expect(result).toHaveProperty("resultUrl");
});
it("saves a config value", async () => {
const { ctx } = createAuthContext("admin");
const caller = appRouter.createCaller(ctx);
const result = await caller.admin.saveConfig({
key: "llm_config",
value: JSON.stringify({ model: "built-in", behavior: "guided" }),
});
expect(result).toEqual({ success: true });
});
it("gets a config value", async () => {
const { ctx } = createAuthContext("admin");
const caller = appRouter.createCaller(ctx);
const result = await caller.admin.getConfig({ key: "llm_config" });
expect(result).toBeNull();
});
});
describe("generation jobs", () => {
it("lists generation jobs for a project", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.jobs.list({ projectId: 1 });
expect(Array.isArray(result)).toBe(true);
});
it("creates a generation job", async () => {
const { ctx } = createAuthContext();
const caller = appRouter.createCaller(ctx);
const result = await caller.jobs.create({
projectId: 1,
type: "background_generation",
prompt: "A lush forest background in Studio Ghibli style",
isTestMode: true,
});
expect(result).toHaveProperty("id", 1);
});
});