Spaces:
Runtime error
Runtime error
| /** | |
| * Unit tests for src/lib/services/htmlRewriter.ts | |
| * | |
| * Tests HTML rewriting for the embedded-service reverse proxy: | |
| * - <base href> injection | |
| * - Path-absolute URL rewriting in <a>, <link>, <script>, <img>, <form>, <iframe>, <source> | |
| * - Non-rewrite cases: external URLs, protocol-relative, mailto:, javascript: | |
| * - Edge cases: HTML without <head>, empty href, multi-URL srcset | |
| */ | |
| import { describe, it } from "node:test"; | |
| import assert from "node:assert/strict"; | |
| import { rewriteHtml } from "../../../src/lib/services/htmlRewriter.ts"; | |
| const PREFIX = "/dashboard/providers/services/9router/embed"; | |
| // Helper: assert string contains substring | |
| function assertContains(haystack: string, needle: string, msg?: string): void { | |
| assert.ok( | |
| haystack.includes(needle), | |
| msg ?? `Expected output to contain: ${JSON.stringify(needle)}\nActual: ${haystack}` | |
| ); | |
| } | |
| function assertNotContains(haystack: string, needle: string, msg?: string): void { | |
| assert.ok( | |
| !haystack.includes(needle), | |
| msg ?? `Expected output NOT to contain: ${JSON.stringify(needle)}\nActual: ${haystack}` | |
| ); | |
| } | |
| // βββ base tag injection βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe("rewriteHtml β <base> injection", () => { | |
| it("inserts <base href> as first child of <head>", () => { | |
| const input = "<html><head></head><body></body></html>"; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, `<base href="${PREFIX}/">`); | |
| }); | |
| it("inserts <base href> before other head children", () => { | |
| const input = "<html><head><title>T</title></head><body></body></html>"; | |
| const out = rewriteHtml(input, PREFIX); | |
| const baseIdx = out.indexOf("<base "); | |
| const titleIdx = out.indexOf("<title>"); | |
| assert.ok( | |
| baseIdx < titleIdx, | |
| `<base> should appear before <title>; base=${baseIdx}, title=${titleIdx}` | |
| ); | |
| }); | |
| it("strips trailing slash from prefix before forming base href", () => { | |
| const out = rewriteHtml("<html><head></head><body></body></html>", `${PREFIX}/`); | |
| assertContains(out, `<base href="${PREFIX}/">`); | |
| // Should not have double slash | |
| assertNotContains(out, `<base href="${PREFIX}//">`); | |
| }); | |
| it("handles HTML without <head> by creating one", () => { | |
| const out = rewriteHtml("<html><body><p>hello</p></body></html>", PREFIX); | |
| assertContains(out, `<base href="${PREFIX}/">`); | |
| }); | |
| it("updates existing <base> href instead of inserting a second one", () => { | |
| const input = '<html><head><base href="/old/"></head><body></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, `<base href="${PREFIX}/">`); | |
| assertNotContains(out, 'href="/old/"'); | |
| // Exactly one <base tag | |
| const matches = out.match(/<base /g); | |
| assert.equal(matches?.length ?? 0, 1, "should have exactly one <base> element"); | |
| }); | |
| }); | |
| // βββ link rewriting βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe("rewriteHtml β <a href> rewriting", () => { | |
| it("rewrites path-absolute <a href> to prefixed URL", () => { | |
| const input = '<html><head></head><body><a href="/foo">link</a></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, `href="${PREFIX}/foo"`); | |
| }); | |
| it("does NOT rewrite https:// external URL", () => { | |
| const input = '<html><head></head><body><a href="https://example.com/foo">x</a></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, 'href="https://example.com/foo"'); | |
| }); | |
| it("does NOT rewrite http:// external URL", () => { | |
| const input = '<html><head></head><body><a href="http://external.com">x</a></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, 'href="http://external.com"'); | |
| }); | |
| it("does NOT rewrite protocol-relative URL (//cdn)", () => { | |
| const input = '<html><head></head><body><a href="//cdn.example.com/x">x</a></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, 'href="//cdn.example.com/x"'); | |
| }); | |
| it("does NOT rewrite mailto: URL", () => { | |
| const input = | |
| '<html><head></head><body><a href="mailto:admin@example.com">email</a></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, 'href="mailto:admin@example.com"'); | |
| }); | |
| it("does NOT rewrite javascript: URL", () => { | |
| const input = '<html><head></head><body><a href="javascript:void(0)">x</a></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, 'href="javascript:void(0)"'); | |
| }); | |
| it("does NOT rewrite relative URL (no leading slash)", () => { | |
| const input = '<html><head></head><body><a href="relative/path">x</a></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, 'href="relative/path"'); | |
| }); | |
| it("does NOT rewrite anchor-only href", () => { | |
| const input = '<html><head></head><body><a href="#section">x</a></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, 'href="#section"'); | |
| }); | |
| }); | |
| // βββ other element rewriting ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe("rewriteHtml β other element attributes", () => { | |
| it("rewrites <link href='/stylesheet.css'>", () => { | |
| const input = | |
| '<html><head><link rel="stylesheet" href="/styles.css"></head><body></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, `href="${PREFIX}/styles.css"`); | |
| }); | |
| it("rewrites <script src='/app.js'>", () => { | |
| const input = '<html><head></head><body><script src="/app.js"></script></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, `src="${PREFIX}/app.js"`); | |
| }); | |
| it("rewrites <img src='/logo.png'>", () => { | |
| const input = '<html><head></head><body><img src="/logo.png"></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, `src="${PREFIX}/logo.png"`); | |
| }); | |
| it("rewrites <form action='/submit'>", () => { | |
| const input = | |
| '<html><head></head><body><form action="/submit"><button>Go</button></form></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, `action="${PREFIX}/submit"`); | |
| }); | |
| it("rewrites <iframe src='/frame'>", () => { | |
| const input = '<html><head></head><body><iframe src="/frame"></iframe></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, `src="${PREFIX}/frame"`); | |
| }); | |
| it("rewrites <source src='/video.mp4'>", () => { | |
| const input = '<html><head></head><body><source src="/video.mp4"></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, `src="${PREFIX}/video.mp4"`); | |
| }); | |
| it("does NOT rewrite img srcset with commas (multi-URL β v1 limitation)", () => { | |
| const input = | |
| '<html><head></head><body><img srcset="/img.png 1x, /img@2x.png 2x"></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| // Original srcset must be preserved unchanged | |
| assertContains(out, 'srcset="/img.png 1x, /img@2x.png 2x"'); | |
| }); | |
| }); | |
| // βββ non-anchor markup preservation ββββββββββββββββββββββββββββββββββββββββββ | |
| describe("rewriteHtml β preserves non-affected markup", () => { | |
| it("preserves text content inside elements", () => { | |
| const input = "<html><head></head><body><p>Hello World</p></body></html>"; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, "Hello World"); | |
| }); | |
| it("preserves existing non-rewritten attributes", () => { | |
| const input = | |
| '<html><head></head><body><a href="/foo" class="nav" id="link1">x</a></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, 'class="nav"'); | |
| assertContains(out, 'id="link1"'); | |
| }); | |
| it("preserves data attributes unchanged", () => { | |
| const input = | |
| '<html><head></head><body><div data-url="/foo" data-val="bar"></div></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| // data-url is not in REWRITABLE_ATTRS β stays as-is | |
| assertContains(out, 'data-url="/foo"'); | |
| }); | |
| it("preserves empty href unchanged", () => { | |
| const input = '<html><head></head><body><a href="">empty</a></body></html>'; | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, 'href=""'); | |
| }); | |
| }); | |
| // βββ edge cases βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe("rewriteHtml β edge cases", () => { | |
| it("handles empty HTML string gracefully", () => { | |
| const out = rewriteHtml("", PREFIX); | |
| assertContains(out, `<base href="${PREFIX}/">`); | |
| }); | |
| it("handles HTML fragment (no <html> tag) β parse5 auto-completes tree", () => { | |
| const out = rewriteHtml('<a href="/foo">link</a>', PREFIX); | |
| assertContains(out, `<base href="${PREFIX}/">`); | |
| assertContains(out, `href="${PREFIX}/foo"`); | |
| }); | |
| it("multiple path-absolute URLs in same document are all rewritten", () => { | |
| const input = [ | |
| "<html><head>", | |
| '<link href="/a.css">', | |
| "</head><body>", | |
| '<a href="/page1">1</a>', | |
| '<a href="/page2">2</a>', | |
| '<script src="/app.js"></script>', | |
| "</body></html>", | |
| ].join(""); | |
| const out = rewriteHtml(input, PREFIX); | |
| assertContains(out, `href="${PREFIX}/a.css"`); | |
| assertContains(out, `href="${PREFIX}/page1"`); | |
| assertContains(out, `href="${PREFIX}/page2"`); | |
| assertContains(out, `src="${PREFIX}/app.js"`); | |
| }); | |
| }); | |