mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: improve coverage for URL import feature
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user