test: comprehensive test coverage expansion (+965 tests)

Add 42 new test files covering all untested tool routes, image engine
internals, AI sidecar bridge, Zustand stores, and cross-format
compatibility. Expand e2e-docker suite with 7 spec files covering all
48 tools against a real Docker container.

Unit tests:
- Image engine: format detection, MIME mapping, metadata parsing, pipeline
- AI bridge: sidecar lifecycle, all 11 tool functions (mocked)
- Web stores: 14 Zustand stores (collage, settings, features, analytics, etc.)
- API helpers: format decoders, page range, file validation

Integration tests:
- 25 tool routes that had zero dedicated tests
- Cross-format matrix: 17 input formats x 3 tools
- Edge cases: zero-byte files, corrupted headers, path traversal, XSS, SQL injection
- Concurrent request handling and pipeline edge cases

E2E-Docker (Playwright against real container):
- 7 spec files: essential, adjustment, conversion, creative, utility, AI, pipeline
- Custom buildMultipart helper for multi-file tool uploads
- AI tools gracefully skip when sidecar not installed

Fixtures:
- Organized test media: formats/ (18 formats) + content/ (17 content types)
- Reduced from 3.1 GB unorganized samples to 33 MB structured fixtures

Bug fix:
- color-adjustments: gamma exposure used invalid single-param gamma() for
  positive values; fixed to use two-param gamma(gammaIn, gammaOut) form
This commit is contained in:
ashim-hq
2026-04-23 17:12:02 +08:00
parent 19df740880
commit babca4cf97
89 changed files with 19908 additions and 3 deletions
+306
View File
@@ -0,0 +1,306 @@
/**
* Integration tests for the qr-generate tool (/api/v1/tools/qr-generate).
*
* Covers QR code generation from text/URL input, custom size and colors,
* error correction levels, download verification, and input validation.
*/
import { join } from "node:path";
import sharp from "sharp";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { buildTestApp, loginAsAdmin, type TestApp } from "./test-server.js";
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("QR Generate", () => {
it("generates a QR code from a URL string", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {
text: "https://example.com",
},
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.jobId).toBeDefined();
expect(result.downloadUrl).toBeDefined();
expect(result.processedSize).toBeGreaterThan(0);
});
it("generates a downloadable PNG image", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {
text: "Hello World",
},
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
// Download the QR code image
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
});
expect(dlRes.statusCode).toBe(200);
const meta = await sharp(dlRes.rawPayload).metadata();
expect(meta.format).toBe("png");
expect(meta.width).toBeGreaterThan(0);
expect(meta.height).toBeGreaterThan(0);
});
it("respects the custom size parameter", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {
text: "test",
size: 800,
},
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
});
const meta = await sharp(dlRes.rawPayload).metadata();
expect(meta.width).toBe(800);
expect(meta.height).toBe(800);
});
it("uses default size of 400 when not specified", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {
text: "default size test",
},
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
});
const meta = await sharp(dlRes.rawPayload).metadata();
expect(meta.width).toBe(400);
expect(meta.height).toBe(400);
});
it("accepts custom foreground and background colors", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {
text: "colored QR",
foreground: "#FF0000",
background: "#00FF00",
},
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
expect(result.processedSize).toBeGreaterThan(0);
});
it("accepts all error correction levels", async () => {
for (const level of ["L", "M", "Q", "H"]) {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {
text: `EC level ${level}`,
errorCorrection: level,
},
});
expect(res.statusCode).toBe(200);
}
});
it("returns all expected fields in the response", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {
text: "structure test",
},
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result).toHaveProperty("jobId");
expect(result).toHaveProperty("downloadUrl");
expect(result).toHaveProperty("originalSize");
expect(result).toHaveProperty("processedSize");
expect(result.originalSize).toBe(0);
});
// ── Validation ──────────────────────────────────────────────────────
it("rejects requests without text", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {},
});
expect(res.statusCode).toBe(400);
const result = JSON.parse(res.body);
expect(result.error).toMatch(/invalid settings/i);
});
it("rejects empty text", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: { text: "" },
});
expect(res.statusCode).toBe(400);
});
it("rejects invalid color format", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {
text: "test",
foreground: "red",
},
});
expect(res.statusCode).toBe(400);
const result = JSON.parse(res.body);
expect(result.error).toMatch(/invalid settings/i);
});
it("rejects size below minimum", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {
text: "test",
size: 50,
},
});
expect(res.statusCode).toBe(400);
});
it("rejects size above maximum", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {
text: "test",
size: 20000,
},
});
expect(res.statusCode).toBe(400);
});
it("rejects invalid error correction level", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: {
text: "test",
errorCorrection: "X",
},
});
expect(res.statusCode).toBe(400);
});
it("rejects unauthenticated requests", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/qr-generate",
headers: { "content-type": "application/json" },
payload: { text: "test" },
});
expect(res.statusCode).toBe(401);
});
});