import { describe, expect, it } from "@suaveplan/testing/runner"; import type { Clock, LoggerLike } from "@suaveplan/types"; import { parseConfig } from "../config/config.js"; import { CADDY_IMGPROXY_CODES } from "../error-codes/error-codes.js"; import { ImgproxyHandler, NOOP_LOGGER } from "./handler.js"; function makeLogger(): { logger: LoggerLike; warns: Array<{ message: string; context?: Readonly>; }>; } { const warns: Array<{ message: string; context?: Readonly>; }> = []; return { warns, logger: { debug: () => undefined, info: () => undefined, warn: (message, context) => { warns.push({ message, context }); }, error: () => undefined, }, }; } describe("ImgproxyHandler", () => { it("rewrites a valid imgproxy URL", { timeout: 5000 }, () => { const config = parseConfig({ defaultWidth: 800, key: "aa11bbcc", salt: "deadbeef", }); const handler = new ImgproxyHandler({ config }); const result = handler.rewrite({ path: "/uv16:9/avatar.png", host: "images.example.com", }); if (!result.rewritten) { throw new Error("expected rewritten=true"); } expect(result.operation).toBe("cover"); expect(result.targetPath.startsWith("/")).toBe(true); expect(result.targetPath).toContain("rs:fill:800:450"); expect(result.targetPath).toContain( "plain/s3://images/example.com/avatar@png" ); }); it("records caddy_imgproxy_rewrite_duration_ms via the injected clock", { timeout: 5000, }, () => { let tick = 1000; const clock: Clock = () => { tick += 5; return tick; }; const config = parseConfig({}); const handler = new ImgproxyHandler({ config, clock }); const result = handler.rewrite({ path: "/uv16:9/avatar.png", host: "images.example.com", }); expect(result.rewritten).toBe(true); // No exception means the clock-driven duration was recorded. }); it("reports passthrough for /unsafe paths", { timeout: 5000 }, () => { const config = parseConfig({}); const handler = new ImgproxyHandler({ config }); const result = handler.rewrite({ path: "/unsafe/foo/bar.png", host: "example.com", }); expect(result).toEqual({ rewritten: false, reason: "passthrough", }); }); it("reports passthrough for /health paths", { timeout: 5000 }, () => { const config = parseConfig({}); const handler = new ImgproxyHandler({ config }); const result = handler.rewrite({ path: "/health", host: "example.com", }); expect(result).toEqual({ rewritten: false, reason: "passthrough", }); }); it("reports invalid for unparsable paths and warns the logger", { timeout: 5000, }, () => { const { logger, warns } = makeLogger(); const config = parseConfig({}); const handler = new ImgproxyHandler({ config, logger }); const result = handler.rewrite({ path: "/garbage", host: "example.com", }); if (result.rewritten !== false || result.reason !== "invalid") { throw new Error("expected invalid result"); } expect(result.errorCode).toBe(CADDY_IMGPROXY_CODES.INVALID_URL); expect(result.errorMessage.length).toBeGreaterThan(0); expect(warns.length).toBe(1); expect(warns[0]?.context?.code).toBe(CADDY_IMGPROXY_CODES.INVALID_URL); }); it("raises INVALID_HOST when host is empty", { timeout: 5000 }, () => { const { logger, warns } = makeLogger(); const config = parseConfig({}); const handler = new ImgproxyHandler({ config, logger }); const result = handler.rewrite({ path: "/uv16:9/avatar.png", host: "", }); if (result.rewritten !== false || result.reason !== "invalid") { throw new Error("expected invalid result"); } expect(result.errorCode).toBe(CADDY_IMGPROXY_CODES.INVALID_HOST); expect(warns.length).toBe(1); }); it("uses the default no-op logger when not supplied", { timeout: 5000, }, () => { const config = parseConfig({}); const handler = new ImgproxyHandler({ config }); // No logger provided — default no-op must absorb the warn call. const result = handler.rewrite({ path: "/garbage", host: "example.com", }); expect(result.rewritten).toBe(false); }); it("NOOP_LOGGER every method is callable and returns undefined", { timeout: 5000, }, () => { expect(NOOP_LOGGER.debug("msg")).toBeUndefined(); expect(NOOP_LOGGER.info("msg")).toBeUndefined(); expect(NOOP_LOGGER.warn("msg")).toBeUndefined(); expect(NOOP_LOGGER.error("msg")).toBeUndefined(); }); });