From 2d80f94edafe09329b027424b32908632694553d Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Mon, 4 Sep 2023 16:26:49 -0300 Subject: fix(HTMLRewriter) buffer response before transform (#4418) * html rewriter response buffering * pipe the data when marked as used * fix empty response * add some fetch tests * deinit parent stream * fix decompression * keep byte_reader alive * update builds * remove nonsense * was not nonsense after all * protect tmp ret value from GC, fix readable strong ref deinit/init * fmt * if we detach the stream we cannot update the fetch stream * detach checking source * more tests, progress with javascript and Direct sink * drop support for pure readable stream for now * more fixes --------- Co-authored-by: Jarred Sumner --- test/js/workerd/html-rewriter.test.js | 954 ++++++++++++++++++++-------------- 1 file changed, 558 insertions(+), 396 deletions(-) (limited to 'test/js/workerd/html-rewriter.test.js') diff --git a/test/js/workerd/html-rewriter.test.js b/test/js/workerd/html-rewriter.test.js index 4af73743a..b5c1f7c76 100644 --- a/test/js/workerd/html-rewriter.test.js +++ b/test/js/workerd/html-rewriter.test.js @@ -1,6 +1,7 @@ -import { describe, it, expect } from "bun:test"; +import { describe, it, expect, beforeAll, afterAll } from "bun:test"; import { gcTick } from "harness"; - +import path from "path"; +import fs from "fs"; var setTimeoutAsync = (fn, delay) => { return new Promise((resolve, reject) => { setTimeout(() => { @@ -13,417 +14,578 @@ var setTimeoutAsync = (fn, delay) => { }); }; -describe("HTMLRewriter", () => { - it("HTMLRewriter: async replacement", async () => { - await gcTick(); - const res = new HTMLRewriter() - .on("div", { - async element(element) { - await setTimeoutAsync(() => { - element.setInnerContent("replace", { html: true }); - }, 5); - }, - }) - .transform(new Response("
example.com
")); - await gcTick(); - expect(await res.text()).toBe("
replace
"); - await gcTick(); - }); - - it("supports element handlers", async () => { - var rewriter = new HTMLRewriter(); - rewriter.on("div", { - element(element) { - element.setInnerContent("it worked!", { html: true }); +// describe("HTMLRewriter", () => { +// it("HTMLRewriter: async replacement", async () => { +// await gcTick(); +// const res = new HTMLRewriter() +// .on("div", { +// async element(element) { +// await setTimeoutAsync(() => { +// element.setInnerContent("replace", { html: true }); +// }, 5); +// }, +// }) +// .transform(new Response("
example.com
")); +// await gcTick(); +// expect(await res.text()).toBe("
replace
"); +// await gcTick(); +// }); + +// it("supports element handlers", async () => { +// var rewriter = new HTMLRewriter(); +// rewriter.on("div", { +// element(element) { +// element.setInnerContent("it worked!", { html: true }); +// }, +// }); +// var input = new Response("
hello
"); +// var output = rewriter.transform(input); +// expect(await output.text()).toBe("
it worked!
"); +// }); + +// it("(from file) supports element handlers", async () => { +// var rewriter = new HTMLRewriter(); +// rewriter.on("div", { +// element(element) { +// element.setInnerContent("it worked!", { html: true }); +// }, +// }); +// await Bun.write("/tmp/html-rewriter.txt.js", "
hello
"); +// var input = new Response(Bun.file("/tmp/html-rewriter.txt.js")); +// var output = rewriter.transform(input); +// expect(await output.text()).toBe("
it worked!
"); +// }); + +// it("supports attribute iterator", async () => { +// var rewriter = new HTMLRewriter(); +// var expected = [ +// ["first", ""], +// ["second", "alrihgt"], +// ["third", "123"], +// ["fourth", "5"], +// ["fifth", "helloooo"], +// ]; +// rewriter.on("div", { +// element(element2) { +// for (let attr of element2.attributes) { +// const stack = expected.shift(); +// expect(stack[0]).toBe(attr[0]); +// expect(stack[1]).toBe(attr[1]); +// } +// }, +// }); +// var input = new Response('
hello
'); +// var output = rewriter.transform(input); +// expect(await output.text()).toBe('
hello
'); +// expect(expected.length).toBe(0); +// }); + +// it("handles element specific mutations", async () => { +// // prepend/append +// let res = new HTMLRewriter() +// .on("p", { +// element(element) { +// element.prepend("prepend"); +// element.prepend("prepend html", { html: true }); +// element.append("append"); +// element.append("append html", { html: true }); +// }, +// }) +// .transform(new Response("

test

")); +// expect(await res.text()).toBe( +// [ +// "

", +// "prepend html", +// "<span>prepend</span>", +// "test", +// "<span>append</span>", +// "append html", +// "

", +// ].join(""), +// ); + +// // setInnerContent +// res = new HTMLRewriter() +// .on("p", { +// element(element) { +// element.setInnerContent("replace"); +// }, +// }) +// .transform(new Response("

test

")); +// expect(await res.text()).toBe("

<span>replace</span>

"); +// res = new HTMLRewriter() +// .on("p", { +// element(element) { +// element.setInnerContent("replace", { html: true }); +// }, +// }) +// .transform(new Response("

test

")); +// expect(await res.text()).toBe("

replace

"); + +// // removeAndKeepContent +// res = new HTMLRewriter() +// .on("p", { +// element(element) { +// element.removeAndKeepContent(); +// }, +// }) +// .transform(new Response("

test

")); +// expect(await res.text()).toBe("test"); +// }); + +// it("handles element class properties", async () => { +// class Handler { +// constructor(content) { +// this.content = content; +// } + +// // noinspection JSUnusedGlobalSymbols +// element(element) { +// element.setInnerContent(this.content); +// } +// } +// const res = new HTMLRewriter().on("p", new Handler("new")).transform(new Response("

test

")); +// expect(await res.text()).toBe("

new

"); +// }); + +// const commentsMutationsInput = "

"; +// const commentsMutationsExpected = { +// beforeAfter: [ +// "

", +// "<span>before</span>", +// "before html", +// "", +// "after html", +// "<span>after</span>", +// "

", +// ].join(""), +// replace: "

<span>replace</span>

", +// replaceHtml: "

replace

", +// remove: "

", +// }; + +// const commentPropertiesMacro = async func => { +// const res = func(new HTMLRewriter(), comment => { +// expect(comment.removed).toBe(false); +// expect(comment.text).toBe("test"); +// comment.text = "new"; +// expect(comment.text).toBe("new"); +// }).transform(new Response("

")); +// expect(await res.text()).toBe("

"); +// }; + +// it("HTMLRewriter: handles comment properties", () => +// commentPropertiesMacro((rw, comments) => { +// rw.on("p", { comments }); +// return rw; +// })); + +// it("selector tests", async () => { +// const checkSelector = async (selector, input, expected) => { +// const res = new HTMLRewriter() +// .on(selector, { +// element(element) { +// element.setInnerContent("new"); +// }, +// }) +// .transform(new Response(input)); +// expect(await res.text()).toBe(expected); +// }; + +// await checkSelector("*", "

1

2

", "

new

new

"); +// await checkSelector("p", "

1

2

", "

1

new

"); +// await checkSelector( +// "p:nth-child(2)", +// "

1

2

3

", +// "

1

new

3

", +// ); +// await checkSelector( +// "p:first-child", +// "

1

2

3

", +// "

new

2

3

", +// ); +// await checkSelector( +// "p:nth-of-type(2)", +// "

1

2

3

4

5

", +// "

1

2

new

4

5

", +// ); +// await checkSelector( +// "p:first-of-type", +// "

1

2

3

", +// "

1

new

3

", +// ); +// await checkSelector( +// "p:not(:first-child)", +// "

1

2

3

", +// "

1

new

new

", +// ); +// await checkSelector("p.red", '

1

2

', '

new

2

'); +// await checkSelector("h1#header", '

1

2

', '

new

2

'); +// await checkSelector("p[data-test]", "

1

2

", "

new

2

"); +// await checkSelector( +// 'p[data-test="one"]', +// '

1

2

', +// '

new

2

', +// ); +// await checkSelector( +// 'p[data-test="one" i]', +// '

1

2

3

', +// '

new

new

3

', +// ); +// await checkSelector( +// 'p[data-test="one" s]', +// '

1

2

3

', +// '

new

2

3

', +// ); +// await checkSelector( +// 'p[data-test~="two"]', +// '

1

2

3

', +// '

new

new

3

', +// ); +// await checkSelector( +// 'p[data-test^="a"]', +// '

1

2

3

', +// '

new

new

3

', +// ); +// await checkSelector( +// 'p[data-test$="1"]', +// '

1

2

3

', +// '

new

2

new

', +// ); +// await checkSelector( +// 'p[data-test*="b"]', +// '

1

2

3

', +// '

new

new

3

', +// ); +// await checkSelector( +// 'p[data-test|="a"]', +// '

1

2

3

', +// '

new

new

3

', +// ); +// await checkSelector( +// "div span", +// "

1

23
", +// "

new

new3
", +// ); +// await checkSelector( +// "div > span", +// "

1

23
", +// "

1

new3
", +// ); +// }); + +// it("supports deleting innerContent", async () => { +// expect( +// await new HTMLRewriter() +// .on("div", { +// element(elem) { +// // https://github.com/oven-sh/bun/issues/2323 +// elem.setInnerContent(""); +// }, +// }) +// .transform(new Response("
content
")) +// .text(), +// ).toEqual("
"); +// }); + +// it("supports deleting innerHTML", async () => { +// expect( +// await new HTMLRewriter() +// .on("div", { +// element(elem) { +// // https://github.com/oven-sh/bun/issues/2323 +// elem.setInnerContent("", { html: true }); +// }, +// }) +// .transform(new Response("
content
")) +// .text(), +// ).toEqual("
"); +// }); + +// it("it supports lastInTextNode", async () => { +// let lastInTextNode; + +// await new HTMLRewriter() +// .on("p", { +// text(text) { +// lastInTextNode ??= text.lastInTextNode; +// }, +// }) +// .transform(new Response("

Lorem ipsum!

")) +// .text(); + +// expect(lastInTextNode).toBeBoolean(); +// }); + +// it("it supports selfClosing", async () => { +// const selfClosing = {}; +// await new HTMLRewriter() +// .on("*", { +// element(el) { +// selfClosing[el.tagName] = el.selfClosing; +// }, +// }) + +// .transform(new Response("

Lorem ipsum!

")) +// .text(); + +// expect(selfClosing).toEqual({ +// p: false, +// br: false, +// div: true, +// }); +// }); + +// it("it supports canHaveContent", async () => { +// const canHaveContent = {}; +// await new HTMLRewriter() +// .on("*", { +// element(el) { +// canHaveContent[el.tagName] = el.canHaveContent; +// }, +// }) +// .transform(new Response("

Lorem ipsum!

")) +// .text(); + +// expect(canHaveContent).toEqual({ +// p: true, +// br: false, +// div: true, +// svg: true, +// circle: false, +// }); +// }); +// }); + +// // By not segfaulting, this test passes +// it("#3334 regression", async () => { +// for (let i = 0; i < 10; i++) { +// const headers = new Headers({ +// "content-type": "text/html", +// }); +// const response = new Response("
content
", { headers }); + +// const result = await new HTMLRewriter() +// .on("div", { +// element(elem) { +// elem.setInnerContent("new"); +// }, +// }) +// .transform(response) +// .text(); +// expect(result).toEqual("
new
"); +// } +// Bun.gc(true); +// }); + +// it("#3489", async () => { +// var el; +// await new HTMLRewriter() +// .on("p", { +// element(element) { +// el = element.getAttribute("id"); +// }, +// }) +// .transform(new Response('

')) +// .text(); +// expect(el).toEqual("Šžõäöü"); +// }); + +// it("get attribute - ascii", async () => { +// for (let i = 0; i < 10; i++) { +// var el; +// await new HTMLRewriter() +// .on("p", { +// element(element) { +// el = element.getAttribute("id"); +// }, +// }) +// .transform(new Response(`

`)) +// .text(); +// expect(el).toEqual("asciii"); +// } +// }); + +// it("#3520", async () => { +// const pairs = []; + +// await new HTMLRewriter() +// .on("p", { +// element(element) { +// for (const pair of element.attributes) { +// pairs.push(pair); +// } +// }, +// }) +// .transform(new Response('

')) +// .text(); + +// expect(pairs).toEqual([ +// ["šž", "Õäöü"], +// ["ab", "Õäöü"], +// ["šž", "Õäöü"], +// ["šž", "dc"], +// ["šž", "🕵🏻"], +// ]); +// }); + +const fixture_html = path.join(import.meta.dir, "../web/fetch/fixture.html"); +const fixture_html_content = fs.readFileSync(fixture_html); +const fixture_html_gz = path.join(import.meta.dir, "../web/fetch/fixture.html.gz"); +const fixture_html_gz_content = fs.readFileSync(fixture_html_gz); +function getStream(type, fixture) { + const data = fixture === "gz" ? fixture_html_gz_content : fixture_html_content; + const half = parseInt(data.length / 2, 10); + + if (type === "direct") { + return new ReadableStream({ + type: "direct", + async pull(controller) { + controller.write(data.slice(0, half)); + await controller.flush(); + controller.write(data.slice(half)); + await controller.flush(); + controller.close(); }, }); - var input = new Response("
hello
"); - var output = rewriter.transform(input); - expect(await output.text()).toBe("
it worked!
"); - }); + } - it("(from file) supports element handlers", async () => { - var rewriter = new HTMLRewriter(); - rewriter.on("div", { - element(element) { - element.setInnerContent("it worked!", { html: true }); - }, - }); - await Bun.write("/tmp/html-rewriter.txt.js", "
hello
"); - var input = new Response(Bun.file("/tmp/html-rewriter.txt.js")); - var output = rewriter.transform(input); - expect(await output.text()).toBe("
it worked!
"); + return new ReadableStream({ + async pull(controller) { + controller.enqueue(data.slice(0, half)); + await Bun.sleep(15); + controller.enqueue(data.slice(half)); + await Bun.sleep(15); + controller.close(); + }, }); - - it("supports attribute iterator", async () => { - var rewriter = new HTMLRewriter(); - var expected = [ - ["first", ""], - ["second", "alrihgt"], - ["third", "123"], - ["fourth", "5"], - ["fifth", "helloooo"], - ]; - rewriter.on("div", { - element(element2) { - for (let attr of element2.attributes) { - const stack = expected.shift(); - expect(stack[0]).toBe(attr[0]); - expect(stack[1]).toBe(attr[1]); +} +function createServer(tls) { + return Bun.serve({ + port: 0, + tls, + async fetch(req) { + const is_compressed = req.url.endsWith("gz"); + + let payload; + if (req.url.indexOf("chunked") !== -1) { + if (req.url.indexOf("direct")) { + payload = getStream("direct", is_compressed ? "gz" : "default"); + } else { + payload = getStream("default", is_compressed ? "gz" : "default"); } - }, - }); - var input = new Response('
hello
'); - var output = rewriter.transform(input); - expect(await output.text()).toBe('
hello
'); - expect(expected.length).toBe(0); - }); - - it("handles element specific mutations", async () => { - // prepend/append - let res = new HTMLRewriter() - .on("p", { - element(element) { - element.prepend("prepend"); - element.prepend("prepend html", { html: true }); - element.append("append"); - element.append("append html", { html: true }); - }, - }) - .transform(new Response("

test

")); - expect(await res.text()).toBe( - [ - "

", - "prepend html", - "<span>prepend</span>", - "test", - "<span>append</span>", - "append html", - "

", - ].join(""), - ); - - // setInnerContent - res = new HTMLRewriter() - .on("p", { - element(element) { - element.setInnerContent("replace"); - }, - }) - .transform(new Response("

test

")); - expect(await res.text()).toBe("

<span>replace</span>

"); - res = new HTMLRewriter() - .on("p", { - element(element) { - element.setInnerContent("replace", { html: true }); - }, - }) - .transform(new Response("

test

")); - expect(await res.text()).toBe("

replace

"); - - // removeAndKeepContent - res = new HTMLRewriter() - .on("p", { - element(element) { - element.removeAndKeepContent(); - }, - }) - .transform(new Response("

test

")); - expect(await res.text()).toBe("test"); - }); - - it("handles element class properties", async () => { - class Handler { - constructor(content) { - this.content = content; + } else if (req.url.indexOf("file") !== -1) { + payload = is_compressed ? Bun.file(fixture_html_gz) : Bun.file(fixture_html); + } else { + payload = is_compressed ? fixture_html_gz_content : fixture_html_content; } - // noinspection JSUnusedGlobalSymbols - element(element) { - element.setInnerContent(this.content); - } - } - const res = new HTMLRewriter().on("p", new Handler("new")).transform(new Response("

test

")); - expect(await res.text()).toBe("

new

"); - }); - - const commentsMutationsInput = "

"; - const commentsMutationsExpected = { - beforeAfter: [ - "

", - "<span>before</span>", - "before html", - "", - "after html", - "<span>after</span>", - "

", - ].join(""), - replace: "

<span>replace</span>

", - replaceHtml: "

replace

", - remove: "

", - }; - - const commentPropertiesMacro = async func => { - const res = func(new HTMLRewriter(), comment => { - expect(comment.removed).toBe(false); - expect(comment.text).toBe("test"); - comment.text = "new"; - expect(comment.text).toBe("new"); - }).transform(new Response("

")); - expect(await res.text()).toBe("

"); - }; - - it("HTMLRewriter: handles comment properties", () => - commentPropertiesMacro((rw, comments) => { - rw.on("p", { comments }); - return rw; - })); - - it("selector tests", async () => { - const checkSelector = async (selector, input, expected) => { - const res = new HTMLRewriter() - .on(selector, { - element(element) { - element.setInnerContent("new"); - }, - }) - .transform(new Response(input)); - expect(await res.text()).toBe(expected); - }; - - await checkSelector("*", "

1

2

", "

new

new

"); - await checkSelector("p", "

1

2

", "

1

new

"); - await checkSelector( - "p:nth-child(2)", - "

1

2

3

", - "

1

new

3

", - ); - await checkSelector( - "p:first-child", - "

1

2

3

", - "

new

2

3

", - ); - await checkSelector( - "p:nth-of-type(2)", - "

1

2

3

4

5

", - "

1

2

new

4

5

", - ); - await checkSelector( - "p:first-of-type", - "

1

2

3

", - "

1

new

3

", - ); - await checkSelector( - "p:not(:first-child)", - "

1

2

3

", - "

1

new

new

", - ); - await checkSelector("p.red", '

1

2

', '

new

2

'); - await checkSelector("h1#header", '

1

2

', '

new

2

'); - await checkSelector("p[data-test]", "

1

2

", "

new

2

"); - await checkSelector( - 'p[data-test="one"]', - '

1

2

', - '

new

2

', - ); - await checkSelector( - 'p[data-test="one" i]', - '

1

2

3

', - '

new

new

3

', - ); - await checkSelector( - 'p[data-test="one" s]', - '

1

2

3

', - '

new

2

3

', - ); - await checkSelector( - 'p[data-test~="two"]', - '

1

2

3

', - '

new

new

3

', - ); - await checkSelector( - 'p[data-test^="a"]', - '

1

2

3

', - '

new

new

3

', - ); - await checkSelector( - 'p[data-test$="1"]', - '

1

2

3

', - '

new

2

new

', - ); - await checkSelector( - 'p[data-test*="b"]', - '

1

2

3

', - '

new

new

3

', - ); - await checkSelector( - 'p[data-test|="a"]', - '

1

2

3

', - '

new

new

3

', - ); - await checkSelector( - "div span", - "

1

23
", - "

new

new3
", - ); - await checkSelector( - "div > span", - "

1

23
", - "

1

new3
", - ); - }); - - it("supports deleting innerContent", async () => { - expect( - await new HTMLRewriter() - .on("div", { - element(elem) { - // https://github.com/oven-sh/bun/issues/2323 - elem.setInnerContent(""); - }, - }) - .transform(new Response("
content
")) - .text(), - ).toEqual("
"); - }); - - it("supports deleting innerHTML", async () => { - expect( - await new HTMLRewriter() - .on("div", { - element(elem) { - // https://github.com/oven-sh/bun/issues/2323 - elem.setInnerContent("", { html: true }); - }, - }) - .transform(new Response("
content
")) - .text(), - ).toEqual("
"); - }); + let headers = { + "content-type": "text/html", + }; - it("it supports lastInTextNode", async () => { - let lastInTextNode; - - await new HTMLRewriter() - .on("p", { - text(text) { - lastInTextNode ??= text.lastInTextNode; - }, - }) - .transform(new Response("

Lorem ipsum!

")) - .text(); - - expect(lastInTextNode).toBeBoolean(); - }); - - it("it supports selfClosing", async () => { - const selfClosing = {}; - await new HTMLRewriter() - .on("*", { - element(el) { - selfClosing[el.tagName] = el.selfClosing; - }, - }) - - .transform(new Response("

Lorem ipsum!

")) - .text(); + if (is_compressed) { + headers["content-encoding"] = "gzip"; + } - expect(selfClosing).toEqual({ - p: false, - br: false, - div: true, - }); + return new Response(payload, { headers }); + }, }); - - it("it supports canHaveContent", async () => { - const canHaveContent = {}; - await new HTMLRewriter() - .on("*", { - element(el) { - canHaveContent[el.tagName] = el.canHaveContent; - }, - }) - .transform(new Response("

Lorem ipsum!

")) - .text(); - - expect(canHaveContent).toEqual({ - p: true, - br: false, - div: true, - svg: true, - circle: false, - }); +} +let http_server; +let https_server; +beforeAll(() => { + http_server = createServer(); + https_server = createServer({ + cert: "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAKLdQVPy90jjMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\nBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\naWRnaXRzIFB0eSBMdGQwHhcNMTkwMjAzMTQ0OTM1WhcNMjAwMjAzMTQ0OTM1WjBF\nMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\nZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA7i7IIEdICTiSTVx+ma6xHxOtcbd6wGW3nkxlCkJ1UuV8NmY5ovMsGnGD\nhJJtUQ2j5ig5BcJUf3tezqCNW4tKnSOgSISfEAKvpn2BPvaFq3yx2Yjz0ruvcGKp\nDMZBXmB/AAtGyN/UFXzkrcfppmLHJTaBYGG6KnmU43gPkSDy4iw46CJFUOupc51A\nFIz7RsE7mbT1plCM8e75gfqaZSn2k+Wmy+8n1HGyYHhVISRVvPqkS7gVLSVEdTea\nUtKP1Vx/818/HDWk3oIvDVWI9CFH73elNxBkMH5zArSNIBTehdnehyAevjY4RaC/\nkK8rslO3e4EtJ9SnA4swOjCiqAIQEwIDAQABo1AwTjAdBgNVHQ4EFgQUv5rc9Smm\n9c4YnNf3hR49t4rH4yswHwYDVR0jBBgwFoAUv5rc9Smm9c4YnNf3hR49t4rH4ysw\nDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATcL9CAAXg0u//eYUAlQa\nL+l8yKHS1rsq1sdmx7pvsmfZ2g8ONQGfSF3TkzkI2OOnCBokeqAYuyT8awfdNUtE\nEHOihv4ZzhK2YZVuy0fHX2d4cCFeQpdxno7aN6B37qtsLIRZxkD8PU60Dfu9ea5F\nDDynnD0TUabna6a0iGn77yD8GPhjaJMOz3gMYjQFqsKL252isDVHEDbpVxIzxPmN\nw1+WK8zRNdunAcHikeoKCuAPvlZ83gDQHp07dYdbuZvHwGj0nfxBLc9qt90XsBtC\n4IYR7c/bcLMmKXYf0qoQ4OzngsnPI5M+v9QEHvYWaKVwFY4CTcSNJEwfXw+BAeO5\nOA==\n-----END CERTIFICATE-----", + key: "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDuLsggR0gJOJJN\nXH6ZrrEfE61xt3rAZbeeTGUKQnVS5Xw2Zjmi8ywacYOEkm1RDaPmKDkFwlR/e17O\noI1bi0qdI6BIhJ8QAq+mfYE+9oWrfLHZiPPSu69wYqkMxkFeYH8AC0bI39QVfOSt\nx+mmYsclNoFgYboqeZTjeA+RIPLiLDjoIkVQ66lznUAUjPtGwTuZtPWmUIzx7vmB\n+pplKfaT5abL7yfUcbJgeFUhJFW8+qRLuBUtJUR1N5pS0o/VXH/zXz8cNaTegi8N\nVYj0IUfvd6U3EGQwfnMCtI0gFN6F2d6HIB6+NjhFoL+QryuyU7d7gS0n1KcDizA6\nMKKoAhATAgMBAAECggEAd5g/3o1MK20fcP7PhsVDpHIR9faGCVNJto9vcI5cMMqP\n6xS7PgnSDFkRC6EmiLtLn8Z0k2K3YOeGfEP7lorDZVG9KoyE/doLbpK4MfBAwBG1\nj6AHpbmd5tVzQrnNmuDjBBelbDmPWVbD0EqAFI6mphXPMqD/hFJWIz1mu52Kt2s6\n++MkdqLO0ORDNhKmzu6SADQEcJ9Suhcmv8nccMmwCsIQAUrfg3qOyqU4//8QB8ZM\njosO3gMUesihVeuF5XpptFjrAliPgw9uIG0aQkhVbf/17qy0XRi8dkqXj3efxEDp\n1LSqZjBFiqJlFchbz19clwavMF/FhxHpKIhhmkkRSQKBgQD9blaWSg/2AGNhRfpX\nYq+6yKUkUD4jL7pmX1BVca6dXqILWtHl2afWeUorgv2QaK1/MJDH9Gz9Gu58hJb3\nymdeAISwPyHp8euyLIfiXSAi+ibKXkxkl1KQSweBM2oucnLsNne6Iv6QmXPpXtro\nnTMoGQDS7HVRy1on5NQLMPbUBQKBgQDwmN+um8F3CW6ZV1ZljJm7BFAgNyJ7m/5Q\nYUcOO5rFbNsHexStrx/h8jYnpdpIVlxACjh1xIyJ3lOCSAWfBWCS6KpgeO1Y484k\nEYhGjoUsKNQia8UWVt+uWnwjVSDhQjy5/pSH9xyFrUfDg8JnSlhsy0oC0C/PBjxn\nhxmADSLnNwKBgQD2A51USVMTKC9Q50BsgeU6+bmt9aNMPvHAnPf76d5q78l4IlKt\nwMs33QgOExuYirUZSgjRwknmrbUi9QckRbxwOSqVeMOwOWLm1GmYaXRf39u2CTI5\nV9gTMHJ5jnKd4gYDnaA99eiOcBhgS+9PbgKSAyuUlWwR2ciL/4uDzaVeDQKBgDym\nvRSeTRn99bSQMMZuuD5N6wkD/RxeCbEnpKrw2aZVN63eGCtkj0v9LCu4gptjseOu\n7+a4Qplqw3B/SXN5/otqPbEOKv8Shl/PT6RBv06PiFKZClkEU2T3iH27sws2EGru\nw3C3GaiVMxcVewdg1YOvh5vH8ZVlxApxIzuFlDvnAoGAN5w+gukxd5QnP/7hcLDZ\nF+vesAykJX71AuqFXB4Wh/qFY92CSm7ImexWA/L9z461+NKeJwb64Nc53z59oA10\n/3o2OcIe44kddZXQVP6KTZBd7ySVhbtOiK3/pCy+BQRsrC7d71W914DxNWadwZ+a\njtwwKjDzmPwdIXDSQarCx0U=\n-----END PRIVATE KEY-----", + passphrase: "1234", }); }); -// By not segfaulting, this test passes -it("#3334 regression", async () => { - for (let i = 0; i < 10; i++) { - const headers = new Headers({ - "content-type": "text/html", - }); - const response = new Response("
content
", { headers }); - - const result = await new HTMLRewriter() - .on("div", { - element(elem) { - elem.setInnerContent("new"); - }, - }) - .transform(response) - .text(); - expect(result).toEqual("
new
"); - } - Bun.gc(true); -}); - -it("#3489", async () => { - var el; - await new HTMLRewriter() - .on("p", { - element(element) { - el = element.getAttribute("id"); - }, - }) - .transform(new Response('

')) - .text(); - expect(el).toEqual("Šžõäöü"); +afterAll(() => { + http_server?.stop(true); + https_server?.stop(true); }); -it("get attribute - ascii", async () => { - for (let i = 0; i < 10; i++) { - var el; - await new HTMLRewriter() - .on("p", { - element(element) { - el = element.getAttribute("id"); +const request_types = ["/", "/gzip", "/chunked/gzip", "/chunked", "/file", "/file/gzip"]; +["http", "https"].forEach(protocol => { + request_types.forEach(url => { + //TODO: change this when Bun.file supports https + const test = url.indexOf("file") !== -1 && protocol === "https" ? it.todo : it; + test(`works with ${protocol} fetch using ${url}`, async () => { + const server = protocol === "http" ? http_server : https_server; + const server_url = `${protocol}://${server?.hostname}:${server?.port}`; + const res = await fetch(`${server_url}${url}`); + let calls = 0; + const rw = new HTMLRewriter(); + rw.on("h1", { + text() { + calls++; }, - }) - .transform(new Response(`

`)) - .text(); - expect(el).toEqual("asciii"); - } + }); + + const transformed = rw.transform(res); + if (transformed instanceof Error) throw transformed; + const body = await transformed.text(); + let trimmed = body?.trim(); + expect(body).toBe(fixture_html_content.toString("utf8")); + expect(trimmed).toEndWith(""); + expect(trimmed).toStartWith(""); + expect(calls).toBeGreaterThan(0); + }); + }); }); -it("#3520", async () => { - const pairs = []; - - await new HTMLRewriter() - .on("p", { - element(element) { - for (const pair of element.attributes) { - pairs.push(pair); - } +const payloads = [ + { + name: "direct", + data: getStream("direct", "none"), + test: it.todo, + }, + { + name: "default", + data: getStream("default", "none"), + test: it.todo, + }, + { + name: "file", + data: Bun.file(fixture_html), + test: it, + }, + { + name: "blob", + data: new Blob([fixture_html_content]), + test: it, + }, + { + name: "buffer", + data: fixture_html_content, + test: it, + }, + { + name: "string", + data: fixture_html_content.toString("utf8"), + test: it, + }, +]; + +payloads.forEach(type => { + type.test(`works with payload of type ${type.name}`, async () => { + let calls = 0; + const rw = new HTMLRewriter(); + rw.on("h1", { + text() { + calls++; }, - }) - .transform(new Response('

')) - .text(); - - expect(pairs).toEqual([ - ["šž", "Õäöü"], - ["ab", "Õäöü"], - ["šž", "Õäöü"], - ["šž", "dc"], - ["šž", "🕵🏻"], - ]); + }); + const transformed = rw.transform(new Response(type.data)); + if (transformed instanceof Error) throw transformed; + const body = await transformed.text(); + let trimmed = body?.trim(); + expect(body).toBe(fixture_html_content.toString("utf8")); + expect(trimmed).toEndWith(""); + expect(trimmed).toStartWith(""); + expect(calls).toBeGreaterThan(0); + }); }); -- cgit v1.2.3