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.
1208 lines
45 KiB
TypeScript
1208 lines
45 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
// ─── Optimization Tools ────────────────────────────────────────────
|
|
// Tests for: optimize-for-web (extended options), bulk-rename (patterns),
|
|
// favicon (multi-format output), image-to-pdf (page options),
|
|
// replace-color (tolerance edge cases)
|
|
// Complements adjustment-tools.spec.ts and conversion-tools.spec.ts
|
|
// with deeper coverage of optimization and utility tools.
|
|
|
|
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 fixture(name: string): Buffer {
|
|
return readFileSync(join(FIXTURES, name));
|
|
}
|
|
|
|
function formatFixture(name: string): Buffer {
|
|
return readFileSync(join(FORMATS, name));
|
|
}
|
|
|
|
function contentFixture(name: string): Buffer {
|
|
return readFileSync(join(CONTENT, name));
|
|
}
|
|
|
|
/**
|
|
* Build a raw multipart/form-data body for multi-file uploads.
|
|
*/
|
|
function buildMultipart(
|
|
files: Array<{ name: string; filename: string; contentType: string; buffer: Buffer }>,
|
|
fields: Array<{ name: string; value: string }>,
|
|
): { body: Buffer; contentType: string } {
|
|
const boundary = `----PlaywrightBoundary${Date.now()}`;
|
|
const parts: Buffer[] = [];
|
|
for (const file of files) {
|
|
parts.push(
|
|
Buffer.from(
|
|
`--${boundary}\r\nContent-Disposition: form-data; name="${file.name}"; filename="${file.filename}"\r\nContent-Type: ${file.contentType}\r\n\r\n`,
|
|
),
|
|
);
|
|
parts.push(file.buffer);
|
|
parts.push(Buffer.from("\r\n"));
|
|
}
|
|
for (const field of fields) {
|
|
parts.push(
|
|
Buffer.from(
|
|
`--${boundary}\r\nContent-Disposition: form-data; name="${field.name}"\r\n\r\n${field.value}\r\n`,
|
|
),
|
|
);
|
|
}
|
|
parts.push(Buffer.from(`--${boundary}--\r\n`));
|
|
return {
|
|
body: Buffer.concat(parts),
|
|
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
};
|
|
}
|
|
|
|
const PNG_200x150 = fixture("test-200x150.png");
|
|
const JPG_100x100 = fixture("test-100x100.jpg");
|
|
const WEBP_50x50 = fixture("test-50x50.webp");
|
|
const HEIC_200x150 = fixture("test-200x150.heic");
|
|
const JPG_SAMPLE = formatFixture("sample.jpg");
|
|
|
|
// ─── Optimize for Web — Extended ───────────────────────────────────
|
|
|
|
test.describe("Optimize for Web — extended", () => {
|
|
test("optimize with aggressive quality (20)", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({ maxWidth: 640, quality: 20 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("optimize with high quality (95) preserves detail", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({ maxWidth: 1920, quality: 95 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("optimize HEIC for web", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
|
settings: JSON.stringify({ maxWidth: 400, quality: 75 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("optimize WebP for web", async ({ request }) => {
|
|
const webpSample = formatFixture("sample.webp");
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.webp", mimeType: "image/webp", buffer: webpSample },
|
|
settings: JSON.stringify({ maxWidth: 600, quality: 60 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("optimize without maxWidth (no resize, only compress)", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({ quality: 50 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("optimize large content image for web", async ({ request }) => {
|
|
const stressImg = contentFixture("stress-large.jpg");
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "stress-large.jpg", mimeType: "image/jpeg", buffer: stressImg },
|
|
settings: JSON.stringify({ maxWidth: 1280, quality: 70 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ─── Bulk Rename — Extended Patterns ───────────────────────────────
|
|
|
|
test.describe("Bulk Rename — extended", () => {
|
|
test("rename with date-based pattern", async ({ request }) => {
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[
|
|
{ name: "file", filename: "photo1.png", contentType: "image/png", buffer: PNG_200x150 },
|
|
{ name: "file", filename: "photo2.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
|
{ name: "file", filename: "photo3.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ pattern: "2024-01-{{index}}" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/bulk-rename", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const resContentType = res.headers()["content-type"] ?? "";
|
|
if (resContentType.includes("application/json")) {
|
|
const json = await res.json();
|
|
expect(json.downloadUrl || json.files).toBeTruthy();
|
|
} else {
|
|
const buffer = Buffer.from(await res.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test("rename with padded index (starting at 001)", async ({ request }) => {
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[
|
|
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
|
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
|
],
|
|
[
|
|
{
|
|
name: "settings",
|
|
value: JSON.stringify({ pattern: "img-{{index}}", startIndex: 1 }),
|
|
},
|
|
],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/bulk-rename", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
|
|
test("rename 5 files preserving extensions", async ({ request }) => {
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[
|
|
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
|
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
|
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
{
|
|
name: "file",
|
|
filename: "d.heic",
|
|
contentType: "image/heic",
|
|
buffer: HEIC_200x150,
|
|
},
|
|
{
|
|
name: "file",
|
|
filename: "e.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: formatFixture("sample.jpg"),
|
|
},
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ pattern: "gallery-{{index}}" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/bulk-rename", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Favicon — Extended ────────────────────────────────────────────
|
|
|
|
test.describe("Favicon — extended", () => {
|
|
test("generate favicon from WebP", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/favicon", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const ct = res.headers()["content-type"] ?? "";
|
|
if (ct.includes("application/json")) {
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
} else {
|
|
const buffer = Buffer.from(await res.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test("generate favicon from HEIC", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/favicon", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
|
|
test("generate favicon from large sample JPEG", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/favicon", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const ct = res.headers()["content-type"] ?? "";
|
|
if (ct.includes("application/json")) {
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
} else {
|
|
const buffer = Buffer.from(await res.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test("generate favicon from AVIF format", async ({ request }) => {
|
|
const avif = formatFixture("sample.avif");
|
|
const res = await request.post("/api/v1/tools/image/favicon", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.avif", mimeType: "image/avif", buffer: avif },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Image to PDF — Extended ───────────────────────────────────────
|
|
|
|
test.describe("Image to PDF — extended", () => {
|
|
test("convert 3 images to multi-page PDF", async ({ request }) => {
|
|
const { body, contentType } = buildMultipart(
|
|
[
|
|
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
|
{ name: "file", filename: "b.png", contentType: "image/png", buffer: PNG_200x150 },
|
|
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ pageSize: "A4" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/image-to-pdf", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: body,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const json = await res.json();
|
|
expect(json.downloadUrl).toBeTruthy();
|
|
expect(json.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("convert image to PDF with letter page size", async ({ request }) => {
|
|
const { body, contentType } = buildMultipart(
|
|
[{ name: "file", filename: "test.jpg", contentType: "image/jpeg", buffer: JPG_100x100 }],
|
|
[{ name: "settings", value: JSON.stringify({ pageSize: "Letter" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/image-to-pdf", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: body,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const json = await res.json();
|
|
expect(json.downloadUrl).toBeTruthy();
|
|
expect(json.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("convert HEIC image to PDF", async ({ request }) => {
|
|
const { body, contentType } = buildMultipart(
|
|
[{ name: "file", filename: "test.heic", contentType: "image/heic", buffer: HEIC_200x150 }],
|
|
[{ name: "settings", value: JSON.stringify({ pageSize: "A4" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/image-to-pdf", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: body,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const json = await res.json();
|
|
expect(json.downloadUrl).toBeTruthy();
|
|
expect(json.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("convert 5 mixed-format images to PDF", async ({ request }) => {
|
|
const { body, contentType } = buildMultipart(
|
|
[
|
|
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
|
{ name: "file", filename: "b.png", contentType: "image/png", buffer: PNG_200x150 },
|
|
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
{ name: "file", filename: "d.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
|
{
|
|
name: "file",
|
|
filename: "e.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: formatFixture("sample.jpg"),
|
|
},
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ pageSize: "A4" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/image-to-pdf", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: body,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const json = await res.json();
|
|
expect(json.downloadUrl).toBeTruthy();
|
|
expect(json.processedSize).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ─── Replace Color — Extended ──────────────────────────────────────
|
|
|
|
test.describe("Replace Color — extended", () => {
|
|
test("replace color on WebP image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/replace-color", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
|
settings: JSON.stringify({
|
|
targetColor: "#000000",
|
|
replacementColor: "#ffffff",
|
|
tolerance: 20,
|
|
}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("replace color with zero tolerance (exact match)", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/replace-color", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
|
settings: JSON.stringify({
|
|
targetColor: "#ff0000",
|
|
replacementColor: "#00ff00",
|
|
tolerance: 0,
|
|
}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("replace color with maximum tolerance (100)", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/replace-color", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
|
settings: JSON.stringify({
|
|
targetColor: "#808080",
|
|
replacementColor: "#ff0000",
|
|
tolerance: 100,
|
|
}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("replace color on HEIC image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/replace-color", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
|
settings: JSON.stringify({
|
|
targetColor: "#ffffff",
|
|
replacementColor: "#cccccc",
|
|
tolerance: 15,
|
|
}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("replace color on content image", async ({ request }) => {
|
|
const portrait = contentFixture("portrait-color.jpg");
|
|
const res = await request.post("/api/v1/tools/image/replace-color", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "portrait.jpg", mimeType: "image/jpeg", buffer: portrait },
|
|
settings: JSON.stringify({
|
|
targetColor: "#ffffff",
|
|
replacementColor: "#000000",
|
|
tolerance: 40,
|
|
}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ─── Edit Metadata — Extended ──────────────────────────────────────
|
|
|
|
test.describe("Edit Metadata — extended", () => {
|
|
test("set all EXIF fields at once", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/edit-metadata", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
|
settings: JSON.stringify({
|
|
title: "Test Shot",
|
|
description: "E2E test image",
|
|
artist: "SnapOtter Bot",
|
|
copyright: "CC0-1.0",
|
|
}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("edit metadata on WebP image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/edit-metadata", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
|
settings: JSON.stringify({ title: "WebP Test" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("edit metadata round-trip verification", async ({ request }) => {
|
|
// Set metadata
|
|
const editRes = await request.post("/api/v1/tools/image/edit-metadata", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
|
settings: JSON.stringify({ artist: "RoundTrip Artist" }),
|
|
},
|
|
});
|
|
expect(editRes.ok()).toBe(true);
|
|
const editBody = await editRes.json();
|
|
|
|
// Download the edited image
|
|
const dlRes = await request.get(editBody.downloadUrl, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(dlRes.ok()).toBe(true);
|
|
const editedBuffer = Buffer.from(await dlRes.body());
|
|
|
|
// Check info on the edited image
|
|
const infoRes = await request.post("/api/v1/tools/image/info", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "edited.jpg", mimeType: "image/jpeg", buffer: editedBuffer },
|
|
},
|
|
});
|
|
expect(infoRes.ok()).toBe(true);
|
|
const infoBody = await infoRes.json();
|
|
expect(infoBody.hasExif).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Strip Metadata — Extended ────────────────────────────────────
|
|
|
|
test.describe("Strip Metadata — extended", () => {
|
|
test("strip metadata from JPEG with EXIF preserves dimensions", async ({ request }) => {
|
|
const jpgExif = fixture("test-with-exif.jpg");
|
|
const res = await request.post("/api/v1/tools/image/strip-metadata", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: jpgExif },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
expect(body.processedSize).toBeLessThanOrEqual(body.originalSize);
|
|
});
|
|
|
|
test("strip metadata from WebP image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/strip-metadata", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("strip metadata from HEIC image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/strip-metadata", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("stripped image verified via info has no EXIF", async ({ request }) => {
|
|
const jpgExif = fixture("test-with-exif.jpg");
|
|
const stripRes = await request.post("/api/v1/tools/image/strip-metadata", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: jpgExif },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(stripRes.ok()).toBe(true);
|
|
const stripBody = await stripRes.json();
|
|
|
|
// Download stripped image
|
|
const dlRes = await request.get(stripBody.downloadUrl, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(dlRes.ok()).toBe(true);
|
|
const strippedBuffer = Buffer.from(await dlRes.body());
|
|
|
|
// Verify via info tool
|
|
const infoRes = await request.post("/api/v1/tools/image/info", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "stripped.jpg", mimeType: "image/jpeg", buffer: strippedBuffer },
|
|
},
|
|
});
|
|
expect(infoRes.ok()).toBe(true);
|
|
const infoBody = await infoRes.json();
|
|
expect(infoBody.hasExif).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Image Enhancement — Extended ─────────────────────────────────
|
|
|
|
test.describe("Image Enhancement — extended", () => {
|
|
test("enhancement with auto preset", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/image-enhancement", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
|
settings: JSON.stringify({ preset: "auto" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("enhancement with vivid preset", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/image-enhancement", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({ preset: "vivid" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
// Enhancement should modify the image
|
|
expect(body.processedSize).not.toBe(body.originalSize);
|
|
});
|
|
|
|
test("enhancement on WebP image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/image-enhancement", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
|
settings: JSON.stringify({ preset: "auto" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("enhancement on HEIC image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/image-enhancement", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
|
settings: JSON.stringify({ preset: "auto" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
// ─── Content-Aware Resize ─────────────────────────────────────────
|
|
|
|
test.describe("Content-Aware Resize — extended", () => {
|
|
test("content-aware resize to smaller width", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/content-aware-resize", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
|
settings: JSON.stringify({ width: 150, height: 150 }),
|
|
},
|
|
});
|
|
if (res.status() === 501) {
|
|
const body = await res.json();
|
|
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
|
|
} else {
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test("content-aware resize JPEG image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/content-aware-resize", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
|
settings: JSON.stringify({ width: 80, height: 80 }),
|
|
},
|
|
});
|
|
if (res.status() === 501) {
|
|
const body = await res.json();
|
|
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
|
|
} else {
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
test("content-aware resize on large content image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/content-aware-resize", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({ width: 300, height: 300 }),
|
|
},
|
|
});
|
|
if (res.status() === 501) {
|
|
const body = await res.json();
|
|
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
|
|
} else {
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Content-Aware Resize — HEIC & Edge Cases ───────────────────
|
|
|
|
test.describe("Content-Aware Resize — HEIC & edge cases", () => {
|
|
test("content-aware resize HEIC image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/content-aware-resize", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
|
settings: JSON.stringify({ width: 150, height: 120 }),
|
|
},
|
|
});
|
|
if (res.status() === 501) {
|
|
const body = await res.json();
|
|
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
|
|
} else {
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
test("content-aware resize WebP image", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/content-aware-resize", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
|
settings: JSON.stringify({ width: 40, height: 40 }),
|
|
},
|
|
});
|
|
if (res.status() === 501) {
|
|
const body = await res.json();
|
|
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
|
|
} else {
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Optimize for Web — Output Verification ─────────────────────
|
|
|
|
test.describe("Optimize for Web — output verification", () => {
|
|
test("optimized file can be downloaded and is valid", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({ maxWidth: 400, quality: 60 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
|
|
// Download and verify the optimized file is valid
|
|
const dlRes = await request.get(body.downloadUrl, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(dlRes.ok()).toBe(true);
|
|
const buffer = Buffer.from(await dlRes.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
expect(buffer.length).toBeLessThan(JPG_SAMPLE.length);
|
|
});
|
|
});
|
|
|
|
// ─── Favicon — Output Verification ──────────────────────────────
|
|
|
|
test.describe("Favicon — output verification", () => {
|
|
test("favicon output can be downloaded", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/favicon", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const ct = res.headers()["content-type"] ?? "";
|
|
if (ct.includes("application/json")) {
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
|
|
const dlRes = await request.get(body.downloadUrl, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(dlRes.ok()).toBe(true);
|
|
const buffer = Buffer.from(await dlRes.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
} else {
|
|
const buffer = Buffer.from(await res.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test("favicon from TIFF format", async ({ request }) => {
|
|
const tiff = formatFixture("sample.tiff");
|
|
const res = await request.post("/api/v1/tools/image/favicon", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: tiff },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Optimize for Web -- Target Format ────────────────────────────
|
|
|
|
test.describe("Optimize for Web -- target format", () => {
|
|
test("optimize with target format WebP", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({ maxWidth: 800, quality: 75, targetFormat: "webp" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("optimize with target format AVIF", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({ maxWidth: 640, quality: 60, targetFormat: "avif" }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
|
|
test("optimize preserves original format when no target specified", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
|
settings: JSON.stringify({ maxWidth: 100, quality: 50 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
// ─── Optimize for Web -- Constraint Combinations ──────────────────
|
|
|
|
test.describe("Optimize for Web -- constraints", () => {
|
|
test("optimize with maxWidth only (no quality)", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({ maxWidth: 400 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("optimize with quality only (no resize)", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({ quality: 30 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeLessThan(body.originalSize);
|
|
});
|
|
|
|
test("optimize with very small maxWidth", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
|
settings: JSON.stringify({ maxWidth: 50, quality: 50 }),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeLessThan(body.originalSize);
|
|
});
|
|
});
|
|
|
|
// ─── Favicon -- Multi-size Output ─────────────────────────────────
|
|
|
|
test.describe("Favicon -- multi-size", () => {
|
|
test("favicon from PNG generates downloadable output", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/favicon", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const ct = res.headers()["content-type"] ?? "";
|
|
if (ct.includes("application/json")) {
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
|
|
// Download and verify non-empty
|
|
const dlRes = await request.get(body.downloadUrl, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(dlRes.ok()).toBe(true);
|
|
const buffer = Buffer.from(await dlRes.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
} else {
|
|
const buffer = Buffer.from(await res.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
test("favicon from JPEG", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/favicon", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
|
|
test("favicon from PNG content image", async ({ request }) => {
|
|
const portrait = contentFixture("portrait-isolated.png");
|
|
const res = await request.post("/api/v1/tools/image/favicon", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "portrait.png", mimeType: "image/png", buffer: portrait },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
|
|
test("favicon from GIF format", async ({ request }) => {
|
|
const gif = formatFixture("sample.gif");
|
|
const res = await request.post("/api/v1/tools/image/favicon", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "sample.gif", mimeType: "image/gif", buffer: gif },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Replace Color -- Tolerance Levels ────────────────────────────
|
|
|
|
test.describe("Replace Color -- tolerance levels", () => {
|
|
const tolerances = [0, 5, 10, 25, 50, 75, 100] as const;
|
|
|
|
for (const tolerance of tolerances) {
|
|
test(`replace color with tolerance=${tolerance}`, async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/replace-color", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
|
settings: JSON.stringify({
|
|
targetColor: "#FFFFFF",
|
|
replacementColor: "#FF0000",
|
|
tolerance,
|
|
}),
|
|
},
|
|
});
|
|
expect(res.ok(), `replace-color with tolerance=${tolerance} should succeed`).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
}
|
|
});
|
|
|
|
// ─── Replace Color -- Download Verification ───────────────────────
|
|
|
|
test.describe("Replace Color -- download verification", () => {
|
|
test("replaced-color image is downloadable and valid", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/replace-color", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
|
settings: JSON.stringify({
|
|
targetColor: "#FFFFFF",
|
|
replacementColor: "#00FF00",
|
|
tolerance: 30,
|
|
}),
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
|
|
const dlRes = await request.get(body.downloadUrl, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(dlRes.ok()).toBe(true);
|
|
const buffer = Buffer.from(await dlRes.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ─── Bulk Rename -- ZIP Verification ──────────────────────────────
|
|
|
|
test.describe("Bulk Rename -- ZIP output", () => {
|
|
test("rename 5 files and verify ZIP output", async ({ request }) => {
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[
|
|
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
|
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
|
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
{
|
|
name: "file",
|
|
filename: "d.heic",
|
|
contentType: "image/heic",
|
|
buffer: HEIC_200x150,
|
|
},
|
|
{
|
|
name: "file",
|
|
filename: "e.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: JPG_SAMPLE,
|
|
},
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ pattern: "photo-{{index}}", startIndex: 1 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/bulk-rename", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const resContentType = res.headers()["content-type"] ?? "";
|
|
if (resContentType.includes("application/json")) {
|
|
const json = await res.json();
|
|
if (json.downloadUrl) {
|
|
const dlRes = await request.get(json.downloadUrl, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(dlRes.ok()).toBe(true);
|
|
const buffer = Buffer.from(await dlRes.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
// ZIP magic bytes: PK
|
|
expect(buffer[0]).toBe(0x50);
|
|
expect(buffer[1]).toBe(0x4b);
|
|
}
|
|
} else {
|
|
const buffer = Buffer.from(await res.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
expect(buffer[0]).toBe(0x50);
|
|
expect(buffer[1]).toBe(0x4b);
|
|
}
|
|
});
|
|
|
|
test("rename with prefix-suffix pattern", async ({ request }) => {
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[
|
|
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
|
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
|
],
|
|
[
|
|
{
|
|
name: "settings",
|
|
value: JSON.stringify({ pattern: "project_{{index}}_final", startIndex: 10 }),
|
|
},
|
|
],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/bulk-rename", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Image Enhancement -- Preset Variations ───────────────────────
|
|
|
|
test.describe("Image Enhancement -- presets", () => {
|
|
const presets = ["auto", "vivid", "soft", "portrait"] as const;
|
|
|
|
for (const preset of presets) {
|
|
test(`enhancement with preset=${preset}`, async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/image-enhancement", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: {
|
|
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
|
settings: JSON.stringify({ preset }),
|
|
},
|
|
});
|
|
expect(res.ok(), `enhancement with preset=${preset} should succeed`).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.downloadUrl).toBeTruthy();
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
});
|
|
}
|
|
});
|
|
|
|
// ─── Auth Failure ──────────────────────────────────────────────────
|
|
|
|
test.describe("Auth failure", () => {
|
|
test("optimize-for-web without token returns 401", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web", {
|
|
multipart: {
|
|
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
|
settings: JSON.stringify({ maxWidth: 800, quality: 75 }),
|
|
},
|
|
});
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
|
|
test("favicon without token returns 401", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/favicon", {
|
|
multipart: {
|
|
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
|
settings: JSON.stringify({}),
|
|
},
|
|
});
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
|
|
test("replace-color without token returns 401", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/replace-color", {
|
|
multipart: {
|
|
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
|
settings: JSON.stringify({
|
|
targetColor: "#ffffff",
|
|
replacementColor: "#000000",
|
|
tolerance: 30,
|
|
}),
|
|
},
|
|
});
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
|
|
test("image-to-pdf without token returns 401", async ({ request }) => {
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[{ name: "file", filename: "test.jpg", contentType: "image/jpeg", buffer: JPG_100x100 }],
|
|
[{ name: "settings", value: JSON.stringify({ pageSize: "A4" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/image-to-pdf", {
|
|
headers: { "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
|
|
test("content-aware-resize without token returns 401", async ({ request }) => {
|
|
const res = await request.post("/api/v1/tools/image/content-aware-resize", {
|
|
multipart: {
|
|
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
|
settings: JSON.stringify({ width: 100, height: 100 }),
|
|
},
|
|
});
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
});
|