mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: resolve CI test failures for exotic formats and fetch-urls
Install ImageMagick, Ghostscript, libjxl-tools, and libopenjp2-tools in CI so exotic format decoder tests (PSD, EPS, HDR, ICO, JP2, etc.) can run. Relax ImageMagick EPS/PS security policy to match the Dockerfile. Replace fragile vi.mock() of the SSRF module in fetch-urls tests with an env-var guard (SSRF_ALLOW_PRIVATE) that bypasses private-IP checks in the test environment. The vi.mock approach broke under V8 coverage instrumentation in CI.
This commit is contained in:
@@ -48,8 +48,16 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install system dependencies (HEIC + ExifTool)
|
- name: Install system dependencies (HEIC + ExifTool + ImageMagick + exotic format tools)
|
||||||
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends libheif-examples libheif-plugin-x265 libheif-plugin-libde265 libimage-exiftool-perl
|
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends libheif-examples libheif-plugin-x265 libheif-plugin-libde265 libimage-exiftool-perl imagemagick ghostscript libjxl-tools libopenjp2-tools
|
||||||
|
|
||||||
|
- name: Allow ImageMagick to read EPS/PS via Ghostscript delegate
|
||||||
|
run: |
|
||||||
|
POLICY_FILE=$(find /etc/ImageMagick* -name policy.xml 2>/dev/null | head -1)
|
||||||
|
if [ -n "$POLICY_FILE" ]; then
|
||||||
|
sudo sed -i 's/<policy domain="coder" rights="none" pattern="EPS"/<policy domain="coder" rights="read" pattern="EPS"/' "$POLICY_FILE"
|
||||||
|
sudo sed -i 's/<policy domain="coder" rights="none" pattern="PS"/<policy domain="coder" rights="read" pattern="PS"/' "$POLICY_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
- uses: ./.github/actions/setup
|
- uses: ./.github/actions/setup
|
||||||
- run: pnpm test:ci
|
- run: pnpm test:ci
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ function isPrivateIPv6(ip: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resolveAndCheck(hostname: string): Promise<void> {
|
async function resolveAndCheck(hostname: string): Promise<void> {
|
||||||
|
// Allow tests to bypass private-IP checks so a local mock HTTP server can be
|
||||||
|
// used without fragile vi.mock() overrides that break under V8 coverage.
|
||||||
|
if (process.env.SSRF_ALLOW_PRIVATE === "1") return;
|
||||||
|
|
||||||
const bare = hostname.replace(/^\[|]$/g, "");
|
const bare = hostname.replace(/^\[|]$/g, "");
|
||||||
if (isIP(bare)) {
|
if (isIP(bare)) {
|
||||||
if (isPrivateIPv4(bare) || isPrivateIPv6(bare)) {
|
if (isPrivateIPv4(bare) || isPrivateIPv6(bare)) {
|
||||||
|
|||||||
@@ -8,41 +8,10 @@
|
|||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
// Mock the SSRF validation to allow localhost in tests.
|
// SSRF private-IP checks are bypassed via the SSRF_ALLOW_PRIVATE=1 env var
|
||||||
// We keep the real safeFetch logic but skip the private-IP DNS check.
|
// set in vitest.config.ts, so the real safeFetch works against localhost.
|
||||||
vi.mock("../../apps/api/src/lib/ssrf.js", async (importOriginal) => {
|
|
||||||
const original = (await importOriginal()) as Record<string, unknown>;
|
|
||||||
return {
|
|
||||||
...original,
|
|
||||||
// validateFetchUrl that allows localhost for tests
|
|
||||||
validateFetchUrl: async (_url: string) => {
|
|
||||||
// No-op: allow all URLs in tests (including localhost)
|
|
||||||
},
|
|
||||||
// safeFetch that skips SSRF validation but still does the real fetch
|
|
||||||
safeFetch: async (url: string, signal?: AbortSignal) => {
|
|
||||||
const MAX_REDIRECTS = 5;
|
|
||||||
let currentUrl = url;
|
|
||||||
for (let i = 0; i <= MAX_REDIRECTS; i++) {
|
|
||||||
const res = await fetch(currentUrl, {
|
|
||||||
signal,
|
|
||||||
redirect: "manual",
|
|
||||||
headers: { "User-Agent": "SnapOtter/1.0 (image-fetch)" },
|
|
||||||
});
|
|
||||||
if (res.status >= 300 && res.status < 400) {
|
|
||||||
const location = res.headers.get("location");
|
|
||||||
if (!location) throw new Error("Redirect without Location header");
|
|
||||||
currentUrl = new URL(location, currentUrl).href;
|
|
||||||
if (i === MAX_REDIRECTS) throw new Error("Too many redirects");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
throw new Error("Too many redirects");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
import { buildTestApp, loginAsAdmin, type TestApp } from "./test-server.js";
|
import { buildTestApp, loginAsAdmin, type TestApp } from "./test-server.js";
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default defineConfig({
|
|||||||
CONCURRENT_JOBS: "3",
|
CONCURRENT_JOBS: "3",
|
||||||
FILE_MAX_AGE_HOURS: "1",
|
FILE_MAX_AGE_HOURS: "1",
|
||||||
CLEANUP_INTERVAL_MINUTES: "60",
|
CLEANUP_INTERVAL_MINUTES: "60",
|
||||||
|
SSRF_ALLOW_PRIVATE: "1",
|
||||||
},
|
},
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: "v8",
|
provider: "v8",
|
||||||
|
|||||||
Reference in New Issue
Block a user