mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: percent-encode X-File-Results header for non-ASCII filenames (#133)
Closes #133
This commit is contained in:
@@ -1104,7 +1104,7 @@ describe("Batch with mixed image formats (PNG + JPEG + WebP)", () => {
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers["content-type"]).toBe("application/zip");
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
expect(Object.keys(fileResults).length).toBe(3);
|
||||
});
|
||||
|
||||
|
||||
@@ -649,7 +649,7 @@ describe("Batch edge cases — extended", () => {
|
||||
// Should succeed and deduplicate filenames in the ZIP
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers["content-type"]).toBe("application/zip");
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
// All three entries should have unique names
|
||||
const names = Object.values(fileResults);
|
||||
expect(new Set(names).size).toBe(3);
|
||||
@@ -1524,7 +1524,7 @@ describe("Batch limits — boundary tests", () => {
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers["content-type"]).toBe("application/zip");
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
expect(Object.keys(fileResults).length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -1546,7 +1546,7 @@ describe("Batch limits — boundary tests", () => {
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers["content-type"]).toBe("application/zip");
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
expect(Object.keys(fileResults).length).toBe(10);
|
||||
}, 120_000);
|
||||
|
||||
|
||||
@@ -1035,7 +1035,7 @@ describe("Batch -- all identical files", () => {
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers["content-type"]).toBe("application/zip");
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
expect(Object.keys(fileResults).length).toBe(5);
|
||||
});
|
||||
|
||||
@@ -1050,7 +1050,7 @@ describe("Batch -- all identical files", () => {
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers["content-type"]).toBe("application/zip");
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
const names = Object.values(fileResults);
|
||||
// All three entries should have unique names in the ZIP
|
||||
expect(new Set(names).size).toBe(3);
|
||||
|
||||
@@ -611,7 +611,9 @@ describe("Concurrent requests -- data integrity verification", () => {
|
||||
// Batch request must succeed
|
||||
expect(batchRes.statusCode).toBe(200);
|
||||
expect(batchRes.headers["content-type"]).toBe("application/zip");
|
||||
const fileResults = JSON.parse(batchRes.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(
|
||||
decodeURIComponent(batchRes.headers["x-file-results"] as string),
|
||||
);
|
||||
expect(Object.keys(fileResults).length).toBe(3);
|
||||
|
||||
// The single request's job ID must not appear in the batch results
|
||||
|
||||
@@ -3102,7 +3102,7 @@ describe("Batch processing", () => {
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers["x-file-results"]).toBeDefined();
|
||||
const parsed = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const parsed = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
expect(parsed["0"]).toBeDefined();
|
||||
expect(parsed["1"]).toBeDefined();
|
||||
});
|
||||
@@ -3189,7 +3189,7 @@ describe("Batch processing", () => {
|
||||
|
||||
const fileResults = res.headers["x-file-results"];
|
||||
expect(fileResults).toBeDefined();
|
||||
const parsed = JSON.parse(fileResults as string);
|
||||
const parsed = JSON.parse(decodeURIComponent(fileResults as string));
|
||||
expect(parsed["0"]).toBeDefined();
|
||||
expect(parsed["1"]).toBeDefined();
|
||||
expect(typeof parsed["0"]).toBe("string");
|
||||
@@ -3215,7 +3215,7 @@ describe("Batch processing", () => {
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
const names = [fileResults["0"], fileResults["1"], fileResults["2"]];
|
||||
expect(names[0]).toContain("aaa");
|
||||
expect(names[1]).toContain("bbb");
|
||||
|
||||
@@ -130,12 +130,90 @@ describe("X-File-Results header", () => {
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
expect(fileResults).toBeDefined();
|
||||
// Should have entries for index 0 and 1
|
||||
expect(fileResults["0"]).toBeDefined();
|
||||
expect(fileResults["1"]).toBeDefined();
|
||||
});
|
||||
|
||||
it("encodes non-ASCII filenames in header without ERR_INVALID_CHAR", async () => {
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "图片测试.png", contentType: "image/png", content: PNG },
|
||||
{ name: "file", filename: "テスト.jpg", contentType: "image/jpeg", content: JPG },
|
||||
{ name: "settings", content: JSON.stringify({ width: 80 }) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/resize/batch",
|
||||
headers: {
|
||||
"content-type": contentType,
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const raw = res.headers["x-file-results"] as string;
|
||||
expect(raw).toMatch(/^[\x20-\x7E]+$/);
|
||||
const fileResults = JSON.parse(decodeURIComponent(raw));
|
||||
expect(fileResults["0"]).toContain("图片测试");
|
||||
expect(fileResults["1"]).toContain("テスト");
|
||||
});
|
||||
|
||||
it("handles mixed ASCII and non-ASCII filenames", async () => {
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "normal.png", contentType: "image/png", content: PNG },
|
||||
{ name: "file", filename: "élève-photo.jpg", contentType: "image/jpeg", content: JPG },
|
||||
{ name: "file", filename: "📷-snap.png", contentType: "image/png", content: PNG },
|
||||
{ name: "settings", content: JSON.stringify({ width: 80 }) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/resize/batch",
|
||||
headers: {
|
||||
"content-type": contentType,
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const raw = res.headers["x-file-results"] as string;
|
||||
expect(raw).toMatch(/^[\x20-\x7E]+$/);
|
||||
const fileResults = JSON.parse(decodeURIComponent(raw));
|
||||
expect(fileResults["0"]).toContain("normal");
|
||||
expect(fileResults["1"]).toContain("élève");
|
||||
expect(fileResults["2"]).toContain("📷");
|
||||
});
|
||||
|
||||
it("round-trips filenames with special URI characters", async () => {
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{
|
||||
name: "file",
|
||||
filename: "file with spaces & (parens).png",
|
||||
contentType: "image/png",
|
||||
content: PNG,
|
||||
},
|
||||
{ name: "settings", content: JSON.stringify({ width: 80 }) },
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/tools/resize/batch",
|
||||
headers: {
|
||||
"content-type": contentType,
|
||||
authorization: `Bearer ${adminToken}`,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
expect(fileResults["0"]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── ClientJobId passthrough ─────────────────────────────────────
|
||||
@@ -363,7 +441,7 @@ describe("Exotic format batch processing", () => {
|
||||
it(`processes ${ext.toUpperCase()} through batch compress`, async () => {
|
||||
const res = await batchCompress(ext, mime);
|
||||
expect(res.statusCode).toBe(200);
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
expect(fileResults["0"]).toBeDefined();
|
||||
});
|
||||
}
|
||||
@@ -408,7 +486,7 @@ describe("Exotic format batch processing", () => {
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
expect(fileResults["0"]).toBeDefined();
|
||||
expect(fileResults["1"]).toBeDefined();
|
||||
expect(fileResults["2"]).toBeDefined();
|
||||
@@ -439,7 +517,7 @@ describe("Batch preserves upload order", () => {
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
|
||||
// Index 0 should be derived from alpha, 1 from beta, 2 from gamma
|
||||
expect(fileResults["0"]).toContain("alpha");
|
||||
|
||||
@@ -426,6 +426,33 @@ describe("Pipeline batch execution", () => {
|
||||
expect(res.headers["x-file-results"]).toBeDefined();
|
||||
});
|
||||
|
||||
it("handles non-ASCII filenames without ERR_INVALID_CHAR (#133)", async () => {
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{ name: "file", filename: "图片.png", contentType: "image/png", content: PNG_200x150 },
|
||||
{ name: "file", filename: "写真テスト.png", contentType: "image/png", content: PNG_200x150 },
|
||||
{
|
||||
name: "pipeline",
|
||||
content: JSON.stringify({
|
||||
steps: [{ toolId: "resize", settings: { width: 50 } }],
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/pipeline/batch",
|
||||
headers: { "content-type": contentType, authorization: `Bearer ${adminToken}` },
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const raw = res.headers["x-file-results"] as string;
|
||||
expect(raw).toMatch(/^[\x20-\x7E]+$/);
|
||||
const fileResults = JSON.parse(decodeURIComponent(raw));
|
||||
expect(fileResults["0"]).toContain("图片");
|
||||
expect(fileResults["1"]).toContain("写真テスト");
|
||||
});
|
||||
|
||||
it("rejects pipeline batch with no files", async () => {
|
||||
const { body, contentType } = createMultipartPayload([
|
||||
{
|
||||
|
||||
@@ -854,7 +854,7 @@ describe("svg-to-raster", () => {
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers["content-type"]).toBe("application/zip");
|
||||
// X-File-Results should contain deduplicated names
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
const filenames = Object.values(fileResults) as string[];
|
||||
// Filenames should be unique
|
||||
expect(new Set(filenames).size).toBe(filenames.length);
|
||||
@@ -1036,7 +1036,7 @@ describe("svg-to-raster", () => {
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers["content-type"]).toBe("application/zip");
|
||||
const fileResults = JSON.parse(res.headers["x-file-results"] as string);
|
||||
const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string));
|
||||
expect(Object.keys(fileResults).length).toBe(5);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user