/** * Integration tests for the restore-photo tool (/api/v1/tools/restore-photo). * * This tool requires the Python sidecar (LaMa / Real-ESRGAN / face enhancement). * Tests accept both 200 (sidecar running) and 501 (not installed) for the * processing path while fully testing validation paths. */ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { buildTestApp, createMultipartPayload, loginAsAdmin, type TestApp } from "./test-server.js"; const FIXTURES = join(__dirname, "..", "fixtures"); const PNG = readFileSync(join(FIXTURES, "test-200x150.png")); const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg")); const HEIC = readFileSync(join(FIXTURES, "test-200x150.heic")); const TINY = readFileSync(join(FIXTURES, "test-1x1.png")); let testApp: TestApp; let app: TestApp["app"]; let adminToken: string; beforeAll(async () => { testApp = await buildTestApp(); app = testApp.app; adminToken = await loginAsAdmin(app); }, 30_000); afterAll(async () => { await testApp.cleanup(); }, 10_000); describe("Restore Photo", () => { // ── Processing (AI-dependent) ──────────────────────────────────── it("route exists and responds to POST", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG }, { name: "settings", content: JSON.stringify({}) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([202, 501]).toContain(res.statusCode); }, 60_000); it("accepts default settings", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG }, { name: "settings", content: JSON.stringify({}) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([202, 501]).toContain(res.statusCode); if (res.statusCode === 202) { const result = JSON.parse(res.body); expect(result.jobId).toBeDefined(); expect(result.async).toBe(true); } if (res.statusCode === 501) { const result = JSON.parse(res.body); expect(result.code).toBe("FEATURE_NOT_INSTALLED"); } }, 60_000); it("accepts auto mode with all features enabled", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG }, { name: "settings", content: JSON.stringify({ mode: "auto", scratchRemoval: true, faceEnhancement: true, denoise: true, denoiseStrength: 40, }), }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([202, 501]).toContain(res.statusCode); }, 60_000); it("accepts heavy mode with colorize enabled", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG }, { name: "settings", content: JSON.stringify({ mode: "heavy", colorize: true, fidelity: 0.9, }), }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([202, 501]).toContain(res.statusCode); }, 60_000); it("accepts light mode with features disabled", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG }, { name: "settings", content: JSON.stringify({ mode: "light", scratchRemoval: false, faceEnhancement: false, denoise: false, }), }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([202, 501]).toContain(res.statusCode); }, 60_000); it("processes JPEG input", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "photo.jpg", contentType: "image/jpeg", content: JPG }, { name: "settings", content: JSON.stringify({}) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([202, 501]).toContain(res.statusCode); }, 60_000); it( "handles HEIC input", { timeout: 120_000 }, async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "photo.heic", contentType: "image/heic", content: HEIC }, { name: "settings", content: JSON.stringify({}) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([202, 501]).toContain(res.statusCode); }, 60_000, ); it("handles 1x1 pixel input", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "tiny.png", contentType: "image/png", content: TINY }, { name: "settings", content: JSON.stringify({}) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([202, 422, 501]).toContain(res.statusCode); }, 60_000); // ── Validation (always testable) ───────────────────────────────── it("rejects requests without a file", async () => { const { body, contentType } = createMultipartPayload([ { name: "settings", content: JSON.stringify({}) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([400, 501]).toContain(res.statusCode); if (res.statusCode === 400) { const result = JSON.parse(res.body); expect(result.error).toMatch(/no image/i); } }); it("rejects invalid settings JSON", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG }, { name: "settings", content: "{{invalid json" }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([400, 501]).toContain(res.statusCode); if (res.statusCode === 400) { const result = JSON.parse(res.body); expect(result.error).toMatch(/json/i); } }); it("rejects invalid mode value", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG }, { name: "settings", content: JSON.stringify({ mode: "turbo" }), }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([400, 501]).toContain(res.statusCode); if (res.statusCode === 400) { const result = JSON.parse(res.body); expect(result.error).toMatch(/invalid settings/i); } }); it("rejects fidelity out of range", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG }, { name: "settings", content: JSON.stringify({ fidelity: 5.0 }), }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([400, 501]).toContain(res.statusCode); if (res.statusCode === 400) { const result = JSON.parse(res.body); expect(result.error).toMatch(/invalid settings/i); } }); it("rejects denoiseStrength out of range", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG }, { name: "settings", content: JSON.stringify({ denoiseStrength: 150 }), }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([400, 501]).toContain(res.statusCode); if (res.statusCode === 400) { const result = JSON.parse(res.body); expect(result.error).toMatch(/invalid settings/i); } }); it("rejects unauthenticated requests", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG }, { name: "settings", content: JSON.stringify({}) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/restore-photo", headers: { "content-type": contentType }, body, }); expect(res.statusCode).toBe(401); }); });