mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
A release-readiness QA pass over the whole product. The commits split into defects a user would hit and gates that were reporting green while measuring nothing. ## Fixes that change behaviour Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so request.ip came from a client-set header and a forged X-Forwarded-For got past the login limiter. The default is now a private-network trust list. A transient Postgres outage stranded in-flight jobs, leaving finished output on disk with no row pointing at it. A reconciler now resolves those rows and adopts the bytes rather than dropping the work. A Redis connection that moved to a new address wedged every read-blocked consumer, so completions stopped signalling while health still answered 200. Socket timeouts plus subscriber pings recover it. Installing more than one AI bundle left the shared venv multi-versioned and silently broke three tools. The installer now reconciles distributions to one version each. Converting an image to JXL at quality 1 through 4 returned a 500, because libjxl 0.7 rejects the distance those values compute. The quality is floored at what the encoder honours. A missing ffmpeg was also reported to the user as a corrupt upload; it now says the engine is unavailable. RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at 0.22.2, and the release scan was split so it can fail on an unfixed critical instead of hiding it behind ignore-unfixed. ## Gates that could not fail Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs build; coverage discarded its whole report on any failing test; the lint gate skipped root tests, scripts, and two workspaces; and several generated matrices counted a host missing ffmpeg as a passing tool. Each now measures what it claims. Full evidence and the outstanding release items are tracked locally and are not part of this branch.
627 lines
23 KiB
TypeScript
627 lines
23 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
// ─── Exotic Format Inputs ────────────────────────────────────────
|
|
// Tests that exercise less common input formats (TIFF, AVIF, PSD,
|
|
// GIF, BMP, SVG) through core processing tools. Verifies the
|
|
// format decode path works end-to-end in the Docker container.
|
|
|
|
const FIXTURES = join(process.cwd(), "tests", "fixtures", "image", "valid");
|
|
const FORMATS = join(process.cwd(), "tests", "fixtures", "image", "formats");
|
|
const CONTENT = FIXTURES;
|
|
|
|
let token: string;
|
|
|
|
test.beforeAll(async ({ request }) => {
|
|
const res = await request.post("/api/auth/login", {
|
|
data: { username: "admin", password: "admin" },
|
|
});
|
|
const body = await res.json();
|
|
token = body.token;
|
|
});
|
|
|
|
function formatFixture(name: string): Buffer {
|
|
return readFileSync(join(FORMATS, name));
|
|
}
|
|
|
|
function contentFixture(name: string): Buffer {
|
|
return readFileSync(join(CONTENT, name));
|
|
}
|
|
|
|
function fixture(name: string): Buffer {
|
|
return readFileSync(join(FIXTURES, name));
|
|
}
|
|
|
|
const TIFF_SAMPLE = formatFixture("sample.tiff");
|
|
const AVIF_SAMPLE = formatFixture("sample.avif");
|
|
const GIF_SAMPLE = formatFixture("sample.gif");
|
|
const SVG_SAMPLE = formatFixture("sample.svg");
|
|
|
|
// ─── TIFF Through Core Tools ─────────────────────────────────────
|
|
|
|
test.describe("TIFF input through core tools", () => {
|
|
test("resize TIFF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/resize", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: TIFF_SAMPLE },
|
|
settings: JSON.stringify({ width: 200, fit: "contain" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("crop TIFF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/crop", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: TIFF_SAMPLE },
|
|
settings: JSON.stringify({ left: 10, top: 10, width: 100, height: 100 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("rotate TIFF image 90 degrees", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/rotate", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: TIFF_SAMPLE },
|
|
settings: JSON.stringify({ angle: 90 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("sharpen TIFF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/sharpening", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: TIFF_SAMPLE },
|
|
settings: JSON.stringify({ sigma: 1.5 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("adjust colors on TIFF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/adjust-colors", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: TIFF_SAMPLE },
|
|
settings: JSON.stringify({ brightness: 15, contrast: 10 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("color palette from TIFF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/color-palette", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: TIFF_SAMPLE },
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.colors).toBeInstanceOf(Array);
|
|
expect(body.colors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("border on TIFF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/border", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: TIFF_SAMPLE },
|
|
settings: JSON.stringify({ size: 15, color: "#336699" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
// ─── AVIF Through Core Tools ─────────────────────────────────────
|
|
|
|
test.describe("AVIF input through core tools", () => {
|
|
test("resize AVIF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/resize", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.avif", mimeType: "image/avif", buffer: AVIF_SAMPLE },
|
|
settings: JSON.stringify({ width: 150, fit: "contain" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("crop AVIF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/crop", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.avif", mimeType: "image/avif", buffer: AVIF_SAMPLE },
|
|
settings: JSON.stringify({ left: 5, top: 5, width: 50, height: 50 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("compress AVIF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/compress", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.avif", mimeType: "image/avif", buffer: AVIF_SAMPLE },
|
|
settings: JSON.stringify({ quality: 40 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("convert AVIF to PNG", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/convert", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.avif", mimeType: "image/avif", buffer: AVIF_SAMPLE },
|
|
settings: JSON.stringify({ format: "png" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toContain(".png");
|
|
});
|
|
|
|
test("convert AVIF to JPEG", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/convert", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.avif", mimeType: "image/avif", buffer: AVIF_SAMPLE },
|
|
settings: JSON.stringify({ format: "jpg", quality: 80 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toContain(".jpg");
|
|
});
|
|
|
|
test("info on AVIF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/info", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.avif", mimeType: "image/avif", buffer: AVIF_SAMPLE },
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.width).toBeGreaterThan(0);
|
|
expect(body.height).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("watermark text on AVIF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/watermark-text", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.avif", mimeType: "image/avif", buffer: AVIF_SAMPLE },
|
|
settings: JSON.stringify({
|
|
text: "AVIF WM",
|
|
fontSize: 20,
|
|
color: "#ffffff",
|
|
opacity: 50,
|
|
position: "center",
|
|
}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
// ─── GIF Through Core Tools ──────────────────────────────────────
|
|
|
|
test.describe("GIF input through core tools", () => {
|
|
test("resize GIF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/resize", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.gif", mimeType: "image/gif", buffer: GIF_SAMPLE },
|
|
settings: JSON.stringify({ width: 100, fit: "contain" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("convert GIF to PNG", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/convert", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.gif", mimeType: "image/gif", buffer: GIF_SAMPLE },
|
|
settings: JSON.stringify({ format: "png" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toContain(".png");
|
|
});
|
|
|
|
test("convert GIF to WebP", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/convert", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.gif", mimeType: "image/gif", buffer: GIF_SAMPLE },
|
|
settings: JSON.stringify({ format: "webp" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toContain(".webp");
|
|
});
|
|
|
|
test("info on GIF image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/info", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.gif", mimeType: "image/gif", buffer: GIF_SAMPLE },
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.width).toBeGreaterThan(0);
|
|
expect(body.height).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ─── PSD Through Core Tools ──────────────────────────────────────
|
|
|
|
test.describe("PSD input through core tools", () => {
|
|
test("info on PSD image", async ({ request }) => {
|
|
const psd = formatFixture("sample.psd");
|
|
const res = await request.post("/api/v1/tools/image/info", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.psd", mimeType: "image/vnd.adobe.photoshop", buffer: psd },
|
|
},
|
|
});
|
|
// PSD support depends on Sharp build; accept success or format error
|
|
if (res.ok()) {
|
|
const body = await res.json();
|
|
expect(body.width).toBeGreaterThan(0);
|
|
expect(body.height).toBeGreaterThan(0);
|
|
} else {
|
|
expect([400, 422]).toContain(res.status());
|
|
}
|
|
});
|
|
|
|
test("resize PSD image", async ({ request }) => {
|
|
const psd = formatFixture("sample.psd");
|
|
const res = await request.post("/api/v1/tools/image/resize", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.psd", mimeType: "image/vnd.adobe.photoshop", buffer: psd },
|
|
settings: JSON.stringify({ width: 200, fit: "contain" }),
|
|
},
|
|
});
|
|
// PSD decode may not be available
|
|
if (res.ok()) {
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
} else {
|
|
expect([400, 422]).toContain(res.status());
|
|
}
|
|
});
|
|
|
|
test("convert PSD to PNG", async ({ request }) => {
|
|
const psd = formatFixture("sample.psd");
|
|
const res = await request.post("/api/v1/tools/image/convert", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.psd", mimeType: "image/vnd.adobe.photoshop", buffer: psd },
|
|
settings: JSON.stringify({ format: "png" }),
|
|
},
|
|
});
|
|
if (res.ok()) {
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toContain(".png");
|
|
} else {
|
|
expect([400, 422]).toContain(res.status());
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── SVG Through Extended Tools ──────────────────────────────────
|
|
|
|
test.describe("SVG input through extended tools", () => {
|
|
test("SVG to raster at multiple widths", async ({ request }) => {
|
|
const widths = [64, 256, 512, 1024] as const;
|
|
for (const width of widths) {
|
|
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.svg", mimeType: "image/svg+xml", buffer: SVG_SAMPLE },
|
|
settings: JSON.stringify({ format: "png", width }),
|
|
},
|
|
});
|
|
expect(res.ok(), `SVG to raster at width=${width} should succeed`).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test("SVG to raster as JPEG", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.svg", mimeType: "image/svg+xml", buffer: SVG_SAMPLE },
|
|
settings: JSON.stringify({ format: "jpg", width: 400 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("SVG to raster as AVIF", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.svg", mimeType: "image/svg+xml", buffer: SVG_SAMPLE },
|
|
settings: JSON.stringify({ format: "avif", width: 300 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("color palette from SVG input", async ({ request }) => {
|
|
const svgLogo = contentFixture("svg-logo.svg");
|
|
const res = await request.post("/api/v1/tools/image/color-palette", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "logo.svg", mimeType: "image/svg+xml", buffer: svgLogo },
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.colors).toBeInstanceOf(Array);
|
|
expect(body.colors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("beautify SVG input", async ({ request }) => {
|
|
const svg100 = fixture("test-100x100.svg");
|
|
const res = await request.post("/api/v1/tools/image/beautify", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: svg100 },
|
|
settings: JSON.stringify({
|
|
frame: "browser-light",
|
|
frameTitle: "design.svg",
|
|
padding: 40,
|
|
borderRadius: 8,
|
|
}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
// ─── HEIF (non-HEIC) Through Core Tools ──────────────────────────
|
|
|
|
test.describe("HEIF input through core tools", () => {
|
|
test("info on HEIF image", async ({ request }) => {
|
|
const heif = contentFixture("motorcycle.heif");
|
|
const res = await request.post("/api/v1/tools/image/info", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "motorcycle.heif", mimeType: "image/heif", buffer: heif },
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.width).toBeGreaterThan(0);
|
|
expect(body.height).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("resize HEIF image", async ({ request }) => {
|
|
const heif = contentFixture("motorcycle.heif");
|
|
const res = await request.post("/api/v1/tools/image/resize", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "motorcycle.heif", mimeType: "image/heif", buffer: heif },
|
|
settings: JSON.stringify({ width: 200, fit: "contain" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("convert HEIF to JPEG", async ({ request }) => {
|
|
const heif = contentFixture("motorcycle.heif");
|
|
const res = await request.post("/api/v1/tools/image/convert", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "motorcycle.heif", mimeType: "image/heif", buffer: heif },
|
|
settings: JSON.stringify({ format: "jpg", quality: 85 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toContain(".jpg");
|
|
});
|
|
|
|
test("convert HEIF to WebP", async ({ request }) => {
|
|
const heif = contentFixture("motorcycle.heif");
|
|
const res = await request.post("/api/v1/tools/image/convert", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "motorcycle.heif", mimeType: "image/heif", buffer: heif },
|
|
settings: JSON.stringify({ format: "webp" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toContain(".webp");
|
|
});
|
|
|
|
test("color palette from HEIF image", async ({ request }) => {
|
|
const heif = contentFixture("motorcycle.heif");
|
|
const res = await request.post("/api/v1/tools/image/color-palette", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "motorcycle.heif", mimeType: "image/heif", buffer: heif },
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.colors).toBeInstanceOf(Array);
|
|
expect(body.colors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("adjust colors on HEIF image", async ({ request }) => {
|
|
const heif = contentFixture("motorcycle.heif");
|
|
const res = await request.post("/api/v1/tools/image/adjust-colors", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "motorcycle.heif", mimeType: "image/heif", buffer: heif },
|
|
settings: JSON.stringify({ brightness: 10, contrast: 15, saturation: -5 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
// ─── Multipage TIFF ──────────────────────────────────────────────
|
|
|
|
test.describe("Multipage TIFF", () => {
|
|
test("info on multipage TIFF", async ({ request }) => {
|
|
const multiTiff = formatFixture("multipage.tiff");
|
|
const res = await request.post("/api/v1/tools/image/info", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "multipage.tiff", mimeType: "image/tiff", buffer: multiTiff },
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.width).toBeGreaterThan(0);
|
|
expect(body.height).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("convert multipage TIFF to PNG", async ({ request }) => {
|
|
const multiTiff = formatFixture("multipage.tiff");
|
|
const res = await request.post("/api/v1/tools/image/convert", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "multipage.tiff", mimeType: "image/tiff", buffer: multiTiff },
|
|
settings: JSON.stringify({ format: "png" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toContain(".png");
|
|
});
|
|
});
|
|
|
|
// ─── Animated GIF Through Core Tools ─────────────────────────────
|
|
|
|
test.describe("Animated GIF through core tools", () => {
|
|
test("info on animated GIF", async ({ request }) => {
|
|
const animated = fixture("animated.gif");
|
|
const res = await request.post("/api/v1/tools/image/info", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "animated.gif", mimeType: "image/gif", buffer: animated },
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.width).toBeGreaterThan(0);
|
|
expect(body.height).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("resize animated GIF (first frame)", async ({ request }) => {
|
|
const animated = fixture("animated.gif");
|
|
const res = await request.post("/api/v1/tools/image/resize", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "animated.gif", mimeType: "image/gif", buffer: animated },
|
|
settings: JSON.stringify({ width: 50, fit: "contain" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("convert animated GIF to PNG (first frame)", async ({ request }) => {
|
|
const animated = fixture("animated.gif");
|
|
const res = await request.post("/api/v1/tools/image/convert", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "animated.gif", mimeType: "image/gif", buffer: animated },
|
|
settings: JSON.stringify({ format: "png" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toContain(".png");
|
|
});
|
|
});
|
|
|
|
// ─── Cross-Format Conversion Matrix ──────────────────────────────
|
|
|
|
test.describe("Cross-format conversion matrix", () => {
|
|
const conversions = [
|
|
{ from: "TIFF", file: "sample.tiff", mime: "image/tiff", to: "webp" },
|
|
{ from: "TIFF", file: "sample.tiff", mime: "image/tiff", to: "avif" },
|
|
{ from: "TIFF", file: "sample.tiff", mime: "image/tiff", to: "jpg" },
|
|
{ from: "AVIF", file: "sample.avif", mime: "image/avif", to: "tiff" },
|
|
{ from: "AVIF", file: "sample.avif", mime: "image/avif", to: "gif" },
|
|
{ from: "GIF", file: "sample.gif", mime: "image/gif", to: "avif" },
|
|
{ from: "GIF", file: "sample.gif", mime: "image/gif", to: "tiff" },
|
|
] as const;
|
|
|
|
for (const { from, file, mime, to } of conversions) {
|
|
test(`convert ${from} to ${to}`, async ({ request }) => {
|
|
const buffer = formatFixture(file);
|
|
const res = await request.post("/api/v1/tools/image/convert", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: file, mimeType: mime, buffer },
|
|
settings: JSON.stringify({ format: to }),
|
|
},
|
|
});
|
|
expect(res.ok(), `${from} to ${to} should succeed`).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toContain(`.${to}`);
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
}
|
|
});
|