fix: close SVG sanitization gap on upload routes and fix OCR/extension bugs

Security:
- Apply sanitizeSvg() to all file upload routes (files.ts, user-files.ts)
  preventing SSRF and script injection via SVG uploads to file library

Functional:
- Handle PaddleOCR-VL 1.5 markdown_texts output format in ocr.py
- Add empty-text fallback in OCR tier chain (ocr.ts) so higher tiers
  that return empty text fall back to the next tier automatically
- Fix SVG->PNG filename extension mismatch in tool-factory.ts so
  download endpoint serves correct Content-Type
- Report original upload size (not decoded size) in API response

Test infrastructure:
- Move Playwright auth state from test-results/ to .playwright/ to
  prevent mid-run cleanup deleting auth files
- Fix auth.setup.ts navigation race with waitForURL
- Fix gui-batch.spec.ts regex matching "Presets" instead of "reset"
- Fix pipeline-advanced.spec.ts crop bounds and resize assertions
- Broaden pipeline cleanup to include all E2E-prefixed pipelines
This commit is contained in:
SnapOtter
2026-04-30 16:11:33 +08:00
parent fc8b549d78
commit b00ef20667
13 changed files with 77 additions and 18 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ import { mkdirSync } from "node:fs";
import path from "node:path";
import { expect, test as setup } from "@playwright/test";
const authFile = path.join(__dirname, "..", "..", "test-results", ".auth", "analytics-user.json");
const authFile = path.join(__dirname, "..", "..", ".playwright", ".auth", "analytics-user.json");
setup("authenticate", async ({ page }) => {
await page.goto("/login");
+2 -2
View File
@@ -375,7 +375,7 @@ test.describe("Deep pipelines (6+ steps)", () => {
steps: [
{ toolId: "strip-metadata", settings: {} },
{ toolId: "rotate", settings: { angle: 90 } },
{ toolId: "resize", settings: { width: 800, fit: "contain" } },
{ toolId: "resize", settings: { width: 400, fit: "contain" } },
{ toolId: "adjust-colors", settings: { brightness: 5, contrast: 10, saturation: -5 } },
{ toolId: "sharpening", settings: { sigma: 1.0 } },
{
@@ -434,7 +434,7 @@ test.describe("Workflow: e-commerce product pipeline", () => {
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
pipeline: JSON.stringify({
steps: [
{ toolId: "crop", settings: { left: 50, top: 50, width: 400, height: 400 } },
{ toolId: "crop", settings: { left: 50, top: 50, width: 400, height: 350 } },
{ toolId: "resize", settings: { width: 800, height: 800, fit: "contain" } },
{ toolId: "image-enhancement", settings: { preset: "vivid" } },
{
+6 -3
View File
@@ -2,7 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import { expect, test as setup } from "@playwright/test";
const authFile = path.join(process.cwd(), "test-results", ".auth", "user.json");
const authFile = path.join(process.cwd(), ".playwright", ".auth", "user.json");
setup("authenticate", async ({ page }) => {
// Ensure directory exists
@@ -28,8 +28,11 @@ setup("authenticate", async ({ page }) => {
});
// Now navigate to "/" - consent guard is satisfied
await page.goto("/");
await expect(page).toHaveURL("/");
// Use waitUntil: "domcontentloaded" to avoid racing with client-side redirects
await page.goto("/", { waitUntil: "domcontentloaded" });
// Wait for the URL to settle (app may redirect through consent/auth guards)
await page.waitForURL((url) => url.pathname === "/", { timeout: 30_000 }).catch(() => {});
await page.waitForLoadState("load");
// Save storage state (includes localStorage with the token)
await page.context().storageState({ path: authFile });
+1 -1
View File
@@ -343,7 +343,7 @@ test.describe("Batch processing", () => {
await expect(page.getByText("Files (2)")).toBeVisible();
// Click undo (resets all processed state for all entries in the store)
const undoBtn = page.getByRole("button", { name: /undo|reset/i });
const undoBtn = page.getByRole("button", { name: /^undo$|^reset$/i });
if (await undoBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
await undoBtn.click();
await page.waitForTimeout(500);
+2 -2
View File
@@ -273,8 +273,8 @@ test.describe("Pipeline Builder - Save/Load", () => {
headers: { Authorization: `Bearer ${token}` },
});
const { pipelines } = await listRes.json();
for (const p of pipelines.filter((p: { name: string }) =>
p.name.startsWith("GUI E2E Pipeline"),
for (const p of pipelines.filter(
(p: { name: string }) => p.name.startsWith("GUI E2E Pipeline") || p.name.startsWith("E2E "),
)) {
await fetch(`${apiUrl}/api/v1/pipeline/${p.id}`, {
method: "DELETE",