mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: deepen integration tests for 6 simpler tools (HEIC, large file, batch, edge cases)
This commit is contained in:
@@ -15,6 +15,9 @@ const FIXTURES = join(__dirname, "..", "fixtures");
|
||||
const PNG = readFileSync(join(FIXTURES, "test-200x150.png"));
|
||||
const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
|
||||
const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp"));
|
||||
const TINY_PNG = readFileSync(join(FIXTURES, "test-1x1.png"));
|
||||
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
|
||||
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
|
||||
|
||||
/** Extract sorted filenames from a ZIP buffer. */
|
||||
function zipEntryNames(buf: Buffer): string[] {
|
||||
@@ -614,4 +617,161 @@ describe("Bulk Rename", () => {
|
||||
expect(filenames).toHaveLength(1);
|
||||
expect(filenames[0]).toBe("solo-1.png");
|
||||
});
|
||||
|
||||
// ── HEIF file handling ─────────────────────────────────────────
|
||||
|
||||
it("renames HEIF files preserving extension", async () => {
|
||||
const HEIF = readFileSync(join(FIXTURES, "formats", "sample.heif"));
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF },
|
||||
{ name: "settings", content: JSON.stringify({ pattern: "renamed-{{index}}" }) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/bulk-rename",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const filenames = zipEntryNames(res.rawPayload);
|
||||
expect(filenames).toContain("renamed-1.heif");
|
||||
});
|
||||
|
||||
// ── Large file handling ────────────────────────────────────────
|
||||
|
||||
it("renames a large stress image preserving content", async () => {
|
||||
const LARGE = readFileSync(join(FIXTURES, "content", "stress-large.jpg"));
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "big.jpg", contentType: "image/jpeg", content: LARGE },
|
||||
{ name: "settings", content: JSON.stringify({ pattern: "large-{{index}}" }) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/bulk-rename",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const filenames = zipEntryNames(res.rawPayload);
|
||||
expect(filenames).toContain("large-1.jpg");
|
||||
|
||||
const zip = new AdmZip(res.rawPayload);
|
||||
const entry = zip.getEntry("large-1.jpg");
|
||||
expect(entry).not.toBeNull();
|
||||
expect(entry!.getData().length).toBe(LARGE.length);
|
||||
});
|
||||
|
||||
// ── Tiny file handling ─────────────────────────────────────────
|
||||
|
||||
it("renames a 1x1 pixel image", async () => {
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "tiny.png", contentType: "image/png", content: TINY_PNG },
|
||||
{ name: "settings", content: JSON.stringify({ pattern: "small-{{index}}" }) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/bulk-rename",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const filenames = zipEntryNames(res.rawPayload);
|
||||
expect(filenames).toContain("small-1.png");
|
||||
});
|
||||
|
||||
// ── SVG file handling ──────────────────────────────────────────
|
||||
|
||||
it("renames SVG files preserving extension", async () => {
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG },
|
||||
{ name: "settings", content: JSON.stringify({ pattern: "vector-{{index}}" }) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/bulk-rename",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const filenames = zipEntryNames(res.rawPayload);
|
||||
expect(filenames).toContain("vector-1.svg");
|
||||
});
|
||||
|
||||
// ── Animated GIF handling ──────────────────────────────────────
|
||||
|
||||
it("renames animated GIF files preserving extension", async () => {
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "animation.gif", contentType: "image/gif", content: GIF },
|
||||
{ name: "settings", content: JSON.stringify({ pattern: "anim-{{index}}" }) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/bulk-rename",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const filenames = zipEntryNames(res.rawPayload);
|
||||
expect(filenames).toContain("anim-1.gif");
|
||||
});
|
||||
|
||||
// ── Mixed format batch (5+ files) ─────────────────────────────
|
||||
|
||||
it("renames a batch of 5+ mixed format files", async () => {
|
||||
const HEIC = readFileSync(join(FIXTURES, "test-200x150.heic"));
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", content: WEBP },
|
||||
{ name: "file", filename: "d.gif", contentType: "image/gif", content: GIF },
|
||||
{ name: "file", filename: "e.svg", contentType: "image/svg+xml", content: SVG },
|
||||
{ name: "file", filename: "f.heic", contentType: "image/heic", content: HEIC },
|
||||
{ name: "settings", content: JSON.stringify({ pattern: "batch-{{padded}}" }) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/bulk-rename",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const filenames = zipEntryNames(res.rawPayload);
|
||||
expect(filenames).toHaveLength(6);
|
||||
expect(filenames).toContain("batch-1.png");
|
||||
expect(filenames).toContain("batch-2.jpg");
|
||||
expect(filenames).toContain("batch-3.webp");
|
||||
expect(filenames).toContain("batch-4.gif");
|
||||
expect(filenames).toContain("batch-5.svg");
|
||||
expect(filenames).toContain("batch-6.heic");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,8 @@ const PNG = readFileSync(join(FIXTURES, "test-200x150.png"));
|
||||
const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
|
||||
const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp"));
|
||||
const HEIC = readFileSync(join(FIXTURES, "test-200x150.heic"));
|
||||
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
|
||||
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
|
||||
|
||||
let testApp: TestApp;
|
||||
let app: TestApp["app"];
|
||||
@@ -583,6 +585,57 @@ describe("Document mode variations", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Authentication ──────────────────────────────────────────────
|
||||
describe("Authentication", () => {
|
||||
it("returns 401 for unauthenticated request", async () => {
|
||||
const { body: payload, contentType } = makePayload({ mode: "auto" });
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/image-enhancement",
|
||||
payload,
|
||||
headers: { "content-type": contentType },
|
||||
});
|
||||
expect(res.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
// ── HEIF input ─────────────────────────────────────────────────
|
||||
describe("HEIF input", () => {
|
||||
it("enhances HEIF (sample.heif) input", async () => {
|
||||
const HEIF = readFileSync(join(FIXTURES, "formats", "sample.heif"));
|
||||
const res = await postTool({ mode: "auto" }, HEIF, "sample.heif", "image/heif");
|
||||
expect([200, 422]).toContain(res.statusCode);
|
||||
if (res.statusCode === 200) {
|
||||
const result = JSON.parse(res.body);
|
||||
expect(result.downloadUrl).toBeDefined();
|
||||
expect(result.processedSize).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── SVG input ──────────────────────────────────────────────────
|
||||
describe("SVG input", () => {
|
||||
it("enhances SVG input after rasterization", async () => {
|
||||
const res = await postTool({ mode: "auto" }, SVG, "test.svg", "image/svg+xml");
|
||||
expect([200, 400, 422]).toContain(res.statusCode);
|
||||
if (res.statusCode === 200) {
|
||||
const result = JSON.parse(res.body);
|
||||
expect(result.downloadUrl).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── Animated GIF input ─────────────────────────────────────────
|
||||
describe("Animated GIF input", () => {
|
||||
it("enhances animated GIF input", async () => {
|
||||
const res = await postTool({ mode: "auto" }, GIF, "animated.gif", "image/gif");
|
||||
expect(res.statusCode).toBe(200);
|
||||
const result = JSON.parse(res.body);
|
||||
expect(result.downloadUrl).toBeDefined();
|
||||
expect(result.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Selective correction combinations ───────────────────────────
|
||||
describe("Selective correction edge cases", () => {
|
||||
it("enables only contrast and white balance", async () => {
|
||||
|
||||
@@ -587,6 +587,33 @@ describe("Info", () => {
|
||||
expect(typeof result.hasXmp).toBe("boolean");
|
||||
});
|
||||
|
||||
// ── HEIF format info ──────────────────────────────────────────
|
||||
|
||||
it("returns correct metadata for HEIF (sample.heif) image", async () => {
|
||||
const HEIF = readFileSync(join(FIXTURES, "formats", "sample.heif"));
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "sample.heif", contentType: "image/heif", content: HEIF },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/info",
|
||||
headers: {
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
"content-type": contentType,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
// HEIF might need system decoder -- skip if 422
|
||||
if (res.statusCode === 422) return;
|
||||
expect(res.statusCode).toBe(200);
|
||||
const result = JSON.parse(res.body);
|
||||
expect(result.width).toBeGreaterThan(0);
|
||||
expect(result.height).toBeGreaterThan(0);
|
||||
expect(result.filename).toBe("sample.heif");
|
||||
});
|
||||
|
||||
// ── Alpha channel detection ─────────────────────────────────────
|
||||
|
||||
it("detects alpha channel in PNG with transparency", async () => {
|
||||
|
||||
@@ -723,6 +723,47 @@ describe("Preview endpoint format variations", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Unauthenticated request ────────────────────────────────────
|
||||
describe("Authentication", () => {
|
||||
it("returns 401 for unauthenticated request", async () => {
|
||||
const { body: payload, contentType } = makePayload({ format: "webp" });
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/optimize-for-web",
|
||||
payload,
|
||||
headers: { "content-type": contentType },
|
||||
});
|
||||
expect(res.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
// ── HEIF input handling ───────────────────────────────────────
|
||||
describe("HEIF input", () => {
|
||||
it("optimizes HEIF (sample.heif) input to webp", async () => {
|
||||
const HEIF = readFileSync(join(FIXTURES, "formats", "sample.heif"));
|
||||
const res = await postTool({ format: "webp" }, HEIF, "sample.heif", "image/heif");
|
||||
// HEIF decode may not be available
|
||||
expect([200, 422]).toContain(res.statusCode);
|
||||
if (res.statusCode === 200) {
|
||||
const result = JSON.parse(res.body);
|
||||
expect(result.downloadUrl).toBeDefined();
|
||||
expect(result.downloadUrl).toContain(".webp");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── Animated GIF input ────────────────────────────────────────
|
||||
describe("Animated GIF input", () => {
|
||||
it("optimizes animated GIF to webp", async () => {
|
||||
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
|
||||
const res = await postTool({ format: "webp" }, GIF, "animated.gif", "image/gif");
|
||||
expect(res.statusCode).toBe(200);
|
||||
const result = JSON.parse(res.body);
|
||||
expect(result.downloadUrl).toBeDefined();
|
||||
expect(result.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── File with no extension ──────────────────────────────────────
|
||||
describe("File naming edge cases", () => {
|
||||
it("handles file without extension", async () => {
|
||||
|
||||
@@ -699,4 +699,74 @@ describe("vectorize", () => {
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
// ── HEIF input ───────────────────────────────────────────────
|
||||
|
||||
it("works with HEIF (sample.heif) input after decoding", async () => {
|
||||
const HEIF = readFileSync(join(FIXTURES, "formats", "sample.heif"));
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "sample.heif", contentType: "image/heif", content: HEIF },
|
||||
{ name: "settings", content: JSON.stringify({}) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/vectorize",
|
||||
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
|
||||
body,
|
||||
});
|
||||
|
||||
// HEIF may fail if system decoder missing
|
||||
expect([200, 422]).toContain(res.statusCode);
|
||||
if (res.statusCode === 200) {
|
||||
const json = JSON.parse(res.body);
|
||||
expect(json.downloadUrl).toMatch(/\.svg$/);
|
||||
}
|
||||
});
|
||||
|
||||
// ── SVG input ────────────────────────────────────────────────
|
||||
|
||||
it("vectorizes SVG input (re-traces after rasterization)", async () => {
|
||||
const SVG_BUF = readFileSync(join(FIXTURES, "test-100x100.svg"));
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "test.svg", contentType: "image/svg+xml", content: SVG_BUF },
|
||||
{ name: "settings", content: JSON.stringify({ colorMode: "bw" }) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/vectorize",
|
||||
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
|
||||
body,
|
||||
});
|
||||
|
||||
// SVG may need rasterization first; accept success or processing error
|
||||
expect([200, 422]).toContain(res.statusCode);
|
||||
if (res.statusCode === 200) {
|
||||
const json = JSON.parse(res.body);
|
||||
expect(json.downloadUrl).toMatch(/\.svg$/);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Animated GIF input ───────────────────────────────────────
|
||||
|
||||
it("vectorizes animated GIF input", async () => {
|
||||
const GIF_BUF = readFileSync(join(FIXTURES, "animated.gif"));
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "animated.gif", contentType: "image/gif", content: GIF_BUF },
|
||||
{ name: "settings", content: JSON.stringify({ colorMode: "bw" }) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/vectorize",
|
||||
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const json = JSON.parse(res.body);
|
||||
expect(json.downloadUrl).toMatch(/\.svg$/);
|
||||
expect(json.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user