test: improve coverage for URL import feature

This commit is contained in:
SnapOtter
2026-05-11 22:39:37 +08:00
parent b9272977df
commit 04e7b21f31
2 changed files with 211 additions and 0 deletions
+138
View File
@@ -48,6 +48,7 @@ import { buildTestApp, loginAsAdmin, type TestApp } from "./test-server.js";
const FIXTURES = join(__dirname, "..", "fixtures");
const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
const TIFF = readFileSync(join(FIXTURES, "formats", "sample.tiff"));
let testApp: TestApp;
let app: TestApp["app"];
@@ -84,6 +85,31 @@ function startMockServer(): Promise<{ server: Server; port: number }> {
return;
}
if (url === "/photo.tiff") {
res.writeHead(200, { "Content-Type": "image/tiff" });
res.end(TIFF);
return;
}
if (url === "/empty") {
res.writeHead(200, { "Content-Type": "image/jpeg" });
res.end();
return;
}
if (url === "/server-error") {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Internal Server Error");
return;
}
if (url === "/slow-close") {
// Return a valid response with no body stream at all
res.writeHead(200, { "Content-Type": "image/jpeg", "Content-Length": "0" });
res.end();
return;
}
res.writeHead(404);
res.end("Not Found");
});
@@ -355,4 +381,116 @@ describe("POST /api/v1/fetch-urls", () => {
const body = JSON.parse(res.body);
expect(body.error).toBeTruthy();
});
it("generates a preview for non-browser-previewable formats", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/fetch-urls",
headers: {
authorization: `Bearer ${adminToken}`,
},
payload: {
urls: [`http://127.0.0.1:${mockPort}/photo.tiff`],
},
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.results).toHaveLength(1);
const result = body.results[0];
expect(result.success).toBe(true);
expect(result.contentType).toBe("image/tiff");
expect(result.previewUrl).toBeTruthy();
expect(result.previewUrl).toContain("preview-");
expect(result.previewUrl).toContain(".webp");
// Preview URL should serve a valid webp image
const previewRes = await app.inject({
method: "GET",
url: result.previewUrl,
headers: {
authorization: `Bearer ${adminToken}`,
},
});
expect(previewRes.statusCode).toBe(200);
});
it("returns failure for empty response body", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/fetch-urls",
headers: {
authorization: `Bearer ${adminToken}`,
},
payload: {
urls: [`http://127.0.0.1:${mockPort}/empty`],
},
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.results).toHaveLength(1);
expect(body.results[0].success).toBe(false);
expect(body.results[0].error).toContain("Empty");
});
it("returns failure for 500 server error", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/fetch-urls",
headers: {
authorization: `Bearer ${adminToken}`,
},
payload: {
urls: [`http://127.0.0.1:${mockPort}/server-error`],
},
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.results).toHaveLength(1);
expect(body.results[0].success).toBe(false);
expect(body.results[0].error).toContain("500");
});
it("returns failure when fetch throws a network error", async () => {
// Port 1 is almost guaranteed to refuse connections, triggering the outer
// catch block (lines 275-278 in fetch-urls.ts).
const res = await app.inject({
method: "POST",
url: "/api/v1/fetch-urls",
headers: {
authorization: `Bearer ${adminToken}`,
},
payload: {
urls: ["http://127.0.0.1:1/unreachable.jpg"],
},
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.results).toHaveLength(1);
expect(body.results[0].success).toBe(false);
expect(body.results[0].error).toBeTruthy();
});
it("returns failure for zero-length content", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/fetch-urls",
headers: {
authorization: `Bearer ${adminToken}`,
},
payload: {
urls: [`http://127.0.0.1:${mockPort}/slow-close`],
},
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.results).toHaveLength(1);
expect(body.results[0].success).toBe(false);
expect(body.results[0].error).toContain("Empty");
});
});
+73
View File
@@ -68,12 +68,85 @@ describe("validateFetchUrl", () => {
await expect(validateFetchUrl("http://[2001:DB8::1]/image.jpg")).rejects.toThrow("private");
});
it("allows a public IP address directly in URL", async () => {
// Exercises the early-return path in resolveAndCheck when hostname is a
// non-private IP literal (covers the `return` after the isIP check).
await expect(validateFetchUrl("http://8.8.8.8/image.jpg")).resolves.toBeUndefined();
});
it("rejects invalid URLs", async () => {
await expect(validateFetchUrl("not-a-url")).rejects.toThrow();
await expect(validateFetchUrl("")).rejects.toThrow();
});
});
/**
* Tests that require DNS mocking to exercise resolveAndCheck paths that only
* trigger when the hostname is a non-IP string and lookup returns results.
*/
describe("validateFetchUrl with DNS mocking", () => {
const originalLookup = vi.hoisted(() => {
return { fn: null as null | ((...args: unknown[]) => unknown) };
});
beforeEach(() => {
vi.restoreAllMocks();
});
vi.mock("node:dns/promises", async (importOriginal) => {
const orig = (await importOriginal()) as Record<string, unknown>;
originalLookup.fn = orig.lookup as (...args: unknown[]) => unknown;
return {
...orig,
lookup: vi.fn((...args: unknown[]) => originalLookup.fn?.(...args)),
};
});
it("rejects hostname that resolves to IPv4-mapped IPv6 with private IPv4", async () => {
// Covers isPrivateIPv6 lines 28-31 (::ffff: mapped address path)
const dns = await import("node:dns/promises");
vi.mocked(dns.lookup).mockResolvedValueOnce([
{ address: "::ffff:127.0.0.1", family: 6 },
] as never);
await expect(validateFetchUrl("http://mapped-v6.example.com/image.jpg")).rejects.toThrow(
"private",
);
});
it("rejects hostname resolving to IPv4-mapped IPv6 with 10.x private", async () => {
const dns = await import("node:dns/promises");
vi.mocked(dns.lookup).mockResolvedValueOnce([
{ address: "::ffff:10.0.0.1", family: 6 },
] as never);
await expect(validateFetchUrl("http://mapped-ten.example.com/image.jpg")).rejects.toThrow(
"private",
);
});
it("handles DNS lookup returning a single result object", async () => {
// Covers the Array.isArray fallback branch (line 45: wrapping non-array in [])
const dns = await import("node:dns/promises");
vi.mocked(dns.lookup).mockResolvedValueOnce({
address: "203.0.113.1",
family: 4,
} as never);
await expect(
validateFetchUrl("http://single-result.example.com/image.jpg"),
).resolves.toBeUndefined();
});
it("rejects when DNS returns multiple addresses with one private", async () => {
const dns = await import("node:dns/promises");
vi.mocked(dns.lookup).mockResolvedValueOnce([
{ address: "203.0.113.1", family: 4 },
{ address: "10.0.0.1", family: 4 },
] as never);
await expect(validateFetchUrl("http://dual-addr.example.com/image.jpg")).rejects.toThrow(
"private",
);
});
});
describe("safeFetch", () => {
let mockFetch: Mock;