Spaces:
Runtime error
Runtime error
File size: 9,941 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | /**
* 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"`);
});
});
|