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>
348 lines
12 KiB
TypeScript
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);
|
|
});
|
|
});
|