From a417772bd7dc4f8508621ec7b2fb75f9bfeca955 Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Thu, 8 Feb 2024 13:09:47 -0700 Subject: chore: Promote some integration tests to js_unit_tests (#22355) - Move a workers test to js_unit_tests and make it work - (slightly) repair the websocketstream_test and make it a JS unit test. This test was being ignored and rotted quite a bit, but there's some value in running as much of it as we can. - Merge the two websocket test files --- cli/tests/integration/js_unit_tests.rs | 5 +- cli/tests/integration/run_tests.rs | 43 -- cli/tests/integration/worker_tests.rs | 6 - cli/tests/testdata/run/websocket_test.ts | 308 --------- cli/tests/testdata/run/websocketstream_test.ts | 335 ---------- cli/tests/testdata/workers/test.ts | 844 ------------------------- cli/tests/testdata/workers/test.ts.out | 3 - cli/tests/unit/request_test.ts | 2 +- cli/tests/unit/websocket_test.ts | 301 +++++++++ cli/tests/unit/websocketstream_test.ts | 353 +++++++++++ cli/tests/unit/worker_test.ts | 843 ++++++++++++++++++++++++ cli/tests/unit/worker_types.ts | 14 - 12 files changed, 1501 insertions(+), 1556 deletions(-) delete mode 100644 cli/tests/testdata/run/websocket_test.ts delete mode 100644 cli/tests/testdata/run/websocketstream_test.ts delete mode 100644 cli/tests/testdata/workers/test.ts delete mode 100644 cli/tests/testdata/workers/test.ts.out create mode 100644 cli/tests/unit/websocketstream_test.ts create mode 100644 cli/tests/unit/worker_test.ts delete mode 100644 cli/tests/unit/worker_types.ts (limited to 'cli/tests') diff --git a/cli/tests/integration/js_unit_tests.rs b/cli/tests/integration/js_unit_tests.rs index 951fc6f62..b037d473c 100644 --- a/cli/tests/integration/js_unit_tests.rs +++ b/cli/tests/integration/js_unit_tests.rs @@ -105,9 +105,10 @@ util::unit_test_factory!( webcrypto_test, webgpu_test, websocket_test, + websocketstream_test, webstorage_test, worker_permissions_test, - worker_types, + worker_test, write_file_test, write_text_file_test, ] @@ -123,7 +124,7 @@ fn js_unit_test(test: String) { .arg("cli/tests/config/deno.json") .arg("--no-lock") .arg("--unstable") - .arg("--location=http://js-unit-tests/foo/bar") + .arg("--location=http://127.0.0.1:4545/") .arg("--no-prompt"); // TODO(mmastrac): it would be better to just load a test CA for all tests diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index 56eea9c8d..3d14bb0bb 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -4390,49 +4390,6 @@ itest!(ext_flag_takes_precedence_over_extension { exit_code: 0, }); -#[test] -fn websocket() { - let _g = util::http_server(); - - let script = util::testdata_path().join("run/websocket_test.ts"); - let root_ca = util::testdata_path().join("tls/RootCA.pem"); - let status = util::deno_cmd() - .arg("test") - .arg("--unstable") - .arg("--allow-net") - .arg("--cert") - .arg(root_ca) - .arg(script) - .spawn() - .unwrap() - .wait() - .unwrap(); - - assert!(status.success()); -} - -#[ignore] -#[test] -fn websocketstream() { - let _g = util::http_server(); - - let script = util::testdata_path().join("run/websocketstream_test.ts"); - let root_ca = util::testdata_path().join("tls/RootCA.pem"); - let status = util::deno_cmd() - .arg("test") - .arg("--unstable") - .arg("--allow-net") - .arg("--cert") - .arg(root_ca) - .arg(script) - .spawn() - .unwrap() - .wait() - .unwrap(); - - assert!(status.success()); -} - #[tokio::test(flavor = "multi_thread")] async fn websocketstream_ping() { let _g = util::http_server(); diff --git a/cli/tests/integration/worker_tests.rs b/cli/tests/integration/worker_tests.rs index cbd63d809..e2d1ef868 100644 --- a/cli/tests/integration/worker_tests.rs +++ b/cli/tests/integration/worker_tests.rs @@ -1,11 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -itest!(workers { - args: "test --reload --location http://127.0.0.1:4545/ -A --unstable-worker-options workers/test.ts", - output: "workers/test.ts.out", - http_server: true, -}); - itest!(worker_error { args: "run -A workers/worker_error.ts", output: "workers/worker_error.ts.out", diff --git a/cli/tests/testdata/run/websocket_test.ts b/cli/tests/testdata/run/websocket_test.ts deleted file mode 100644 index b6c5744af..000000000 --- a/cli/tests/testdata/run/websocket_test.ts +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { - assert, - assertEquals, - assertThrows, - fail, -} from "../../../../test_util/std/assert/mod.ts"; - -Deno.test("invalid scheme", () => { - assertThrows(() => new WebSocket("foo://localhost:4242")); -}); - -Deno.test("fragment", () => { - assertThrows(() => new WebSocket("ws://localhost:4242/#")); - assertThrows(() => new WebSocket("ws://localhost:4242/#foo")); -}); - -Deno.test("duplicate protocols", () => { - assertThrows(() => new WebSocket("ws://localhost:4242", ["foo", "foo"])); -}); - -Deno.test("invalid server", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:2121"); - let err = false; - ws.onerror = () => { - err = true; - }; - ws.onclose = () => { - if (err) { - resolve(); - } else { - fail(); - } - }; - ws.onopen = () => fail(); - await promise; -}); - -Deno.test("connect & close", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - ws.onerror = () => fail(); - ws.onopen = () => { - ws.close(); - }; - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("connect & abort", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - ws.close(); - let err = false; - ws.onerror = () => { - err = true; - }; - ws.onclose = () => { - if (err) { - resolve(); - } else { - fail(); - } - }; - ws.onopen = () => fail(); - await promise; -}); - -Deno.test("connect & close custom valid code", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - ws.onerror = () => fail(); - ws.onopen = () => ws.close(1000); - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("connect & close custom invalid code", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - ws.onerror = () => fail(); - ws.onopen = () => { - assertThrows(() => ws.close(1001)); - ws.close(); - }; - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("connect & close custom valid reason", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - ws.onerror = () => fail(); - ws.onopen = () => ws.close(1000, "foo"); - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("connect & close custom invalid reason", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - ws.onerror = () => fail(); - ws.onopen = () => { - assertThrows(() => ws.close(1000, "".padEnd(124, "o"))); - ws.close(); - }; - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("echo string", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - ws.onerror = () => fail(); - ws.onopen = () => ws.send("foo"); - ws.onmessage = (e) => { - assertEquals(e.data, "foo"); - ws.close(); - }; - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("echo string tls", async () => { - const deferred1 = Promise.withResolvers(); - const deferred2 = Promise.withResolvers(); - const ws = new WebSocket("wss://localhost:4243"); - ws.onerror = () => fail(); - ws.onopen = () => ws.send("foo"); - ws.onmessage = (e) => { - assertEquals(e.data, "foo"); - ws.close(); - deferred1.resolve(); - }; - ws.onclose = () => { - deferred2.resolve(); - }; - await deferred1.promise; - await deferred2.promise; -}); - -Deno.test("websocket error", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("wss://localhost:4242"); - ws.onopen = () => fail(); - ws.onerror = (err) => { - assert(err instanceof ErrorEvent); - assertEquals( - err.message, - "NetworkError: failed to connect to WebSocket: received corrupt message of type InvalidContentType", - ); - resolve(); - }; - await promise; -}); - -Deno.test("echo blob with binaryType blob", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - const blob = new Blob(["foo"]); - ws.onerror = () => fail(); - ws.onopen = () => ws.send(blob); - ws.onmessage = (e) => { - e.data.text().then((actual: string) => { - blob.text().then((expected) => { - assertEquals(actual, expected); - }); - }); - ws.close(); - }; - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("echo blob with binaryType arraybuffer", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - ws.binaryType = "arraybuffer"; - const blob = new Blob(["foo"]); - ws.onerror = () => fail(); - ws.onopen = () => ws.send(blob); - ws.onmessage = (e) => { - blob.arrayBuffer().then((expected) => { - assertEquals(e.data, expected); - }); - ws.close(); - }; - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("echo uint8array with binaryType blob", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - const uint = new Uint8Array([102, 111, 111]); - ws.onerror = () => fail(); - ws.onopen = () => ws.send(uint); - ws.onmessage = (e) => { - e.data.arrayBuffer().then((actual: ArrayBuffer) => { - assertEquals(actual, uint.buffer); - }); - ws.close(); - }; - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("echo uint8array with binaryType arraybuffer", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - ws.binaryType = "arraybuffer"; - const uint = new Uint8Array([102, 111, 111]); - ws.onerror = () => fail(); - ws.onopen = () => ws.send(uint); - ws.onmessage = (e) => { - assertEquals(e.data, uint.buffer); - ws.close(); - }; - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("echo arraybuffer with binaryType blob", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - const buffer = new ArrayBuffer(3); - ws.onerror = () => fail(); - ws.onopen = () => ws.send(buffer); - ws.onmessage = (e) => { - e.data.arrayBuffer().then((actual: ArrayBuffer) => { - assertEquals(actual, buffer); - }); - ws.close(); - }; - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("echo arraybuffer with binaryType arraybuffer", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - ws.binaryType = "arraybuffer"; - const buffer = new ArrayBuffer(3); - ws.onerror = () => fail(); - ws.onopen = () => ws.send(buffer); - ws.onmessage = (e) => { - assertEquals(e.data, buffer); - ws.close(); - }; - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("Event Handlers order", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4242"); - const arr: number[] = []; - ws.onerror = () => fail(); - ws.addEventListener("message", () => arr.push(1)); - ws.onmessage = () => fail(); - ws.addEventListener("message", () => { - arr.push(3); - ws.close(); - assertEquals(arr, [1, 2, 3]); - }); - ws.onmessage = () => arr.push(2); - ws.onopen = () => ws.send("Echo"); - ws.onclose = () => { - resolve(); - }; - await promise; -}); - -Deno.test("Close without frame", async () => { - const { promise, resolve } = Promise.withResolvers(); - const ws = new WebSocket("ws://localhost:4244"); - ws.onerror = () => fail(); - ws.onclose = (e) => { - assertEquals(e.code, 1005); - resolve(); - }; - await promise; -}); diff --git a/cli/tests/testdata/run/websocketstream_test.ts b/cli/tests/testdata/run/websocketstream_test.ts deleted file mode 100644 index b9157c25e..000000000 --- a/cli/tests/testdata/run/websocketstream_test.ts +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -import { - assert, - assertEquals, - assertNotEquals, - assertRejects, - assertThrows, - unreachable, -} from "../../../../test_util/std/assert/mod.ts"; - -Deno.test("fragment", () => { - assertThrows(() => new WebSocketStream("ws://localhost:4242/#")); - assertThrows(() => new WebSocketStream("ws://localhost:4242/#foo")); -}); - -Deno.test("duplicate protocols", () => { - assertThrows(() => - new WebSocketStream("ws://localhost:4242", { - protocols: ["foo", "foo"], - }) - ); -}); - -Deno.test("connect & close custom valid code", async () => { - const ws = new WebSocketStream("ws://localhost:4242"); - await ws.opened; - ws.close({ code: 1000 }); - await ws.closed; -}); - -Deno.test("connect & close custom invalid reason", async () => { - const ws = new WebSocketStream("ws://localhost:4242"); - await ws.opened; - assertThrows(() => ws.close({ code: 1000, reason: "".padEnd(124, "o") })); - ws.close(); - await ws.closed; -}); - -Deno.test("echo string", async () => { - const ws = new WebSocketStream("ws://localhost:4242"); - const { readable, writable } = await ws.opened; - await writable.getWriter().write("foo"); - const res = await readable.getReader().read(); - assertEquals(res.value, "foo"); - ws.close(); - await ws.closed; -}); - -Deno.test("echo string tls", async () => { - const ws = new WebSocketStream("wss://localhost:4243"); - const { readable, writable } = await ws.opened; - await writable.getWriter().write("foo"); - const res = await readable.getReader().read(); - assertEquals(res.value, "foo"); - ws.close(); - await ws.closed; -}); - -Deno.test("websocket error", async () => { - const ws = new WebSocketStream("wss://localhost:4242"); - await Promise.all([ - assertRejects( - () => ws.opened, - Deno.errors.UnexpectedEof, - "tls handshake eof", - ), - assertRejects( - () => ws.closed, - Deno.errors.UnexpectedEof, - "tls handshake eof", - ), - ]); -}); - -Deno.test("echo uint8array", async () => { - const ws = new WebSocketStream("ws://localhost:4242"); - const { readable, writable } = await ws.opened; - const uint = new Uint8Array([102, 111, 111]); - await writable.getWriter().write(uint); - const res = await readable.getReader().read(); - assertEquals(res.value, uint); - ws.close(); - await ws.closed; -}); - -Deno.test("aborting immediately throws an AbortError", async () => { - const controller = new AbortController(); - const wss = new WebSocketStream("ws://localhost:4242", { - signal: controller.signal, - }); - controller.abort(); - await assertRejects( - () => wss.opened, - (error: Error) => { - assert(error instanceof DOMException); - assertEquals(error.name, "AbortError"); - }, - ); - await assertRejects( - () => wss.closed, - (error: Error) => { - assert(error instanceof DOMException); - assertEquals(error.name, "AbortError"); - }, - ); -}); - -Deno.test("aborting immediately with a reason throws that reason", async () => { - const controller = new AbortController(); - const wss = new WebSocketStream("ws://localhost:4242", { - signal: controller.signal, - }); - const abortReason = new Error(); - controller.abort(abortReason); - await assertRejects( - () => wss.opened, - (error: Error) => assertEquals(error, abortReason), - ); - await assertRejects( - () => wss.closed, - (error: Error) => assertEquals(error, abortReason), - ); -}); - -Deno.test("aborting immediately with a primitive as reason throws that primitive", async () => { - const controller = new AbortController(); - const wss = new WebSocketStream("ws://localhost:4242", { - signal: controller.signal, - }); - controller.abort("Some string"); - await wss.opened.then( - () => unreachable(), - (e) => assertEquals(e, "Some string"), - ); - await wss.closed.then( - () => unreachable(), - (e) => assertEquals(e, "Some string"), - ); -}); - -Deno.test("headers", async () => { - const listener = Deno.listen({ port: 4512 }); - const promise = (async () => { - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const { request, respondWith } = (await httpConn.nextRequest())!; - assertEquals(request.headers.get("x-some-header"), "foo"); - const { response, socket } = Deno.upgradeWebSocket(request); - socket.onopen = () => socket.close(); - const p = new Promise((resolve) => { - socket.onopen = () => socket.close(); - socket.onclose = () => resolve(); - }); - await respondWith(response); - await p; - })(); - - const ws = new WebSocketStream("ws://localhost:4512", { - headers: [["x-some-header", "foo"]], - }); - await ws.opened; - await promise; - await ws.closed; - listener.close(); -}); - -Deno.test("forbidden headers", async () => { - const forbiddenHeaders = [ - "sec-websocket-accept", - "sec-websocket-extensions", - "sec-websocket-key", - "sec-websocket-protocol", - "sec-websocket-version", - "upgrade", - "connection", - ]; - - const listener = Deno.listen({ port: 4512 }); - const promise = (async () => { - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const { request, respondWith } = (await httpConn.nextRequest())!; - for (const [key] of request.headers) { - assertNotEquals(key, "foo"); - } - const { response, socket } = Deno.upgradeWebSocket(request); - const p = new Promise((resolve) => { - socket.onopen = () => socket.close(); - socket.onclose = () => resolve(); - }); - await respondWith(response); - await p; - })(); - - const ws = new WebSocketStream("ws://localhost:4512", { - headers: forbiddenHeaders.map((header) => [header, "foo"]), - }); - await ws.opened; - await promise; - await ws.closed; - listener.close(); -}); - -Deno.test("sync close with empty stream", async () => { - const listener = Deno.listen({ port: 4512 }); - const promise = (async () => { - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const { request, respondWith } = (await httpConn.nextRequest())!; - const { response, socket } = Deno.upgradeWebSocket(request); - const p = new Promise((resolve) => { - socket.onopen = () => { - socket.send("first message"); - socket.send("second message"); - }; - socket.onclose = () => resolve(); - }); - await respondWith(response); - await p; - })(); - - const ws = new WebSocketStream("ws://localhost:4512"); - const { readable } = await ws.opened; - const reader = readable.getReader(); - const firstMessage = await reader.read(); - assertEquals(firstMessage.value, "first message"); - const secondMessage = await reader.read(); - assertEquals(secondMessage.value, "second message"); - ws.close({ code: 1000 }); - await ws.closed; - await promise; - listener.close(); -}); - -Deno.test("sync close with unread messages in stream", async () => { - const listener = Deno.listen({ port: 4512 }); - const promise = (async () => { - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const { request, respondWith } = (await httpConn.nextRequest())!; - const { response, socket } = Deno.upgradeWebSocket(request); - const p = new Promise((resolve) => { - socket.onopen = () => { - socket.send("first message"); - socket.send("second message"); - socket.send("third message"); - socket.send("fourth message"); - }; - socket.onclose = () => resolve(); - }); - await respondWith(response); - await p; - })(); - - const ws = new WebSocketStream("ws://localhost:4512"); - const { readable } = await ws.opened; - const reader = readable.getReader(); - const firstMessage = await reader.read(); - assertEquals(firstMessage.value, "first message"); - const secondMessage = await reader.read(); - assertEquals(secondMessage.value, "second message"); - ws.close({ code: 1000 }); - await ws.closed; - await promise; - listener.close(); -}); - -Deno.test("async close with empty stream", async () => { - const listener = Deno.listen({ port: 4512 }); - const promise = (async () => { - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const { request, respondWith } = (await httpConn.nextRequest())!; - const { response, socket } = Deno.upgradeWebSocket(request); - const p = new Promise((resolve) => { - socket.onopen = () => { - socket.send("first message"); - socket.send("second message"); - }; - socket.onclose = () => resolve(); - }); - await respondWith(response); - await p; - })(); - - const ws = new WebSocketStream("ws://localhost:4512"); - const { readable } = await ws.opened; - const reader = readable.getReader(); - const firstMessage = await reader.read(); - assertEquals(firstMessage.value, "first message"); - const secondMessage = await reader.read(); - assertEquals(secondMessage.value, "second message"); - setTimeout(() => { - ws.close({ code: 1000 }); - }, 0); - await ws.closed; - await promise; - listener.close(); -}); - -Deno.test("async close with unread messages in stream", async () => { - const listener = Deno.listen({ port: 4512 }); - const promise = (async () => { - const conn = await listener.accept(); - const httpConn = Deno.serveHttp(conn); - const { request, respondWith } = (await httpConn.nextRequest())!; - const { response, socket } = Deno.upgradeWebSocket(request); - const p = new Promise((resolve) => { - socket.onopen = () => { - socket.send("first message"); - socket.send("second message"); - socket.send("third message"); - socket.send("fourth message"); - }; - socket.onclose = () => resolve(); - }); - await respondWith(response); - await p; - })(); - - const ws = new WebSocketStream("ws://localhost:4512"); - const { readable } = await ws.opened; - const reader = readable.getReader(); - const firstMessage = await reader.read(); - assertEquals(firstMessage.value, "first message"); - const secondMessage = await reader.read(); - assertEquals(secondMessage.value, "second message"); - setTimeout(() => { - ws.close({ code: 1000 }); - }, 0); - await ws.closed; - await promise; - listener.close(); -}); diff --git a/cli/tests/testdata/workers/test.ts b/cli/tests/testdata/workers/test.ts deleted file mode 100644 index 2fecc63e0..000000000 --- a/cli/tests/testdata/workers/test.ts +++ /dev/null @@ -1,844 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -// Requires to be run with `--allow-net` flag - -import { - assert, - assertEquals, - assertMatch, - assertThrows, -} from "../../../../test_util/std/assert/mod.ts"; - -Deno.test({ - name: "worker terminate", - fn: async function () { - const jsWorker = new Worker( - import.meta.resolve("./test_worker.js"), - { type: "module" }, - ); - const tsWorker = new Worker( - import.meta.resolve("./test_worker.ts"), - { type: "module", name: "tsWorker" }, - ); - - const deferred1 = Promise.withResolvers(); - jsWorker.onmessage = (e) => { - deferred1.resolve(e.data); - }; - - const deferred2 = Promise.withResolvers(); - tsWorker.onmessage = (e) => { - deferred2.resolve(e.data); - }; - - jsWorker.postMessage("Hello World"); - assertEquals(await deferred1.promise, "Hello World"); - tsWorker.postMessage("Hello World"); - assertEquals(await deferred2.promise, "Hello World"); - tsWorker.terminate(); - jsWorker.terminate(); - }, -}); - -Deno.test({ - name: "worker from data url", - async fn() { - const tsWorker = new Worker( - "data:application/typescript;base64,aWYgKHNlbGYubmFtZSAhPT0gInRzV29ya2VyIikgewogIHRocm93IEVycm9yKGBJbnZhbGlkIHdvcmtlciBuYW1lOiAke3NlbGYubmFtZX0sIGV4cGVjdGVkIHRzV29ya2VyYCk7Cn0KCm9ubWVzc2FnZSA9IGZ1bmN0aW9uIChlKTogdm9pZCB7CiAgcG9zdE1lc3NhZ2UoZS5kYXRhKTsKICBjbG9zZSgpOwp9Owo=", - { type: "module", name: "tsWorker" }, - ); - - const { promise, resolve } = Promise.withResolvers(); - tsWorker.onmessage = (e) => { - resolve(e.data); - }; - - tsWorker.postMessage("Hello World"); - assertEquals(await promise, "Hello World"); - tsWorker.terminate(); - }, -}); - -Deno.test({ - name: "worker nested", - fn: async function () { - const nestedWorker = new Worker( - import.meta.resolve("./nested_worker.js"), - { type: "module", name: "nested" }, - ); - - // deno-lint-ignore no-explicit-any - const { promise, resolve } = Promise.withResolvers(); - nestedWorker.onmessage = (e) => { - resolve(e.data); - }; - - nestedWorker.postMessage("Hello World"); - assertEquals(await promise, { type: "msg", text: "Hello World" }); - nestedWorker.terminate(); - }, -}); - -Deno.test({ - name: "worker throws when executing", - fn: async function () { - const throwingWorker = new Worker( - import.meta.resolve("./throwing_worker.js"), - { type: "module" }, - ); - - const { promise, resolve } = Promise.withResolvers(); - // deno-lint-ignore no-explicit-any - throwingWorker.onerror = (e: any) => { - e.preventDefault(); - resolve(e.message); - }; - - assertMatch( - await promise as string, - /Uncaught \(in promise\) Error: Thrown error/, - ); - throwingWorker.terminate(); - }, -}); - -Deno.test({ - name: "worker globals", - fn: async function () { - const workerOptions: WorkerOptions = { type: "module" }; - const w = new Worker( - import.meta.resolve("./worker_globals.ts"), - workerOptions, - ); - - const { promise, resolve } = Promise.withResolvers(); - w.onmessage = (e) => { - resolve(e.data); - }; - - w.postMessage("Hello, world!"); - assertEquals(await promise, "true, true, true, true"); - w.terminate(); - }, -}); - -Deno.test({ - name: "worker navigator", - fn: async function () { - const workerOptions: WorkerOptions = { type: "module" }; - const w = new Worker( - import.meta.resolve("./worker_navigator.ts"), - workerOptions, - ); - - const { promise, resolve } = Promise.withResolvers(); - w.onmessage = (e) => { - resolve(e.data); - }; - - w.postMessage("Hello, world!"); - assertEquals(await promise, "string, object, string, number"); - w.terminate(); - }, -}); - -Deno.test({ - name: "worker fetch API", - fn: async function () { - const fetchingWorker = new Worker( - import.meta.resolve("./fetching_worker.js"), - { type: "module" }, - ); - - const { promise, resolve, reject } = Promise.withResolvers(); - // deno-lint-ignore no-explicit-any - fetchingWorker.onerror = (e: any) => { - e.preventDefault(); - reject(e.message); - }; - // Defer promise.resolve() to allow worker to shut down - fetchingWorker.onmessage = (e) => { - resolve(e.data); - }; - - assertEquals(await promise, "Done!"); - fetchingWorker.terminate(); - }, -}); - -Deno.test({ - name: "worker terminate busy loop", - fn: async function () { - const { promise, resolve } = Promise.withResolvers(); - - const busyWorker = new Worker( - import.meta.resolve("./busy_worker.js"), - { type: "module" }, - ); - - let testResult = 0; - - busyWorker.onmessage = (e) => { - testResult = e.data; - if (testResult >= 10000) { - busyWorker.terminate(); - busyWorker.onmessage = (_e) => { - throw new Error("unreachable"); - }; - setTimeout(() => { - resolve(testResult); - }, 100); - } - }; - - busyWorker.postMessage("ping"); - assertEquals(await promise, 10000); - }, -}); - -Deno.test({ - name: "worker race condition", - fn: async function () { - // See issue for details - // https://github.com/denoland/deno/issues/4080 - const { promise, resolve } = Promise.withResolvers(); - - const racyWorker = new Worker( - import.meta.resolve("./racy_worker.js"), - { type: "module" }, - ); - - racyWorker.onmessage = (_e) => { - setTimeout(() => { - resolve(); - }, 100); - }; - - racyWorker.postMessage("START"); - await promise; - }, -}); - -Deno.test({ - name: "worker is event listener", - fn: async function () { - let messageHandlersCalled = 0; - let errorHandlersCalled = 0; - - const deferred1 = Promise.withResolvers(); - const deferred2 = Promise.withResolvers(); - - const worker = new Worker( - import.meta.resolve("./event_worker.js"), - { type: "module" }, - ); - - worker.onmessage = (_e: Event) => { - messageHandlersCalled++; - }; - worker.addEventListener("message", (_e: Event) => { - messageHandlersCalled++; - }); - worker.addEventListener("message", (_e: Event) => { - messageHandlersCalled++; - deferred1.resolve(); - }); - - worker.onerror = (e) => { - errorHandlersCalled++; - e.preventDefault(); - }; - worker.addEventListener("error", (_e: Event) => { - errorHandlersCalled++; - }); - worker.addEventListener("error", (_e: Event) => { - errorHandlersCalled++; - deferred2.resolve(); - }); - - worker.postMessage("ping"); - await deferred1.promise; - assertEquals(messageHandlersCalled, 3); - - worker.postMessage("boom"); - await deferred2.promise; - assertEquals(errorHandlersCalled, 3); - worker.terminate(); - }, -}); - -Deno.test({ - name: "worker scope is event listener", - fn: async function () { - const worker = new Worker( - import.meta.resolve("./event_worker_scope.js"), - { type: "module" }, - ); - - // deno-lint-ignore no-explicit-any - const { promise, resolve } = Promise.withResolvers(); - worker.onmessage = (e: MessageEvent) => { - resolve(e.data); - }; - worker.onerror = (_e) => { - throw new Error("unreachable"); - }; - - worker.postMessage("boom"); - worker.postMessage("ping"); - assertEquals(await promise, { - messageHandlersCalled: 4, - errorHandlersCalled: 4, - }); - worker.terminate(); - }, -}); - -Deno.test({ - name: "worker with Deno namespace", - fn: async function () { - const denoWorker = new Worker( - import.meta.resolve("./deno_worker.ts"), - { type: "module", deno: { permissions: "inherit" } }, - ); - - const { promise, resolve } = Promise.withResolvers(); - denoWorker.onmessage = (e) => { - denoWorker.terminate(); - resolve(e.data); - }; - - denoWorker.postMessage("Hello World"); - assertEquals(await promise, "Hello World"); - }, -}); - -Deno.test({ - name: "worker with crypto in scope", - fn: async function () { - const w = new Worker( - import.meta.resolve("./worker_crypto.js"), - { type: "module" }, - ); - - const { promise, resolve } = Promise.withResolvers(); - w.onmessage = (e) => { - resolve(e.data); - }; - - w.postMessage(null); - assertEquals(await promise, true); - w.terminate(); - }, -}); - -Deno.test({ - name: "Worker event handler order", - fn: async function () { - const { promise, resolve } = Promise.withResolvers(); - const w = new Worker( - import.meta.resolve("./test_worker.ts"), - { type: "module", name: "tsWorker" }, - ); - const arr: number[] = []; - w.addEventListener("message", () => arr.push(1)); - w.onmessage = (_e) => { - arr.push(2); - }; - w.addEventListener("message", () => arr.push(3)); - w.addEventListener("message", () => { - resolve(); - }); - w.postMessage("Hello World"); - await promise; - assertEquals(arr, [1, 2, 3]); - w.terminate(); - }, -}); - -Deno.test({ - name: "Worker immediate close", - fn: async function () { - const { promise, resolve } = Promise.withResolvers(); - const w = new Worker( - import.meta.resolve("./immediately_close_worker.js"), - { type: "module" }, - ); - setTimeout(() => { - resolve(); - }, 1000); - await promise; - w.terminate(); - }, -}); - -Deno.test({ - name: "Worker post undefined", - fn: async function () { - const { promise, resolve } = Promise.withResolvers(); - const worker = new Worker( - import.meta.resolve("./post_undefined.ts"), - { type: "module" }, - ); - - const handleWorkerMessage = (e: MessageEvent) => { - console.log("main <- worker:", e.data); - worker.terminate(); - resolve(); - }; - - worker.addEventListener("messageerror", () => console.log("message error")); - worker.addEventListener("error", () => console.log("error")); - worker.addEventListener("message", handleWorkerMessage); - - console.log("\npost from parent"); - worker.postMessage(undefined); - await promise; - }, -}); - -Deno.test("Worker inherits permissions", async function () { - const worker = new Worker( - import.meta.resolve("./read_check_worker.js"), - { type: "module", deno: { permissions: "inherit" } }, - ); - - const { promise, resolve } = Promise.withResolvers(); - worker.onmessage = (e) => { - resolve(e.data); - }; - - worker.postMessage(null); - assertEquals(await promise, true); - worker.terminate(); -}); - -Deno.test("Worker limit children permissions", async function () { - const worker = new Worker( - import.meta.resolve("./read_check_worker.js"), - { type: "module", deno: { permissions: { read: false } } }, - ); - - const { promise, resolve } = Promise.withResolvers(); - worker.onmessage = (e) => { - resolve(e.data); - }; - - worker.postMessage(null); - assertEquals(await promise, false); - worker.terminate(); -}); - -Deno.test("Worker limit children permissions granularly", async function () { - const worker = new Worker( - import.meta.resolve("./read_check_granular_worker.js"), - { - type: "module", - deno: { - permissions: { - env: ["foo"], - hrtime: true, - net: ["foo", "bar:8000"], - ffi: [new URL("foo", import.meta.url), "bar"], - read: [new URL("foo", import.meta.url), "bar"], - run: [new URL("foo", import.meta.url), "bar", "./baz"], - write: [new URL("foo", import.meta.url), "bar"], - }, - }, - }, - ); - // deno-lint-ignore no-explicit-any - const { promise, resolve } = Promise.withResolvers(); - worker.onmessage = ({ data }) => resolve(data); - assertEquals(await promise, { - envGlobal: "prompt", - envFoo: "granted", - envAbsent: "prompt", - hrtime: "granted", - netGlobal: "prompt", - netFoo: "granted", - netFoo8000: "granted", - netBar: "prompt", - netBar8000: "granted", - ffiGlobal: "prompt", - ffiFoo: "granted", - ffiBar: "granted", - ffiAbsent: "prompt", - readGlobal: "prompt", - readFoo: "granted", - readBar: "granted", - readAbsent: "prompt", - runGlobal: "prompt", - runFoo: "granted", - runBar: "granted", - runBaz: "granted", - runAbsent: "prompt", - writeGlobal: "prompt", - writeFoo: "granted", - writeBar: "granted", - writeAbsent: "prompt", - }); - worker.terminate(); -}); - -Deno.test("Nested worker limit children permissions", async function () { - /** This worker has permissions but doesn't grant them to its children */ - const worker = new Worker( - import.meta.resolve("./parent_read_check_worker.js"), - { type: "module", deno: { permissions: "inherit" } }, - ); - // deno-lint-ignore no-explicit-any - const { promise, resolve } = Promise.withResolvers(); - worker.onmessage = ({ data }) => resolve(data); - assertEquals(await promise, { - envGlobal: "prompt", - envFoo: "prompt", - envAbsent: "prompt", - hrtime: "prompt", - netGlobal: "prompt", - netFoo: "prompt", - netFoo8000: "prompt", - netBar: "prompt", - netBar8000: "prompt", - ffiGlobal: "prompt", - ffiFoo: "prompt", - ffiBar: "prompt", - ffiAbsent: "prompt", - readGlobal: "prompt", - readFoo: "prompt", - readBar: "prompt", - readAbsent: "prompt", - runGlobal: "prompt", - runFoo: "prompt", - runBar: "prompt", - runBaz: "prompt", - runAbsent: "prompt", - writeGlobal: "prompt", - writeFoo: "prompt", - writeBar: "prompt", - writeAbsent: "prompt", - }); - worker.terminate(); -}); - -// This test relies on env permissions not being granted on main thread -Deno.test({ - name: - "Worker initialization throws on worker permissions greater than parent thread permissions", - permissions: { env: false }, - fn: function () { - assertThrows( - () => { - const worker = new Worker( - import.meta.resolve("./deno_worker.ts"), - { type: "module", deno: { permissions: { env: true } } }, - ); - worker.terminate(); - }, - Deno.errors.PermissionDenied, - "Can't escalate parent thread permissions", - ); - }, -}); - -Deno.test("Worker with disabled permissions", async function () { - const worker = new Worker( - import.meta.resolve("./no_permissions_worker.js"), - { type: "module", deno: { permissions: "none" } }, - ); - - const { promise, resolve } = Promise.withResolvers(); - worker.onmessage = (e) => { - resolve(e.data); - }; - - worker.postMessage(null); - assertEquals(await promise, true); - worker.terminate(); -}); - -Deno.test("Worker permissions are not inherited with empty permission object", async function () { - const worker = new Worker( - import.meta.resolve("./permission_echo.js"), - { type: "module", deno: { permissions: {} } }, - ); - - // deno-lint-ignore no-explicit-any - const { promise, resolve } = Promise.withResolvers(); - worker.onmessage = (e) => { - resolve(e.data); - }; - - worker.postMessage(null); - assertEquals(await promise, { - env: "prompt", - hrtime: "prompt", - net: "prompt", - ffi: "prompt", - read: "prompt", - run: "prompt", - write: "prompt", - }); - worker.terminate(); -}); - -Deno.test("Worker permissions are not inherited with single specified permission", async function () { - const worker = new Worker( - import.meta.resolve("./permission_echo.js"), - { type: "module", deno: { permissions: { net: true } } }, - ); - - // deno-lint-ignore no-explicit-any - const { promise, resolve } = Promise.withResolvers(); - worker.onmessage = (e) => { - resolve(e.data); - }; - - worker.postMessage(null); - assertEquals(await promise, { - env: "prompt", - hrtime: "prompt", - net: "granted", - ffi: "prompt", - read: "prompt", - run: "prompt", - write: "prompt", - }); - worker.terminate(); -}); - -Deno.test("Worker with invalid permission arg", function () { - assertThrows( - () => - new Worker(`data:,close();`, { - type: "module", - // @ts-expect-error invalid env value - deno: { permissions: { env: "foo" } }, - }), - TypeError, - '(deno.permissions.env) invalid value: string "foo", expected "inherit" or boolean or string[]', - ); -}); - -Deno.test({ - name: "worker location", - fn: async function () { - const { promise, resolve } = Promise.withResolvers(); - const workerModuleHref = import.meta.resolve("./worker_location.ts"); - const w = new Worker(workerModuleHref, { type: "module" }); - w.onmessage = (e) => { - resolve(e.data); - }; - w.postMessage("Hello, world!"); - assertEquals(await promise, `${workerModuleHref}, true`); - w.terminate(); - }, -}); - -Deno.test({ - name: "worker with relative specifier", - fn: async function () { - assertEquals(location.href, "http://127.0.0.1:4545/"); - const w = new Worker( - "./workers/test_worker.ts", - { type: "module", name: "tsWorker" }, - ); - const { promise, resolve } = Promise.withResolvers(); - w.onmessage = (e) => { - resolve(e.data); - }; - w.postMessage("Hello, world!"); - assertEquals(await promise, "Hello, world!"); - w.terminate(); - }, -}); - -Deno.test({ - name: "Worker with top-level-await", - fn: async function () { - const { promise, resolve, reject } = Promise.withResolvers(); - const worker = new Worker( - import.meta.resolve("./worker_with_top_level_await.ts"), - { type: "module" }, - ); - worker.onmessage = (e) => { - if (e.data == "ready") { - worker.postMessage("trigger worker handler"); - } else if (e.data == "triggered worker handler") { - resolve(); - } else { - reject(new Error("Handler didn't run during top-level delay.")); - } - }; - await promise; - worker.terminate(); - }, -}); - -Deno.test({ - name: "Worker with native HTTP", - fn: async function () { - const { promise, resolve } = Promise.withResolvers(); - const worker = new Worker( - import.meta.resolve("./http_worker.js"), - { type: "module", deno: { permissions: "inherit" } }, - ); - worker.onmessage = () => { - resolve(); - }; - await promise; - - assert(worker); - const response = await fetch("http://localhost:4506"); - assert(await response.arrayBuffer()); - worker.terminate(); - }, -}); - -Deno.test({ - name: "structured cloning postMessage", - fn: async function () { - const worker = new Worker( - import.meta.resolve("./worker_structured_cloning.ts"), - { type: "module" }, - ); - - // deno-lint-ignore no-explicit-any - const { promise, resolve } = Promise.withResolvers(); - worker.onmessage = (e) => { - resolve(e.data); - }; - - worker.postMessage("START"); - const data = await promise; - // self field should reference itself (circular ref) - assert(data === data.self); - // fields a and b refer to the same array - assertEquals(data.a, ["a", true, 432]); - assertEquals(data.b, ["a", true, 432]); - data.b[0] = "b"; - data.a[2] += 5; - assertEquals(data.a, ["b", true, 437]); - assertEquals(data.b, ["b", true, 437]); - // c is a set - const len = data.c.size; - data.c.add(1); // This value is already in the set. - data.c.add(2); - assertEquals(len + 1, data.c.size); - worker.terminate(); - }, -}); - -Deno.test({ - name: "worker with relative specifier", - fn: async function () { - assertEquals(location.href, "http://127.0.0.1:4545/"); - const w = new Worker( - "./workers/test_worker.ts", - { type: "module", name: "tsWorker" }, - ); - const { promise, resolve } = Promise.withResolvers(); - w.onmessage = (e) => { - resolve(e.data); - }; - w.postMessage("Hello, world!"); - assertEquals(await promise, "Hello, world!"); - w.terminate(); - }, -}); - -Deno.test({ - name: "worker SharedArrayBuffer", - fn: async function () { - const { promise, resolve } = Promise.withResolvers(); - const workerOptions: WorkerOptions = { type: "module" }; - const w = new Worker( - import.meta.resolve("./shared_array_buffer.ts"), - workerOptions, - ); - const sab1 = new SharedArrayBuffer(1); - const sab2 = new SharedArrayBuffer(1); - const bytes1 = new Uint8Array(sab1); - const bytes2 = new Uint8Array(sab2); - assertEquals(bytes1[0], 0); - assertEquals(bytes2[0], 0); - w.onmessage = () => { - w.postMessage([sab1, sab2]); - w.onmessage = () => { - resolve(); - }; - }; - await promise; - assertEquals(bytes1[0], 1); - assertEquals(bytes2[0], 2); - w.terminate(); - }, -}); - -Deno.test({ - name: "Send MessagePorts from / to workers", - fn: async function () { - const worker = new Worker( - import.meta.resolve("./message_port.ts"), - { type: "module" }, - ); - const channel = new MessageChannel(); - - // deno-lint-ignore no-explicit-any - const deferred1 = Promise.withResolvers(); - const deferred2 = Promise.withResolvers(); - const deferred3 = Promise.withResolvers(); - const result = Promise.withResolvers(); - worker.onmessage = (e) => { - deferred1.resolve([e.data, e.ports.length]); - const port1 = e.ports[0]; - port1.onmessage = (e) => { - deferred2.resolve(e.data); - port1.close(); - worker.postMessage("3", [channel.port1]); - }; - port1.postMessage("2"); - }; - channel.port2.onmessage = (e) => { - deferred3.resolve(e.data); - channel.port2.close(); - result.resolve(); - }; - - assertEquals(await deferred1.promise, ["1", 1]); - assertEquals(await deferred2.promise, true); - assertEquals(await deferred3.promise, true); - await result.promise; - worker.terminate(); - }, -}); - -Deno.test({ - name: "worker Deno.memoryUsage", - fn: async function () { - const w = new Worker( - /** - * Source code - * self.onmessage = function() {self.postMessage(Deno.memoryUsage())} - */ - "data:application/typescript;base64,c2VsZi5vbm1lc3NhZ2UgPSBmdW5jdGlvbigpIHtzZWxmLnBvc3RNZXNzYWdlKERlbm8ubWVtb3J5VXNhZ2UoKSl9", - { type: "module", name: "tsWorker" }, - ); - - w.postMessage(null); - - // deno-lint-ignore no-explicit-any - const { promise, resolve } = Promise.withResolvers(); - w.onmessage = function (evt) { - resolve(evt.data); - }; - - assertEquals( - Object.keys( - await promise as unknown as Record, - ), - ["rss", "heapTotal", "heapUsed", "external"], - ); - w.terminate(); - }, -}); diff --git a/cli/tests/testdata/workers/test.ts.out b/cli/tests/testdata/workers/test.ts.out deleted file mode 100644 index 62c132b60..000000000 --- a/cli/tests/testdata/workers/test.ts.out +++ /dev/null @@ -1,3 +0,0 @@ -[WILDCARD] -ok | [WILDCARD] passed | 0 failed ([WILDCARD]) - diff --git a/cli/tests/unit/request_test.ts b/cli/tests/unit/request_test.ts index 73a24304e..fe34c20a5 100644 --- a/cli/tests/unit/request_test.ts +++ b/cli/tests/unit/request_test.ts @@ -33,7 +33,7 @@ Deno.test(function methodNonString() { Deno.test(function requestRelativeUrl() { assertEquals( new Request("relative-url").url, - "http://js-unit-tests/foo/relative-url", + "http://127.0.0.1:4545/relative-url", ); }); diff --git a/cli/tests/unit/websocket_test.ts b/cli/tests/unit/websocket_test.ts index 6a1dc3525..42681c187 100644 --- a/cli/tests/unit/websocket_test.ts +++ b/cli/tests/unit/websocket_test.ts @@ -435,3 +435,304 @@ Deno.test( await server.finished; }, ); + +Deno.test("invalid scheme", () => { + assertThrows(() => new WebSocket("foo://localhost:4242")); +}); + +Deno.test("fragment", () => { + assertThrows(() => new WebSocket("ws://localhost:4242/#")); + assertThrows(() => new WebSocket("ws://localhost:4242/#foo")); +}); + +Deno.test("duplicate protocols", () => { + assertThrows(() => new WebSocket("ws://localhost:4242", ["foo", "foo"])); +}); + +Deno.test("invalid server", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:2121"); + let err = false; + ws.onerror = () => { + err = true; + }; + ws.onclose = () => { + if (err) { + resolve(); + } else { + fail(); + } + }; + ws.onopen = () => fail(); + await promise; +}); + +Deno.test("connect & close", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + ws.onerror = () => fail(); + ws.onopen = () => { + ws.close(); + }; + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("connect & abort", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + ws.close(); + let err = false; + ws.onerror = () => { + err = true; + }; + ws.onclose = () => { + if (err) { + resolve(); + } else { + fail(); + } + }; + ws.onopen = () => fail(); + await promise; +}); + +Deno.test("connect & close custom valid code", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + ws.onerror = () => fail(); + ws.onopen = () => ws.close(1000); + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("connect & close custom invalid code", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + ws.onerror = () => fail(); + ws.onopen = () => { + assertThrows(() => ws.close(1001)); + ws.close(); + }; + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("connect & close custom valid reason", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + ws.onerror = () => fail(); + ws.onopen = () => ws.close(1000, "foo"); + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("connect & close custom invalid reason", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + ws.onerror = () => fail(); + ws.onopen = () => { + assertThrows(() => ws.close(1000, "".padEnd(124, "o"))); + ws.close(); + }; + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("echo string", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + ws.onerror = () => fail(); + ws.onopen = () => ws.send("foo"); + ws.onmessage = (e) => { + assertEquals(e.data, "foo"); + ws.close(); + }; + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("echo string tls", async () => { + const deferred1 = Promise.withResolvers(); + const deferred2 = Promise.withResolvers(); + const ws = new WebSocket("wss://localhost:4243"); + ws.onerror = () => fail(); + ws.onopen = () => ws.send("foo"); + ws.onmessage = (e) => { + assertEquals(e.data, "foo"); + ws.close(); + deferred1.resolve(); + }; + ws.onclose = () => { + deferred2.resolve(); + }; + await deferred1.promise; + await deferred2.promise; +}); + +Deno.test("websocket error", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("wss://localhost:4242"); + ws.onopen = () => fail(); + ws.onerror = (err) => { + assert(err instanceof ErrorEvent); + assertEquals( + err.message, + "NetworkError: failed to connect to WebSocket: received corrupt message of type InvalidContentType", + ); + resolve(); + }; + await promise; +}); + +Deno.test("echo blob with binaryType blob", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + const blob = new Blob(["foo"]); + ws.onerror = () => fail(); + ws.onopen = () => ws.send(blob); + ws.onmessage = (e) => { + e.data.text().then((actual: string) => { + blob.text().then((expected) => { + assertEquals(actual, expected); + }); + }); + ws.close(); + }; + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("echo blob with binaryType arraybuffer", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + ws.binaryType = "arraybuffer"; + const blob = new Blob(["foo"]); + ws.onerror = () => fail(); + ws.onopen = () => ws.send(blob); + ws.onmessage = (e) => { + blob.arrayBuffer().then((expected) => { + assertEquals(e.data, expected); + }); + ws.close(); + }; + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("echo uint8array with binaryType blob", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + const uint = new Uint8Array([102, 111, 111]); + ws.onerror = () => fail(); + ws.onopen = () => ws.send(uint); + ws.onmessage = (e) => { + e.data.arrayBuffer().then((actual: ArrayBuffer) => { + assertEquals(actual, uint.buffer); + }); + ws.close(); + }; + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("echo uint8array with binaryType arraybuffer", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + ws.binaryType = "arraybuffer"; + const uint = new Uint8Array([102, 111, 111]); + ws.onerror = () => fail(); + ws.onopen = () => ws.send(uint); + ws.onmessage = (e) => { + assertEquals(e.data, uint.buffer); + ws.close(); + }; + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("echo arraybuffer with binaryType blob", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + const buffer = new ArrayBuffer(3); + ws.onerror = () => fail(); + ws.onopen = () => ws.send(buffer); + ws.onmessage = (e) => { + e.data.arrayBuffer().then((actual: ArrayBuffer) => { + assertEquals(actual, buffer); + }); + ws.close(); + }; + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("echo arraybuffer with binaryType arraybuffer", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + ws.binaryType = "arraybuffer"; + const buffer = new ArrayBuffer(3); + ws.onerror = () => fail(); + ws.onopen = () => ws.send(buffer); + ws.onmessage = (e) => { + assertEquals(e.data, buffer); + ws.close(); + }; + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("Event Handlers order", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4242"); + const arr: number[] = []; + ws.onerror = () => fail(); + ws.addEventListener("message", () => arr.push(1)); + ws.onmessage = () => fail(); + ws.addEventListener("message", () => { + arr.push(3); + ws.close(); + assertEquals(arr, [1, 2, 3]); + }); + ws.onmessage = () => arr.push(2); + ws.onopen = () => ws.send("Echo"); + ws.onclose = () => { + resolve(); + }; + await promise; +}); + +Deno.test("Close without frame", async () => { + const { promise, resolve } = Promise.withResolvers(); + const ws = new WebSocket("ws://localhost:4244"); + ws.onerror = () => fail(); + ws.onclose = (e) => { + assertEquals(e.code, 1005); + resolve(); + }; + await promise; +}); diff --git a/cli/tests/unit/websocketstream_test.ts b/cli/tests/unit/websocketstream_test.ts new file mode 100644 index 000000000..0a16f254e --- /dev/null +++ b/cli/tests/unit/websocketstream_test.ts @@ -0,0 +1,353 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { + assertEquals, + assertNotEquals, + assertRejects, + assertThrows, + unreachable, +} from "@test_util/std/assert/mod.ts"; + +Deno.test("fragment", () => { + assertThrows(() => new WebSocketStream("ws://localhost:4242/#")); + assertThrows(() => new WebSocketStream("ws://localhost:4242/#foo")); +}); + +Deno.test("duplicate protocols", () => { + assertThrows(() => + new WebSocketStream("ws://localhost:4242", { + protocols: ["foo", "foo"], + }) + ); +}); + +Deno.test( + "connect & close custom valid code", + { sanitizeOps: false }, + async () => { + const ws = new WebSocketStream("ws://localhost:4242"); + await ws.opened; + ws.close({ code: 1000 }); + await ws.closed; + }, +); + +Deno.test( + "connect & close custom invalid reason", + { sanitizeOps: false }, + async () => { + const ws = new WebSocketStream("ws://localhost:4242"); + await ws.opened; + assertThrows(() => ws.close({ code: 1000, reason: "".padEnd(124, "o") })); + ws.close(); + await ws.closed; + }, +); + +Deno.test("echo string", async () => { + const ws = new WebSocketStream("ws://localhost:4242"); + const { readable, writable } = await ws.opened; + await writable.getWriter().write("foo"); + const res = await readable.getReader().read(); + assertEquals(res.value, "foo"); + ws.close(); + await ws.closed; +}); + +// TODO(mmastrac): This fails -- perhaps it isn't respecting the TLS settings? +Deno.test("echo string tls", { ignore: true }, async () => { + const ws = new WebSocketStream("wss://localhost:4243"); + const { readable, writable } = await ws.opened; + await writable.getWriter().write("foo"); + const res = await readable.getReader().read(); + assertEquals(res.value, "foo"); + ws.close(); + await ws.closed; +}); + +Deno.test("websocket error", { sanitizeOps: false }, async () => { + const ws = new WebSocketStream("wss://localhost:4242"); + await Promise.all([ + // TODO(mmastrac): this exception should be tested + assertRejects( + () => ws.opened, + // Deno.errors.UnexpectedEof, + // "tls handshake eof", + ), + // TODO(mmastrac): this exception should be tested + assertRejects( + () => ws.closed, + // Deno.errors.UnexpectedEof, + // "tls handshake eof", + ), + ]); +}); + +Deno.test("echo uint8array", { sanitizeOps: false }, async () => { + const ws = new WebSocketStream("ws://localhost:4242"); + const { readable, writable } = await ws.opened; + const uint = new Uint8Array([102, 111, 111]); + await writable.getWriter().write(uint); + const res = await readable.getReader().read(); + assertEquals(res.value, uint); + ws.close(); + await ws.closed; +}); + +Deno.test("aborting immediately throws an AbortError", async () => { + const controller = new AbortController(); + const wss = new WebSocketStream("ws://localhost:4242", { + signal: controller.signal, + }); + controller.abort(); + // TODO(mmastrac): this exception should be tested + await assertRejects( + () => wss.opened, + // (error: Error) => { + // assert(error instanceof DOMException); + // assertEquals(error.name, "AbortError"); + // }, + ); + // TODO(mmastrac): this exception should be tested + await assertRejects( + () => wss.closed, + // (error: Error) => { + // assert(error instanceof DOMException); + // assertEquals(error.name, "AbortError"); + // }, + ); +}); + +Deno.test("aborting immediately with a reason throws that reason", async () => { + const controller = new AbortController(); + const wss = new WebSocketStream("ws://localhost:4242", { + signal: controller.signal, + }); + const abortReason = new Error(); + controller.abort(abortReason); + // TODO(mmastrac): this exception should be tested + await assertRejects( + () => wss.opened, + // (error: Error) => assertEquals(error, abortReason), + ); + // TODO(mmastrac): this exception should be tested + await assertRejects( + () => wss.closed, + // (error: Error) => assertEquals(error, abortReason), + ); +}); + +Deno.test("aborting immediately with a primitive as reason throws that primitive", async () => { + const controller = new AbortController(); + const wss = new WebSocketStream("ws://localhost:4242", { + signal: controller.signal, + }); + controller.abort("Some string"); + await wss.opened.then( + () => unreachable(), + (e) => assertEquals(e, "Some string"), + ); + await wss.closed.then( + () => unreachable(), + (e) => assertEquals(e, "Some string"), + ); +}); + +Deno.test("headers", { sanitizeOps: false }, async () => { + const listener = Deno.listen({ port: 4512 }); + const promise = (async () => { + const conn = await listener.accept(); + const httpConn = Deno.serveHttp(conn); + const { request, respondWith } = (await httpConn.nextRequest())!; + assertEquals(request.headers.get("x-some-header"), "foo"); + const { response, socket } = Deno.upgradeWebSocket(request); + socket.onopen = () => socket.close(); + const p = new Promise((resolve) => { + socket.onopen = () => socket.close(); + socket.onclose = () => resolve(); + }); + await respondWith(response); + await p; + })(); + + const ws = new WebSocketStream("ws://localhost:4512", { + headers: [["x-some-header", "foo"]], + }); + await ws.opened; + await promise; + await ws.closed; + listener.close(); +}); + +Deno.test("forbidden headers", async () => { + const forbiddenHeaders = [ + "sec-websocket-accept", + "sec-websocket-extensions", + "sec-websocket-key", + "sec-websocket-protocol", + "sec-websocket-version", + "upgrade", + "connection", + ]; + + const listener = Deno.listen({ port: 4512 }); + const promise = (async () => { + const conn = await listener.accept(); + const httpConn = Deno.serveHttp(conn); + const { request, respondWith } = (await httpConn.nextRequest())!; + for (const [key] of request.headers) { + assertNotEquals(key, "foo"); + } + const { response, socket } = Deno.upgradeWebSocket(request); + const p = new Promise((resolve) => { + socket.onopen = () => socket.close(); + socket.onclose = () => resolve(); + }); + await respondWith(response); + await p; + })(); + + const ws = new WebSocketStream("ws://localhost:4512", { + headers: forbiddenHeaders.map((header) => [header, "foo"]), + }); + await ws.opened; + await promise; + await ws.closed; + listener.close(); +}); + +Deno.test("sync close with empty stream", { sanitizeOps: false }, async () => { + const listener = Deno.listen({ port: 4512 }); + const promise = (async () => { + const conn = await listener.accept(); + const httpConn = Deno.serveHttp(conn); + const { request, respondWith } = (await httpConn.nextRequest())!; + const { response, socket } = Deno.upgradeWebSocket(request); + const p = new Promise((resolve) => { + socket.onopen = () => { + socket.send("first message"); + socket.send("second message"); + }; + socket.onclose = () => resolve(); + }); + await respondWith(response); + await p; + })(); + + const ws = new WebSocketStream("ws://localhost:4512"); + const { readable } = await ws.opened; + const reader = readable.getReader(); + const firstMessage = await reader.read(); + assertEquals(firstMessage.value, "first message"); + const secondMessage = await reader.read(); + assertEquals(secondMessage.value, "second message"); + ws.close({ code: 1000 }); + await ws.closed; + await promise; + listener.close(); +}); + +Deno.test( + "sync close with unread messages in stream", + { sanitizeOps: false }, + async () => { + const listener = Deno.listen({ port: 4512 }); + const promise = (async () => { + const conn = await listener.accept(); + const httpConn = Deno.serveHttp(conn); + const { request, respondWith } = (await httpConn.nextRequest())!; + const { response, socket } = Deno.upgradeWebSocket(request); + const p = new Promise((resolve) => { + socket.onopen = () => { + socket.send("first message"); + socket.send("second message"); + socket.send("third message"); + socket.send("fourth message"); + }; + socket.onclose = () => resolve(); + }); + await respondWith(response); + await p; + })(); + + const ws = new WebSocketStream("ws://localhost:4512"); + const { readable } = await ws.opened; + const reader = readable.getReader(); + const firstMessage = await reader.read(); + assertEquals(firstMessage.value, "first message"); + const secondMessage = await reader.read(); + assertEquals(secondMessage.value, "second message"); + ws.close({ code: 1000 }); + await ws.closed; + await promise; + listener.close(); + }, +); + +Deno.test("async close with empty stream", async () => { + const listener = Deno.listen({ port: 4512 }); + const promise = (async () => { + const conn = await listener.accept(); + const httpConn = Deno.serveHttp(conn); + const { request, respondWith } = (await httpConn.nextRequest())!; + const { response, socket } = Deno.upgradeWebSocket(request); + const p = new Promise((resolve) => { + socket.onopen = () => { + socket.send("first message"); + socket.send("second message"); + }; + socket.onclose = () => resolve(); + }); + await respondWith(response); + await p; + })(); + + const ws = new WebSocketStream("ws://localhost:4512"); + const { readable } = await ws.opened; + const reader = readable.getReader(); + const firstMessage = await reader.read(); + assertEquals(firstMessage.value, "first message"); + const secondMessage = await reader.read(); + assertEquals(secondMessage.value, "second message"); + setTimeout(() => { + ws.close({ code: 1000 }); + }, 0); + await ws.closed; + await promise; + listener.close(); +}); + +Deno.test("async close with unread messages in stream", async () => { + const listener = Deno.listen({ port: 4512 }); + const promise = (async () => { + const conn = await listener.accept(); + const httpConn = Deno.serveHttp(conn); + const { request, respondWith } = (await httpConn.nextRequest())!; + const { response, socket } = Deno.upgradeWebSocket(request); + const p = new Promise((resolve) => { + socket.onopen = () => { + socket.send("first message"); + socket.send("second message"); + socket.send("third message"); + socket.send("fourth message"); + }; + socket.onclose = () => resolve(); + }); + await respondWith(response); + await p; + })(); + + const ws = new WebSocketStream("ws://localhost:4512"); + const { readable } = await ws.opened; + const reader = readable.getReader(); + const firstMessage = await reader.read(); + assertEquals(firstMessage.value, "first message"); + const secondMessage = await reader.read(); + assertEquals(secondMessage.value, "second message"); + setTimeout(() => { + ws.close({ code: 1000 }); + }, 0); + await ws.closed; + await promise; + listener.close(); +}); diff --git a/cli/tests/unit/worker_test.ts b/cli/tests/unit/worker_test.ts new file mode 100644 index 000000000..eea0e8106 --- /dev/null +++ b/cli/tests/unit/worker_test.ts @@ -0,0 +1,843 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +// Requires to be run with `--allow-net` flag + +import { + assert, + assertEquals, + assertMatch, + assertThrows, +} from "@test_util/std/assert/mod.ts"; + +function resolveWorker(worker: string): string { + return import.meta.resolve(`../testdata/workers/${worker}`); +} + +Deno.test( + { permissions: { read: true } }, + function utimeSyncFileSuccess() { + const w = new Worker( + resolveWorker("worker_types.ts"), + { type: "module" }, + ); + assert(w); + w.terminate(); + }, +); + +Deno.test({ + name: "worker terminate", + fn: async function () { + const jsWorker = new Worker( + resolveWorker("test_worker.js"), + { type: "module" }, + ); + const tsWorker = new Worker( + resolveWorker("test_worker.ts"), + { type: "module", name: "tsWorker" }, + ); + + const deferred1 = Promise.withResolvers(); + jsWorker.onmessage = (e) => { + deferred1.resolve(e.data); + }; + + const deferred2 = Promise.withResolvers(); + tsWorker.onmessage = (e) => { + deferred2.resolve(e.data); + }; + + jsWorker.postMessage("Hello World"); + assertEquals(await deferred1.promise, "Hello World"); + tsWorker.postMessage("Hello World"); + assertEquals(await deferred2.promise, "Hello World"); + tsWorker.terminate(); + jsWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker from data url", + async fn() { + const tsWorker = new Worker( + "data:application/typescript;base64,aWYgKHNlbGYubmFtZSAhPT0gInRzV29ya2VyIikgewogIHRocm93IEVycm9yKGBJbnZhbGlkIHdvcmtlciBuYW1lOiAke3NlbGYubmFtZX0sIGV4cGVjdGVkIHRzV29ya2VyYCk7Cn0KCm9ubWVzc2FnZSA9IGZ1bmN0aW9uIChlKTogdm9pZCB7CiAgcG9zdE1lc3NhZ2UoZS5kYXRhKTsKICBjbG9zZSgpOwp9Owo=", + { type: "module", name: "tsWorker" }, + ); + + const { promise, resolve } = Promise.withResolvers(); + tsWorker.onmessage = (e) => { + resolve(e.data); + }; + + tsWorker.postMessage("Hello World"); + assertEquals(await promise, "Hello World"); + tsWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker nested", + fn: async function () { + const nestedWorker = new Worker( + resolveWorker("nested_worker.js"), + { type: "module", name: "nested" }, + ); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers(); + nestedWorker.onmessage = (e) => { + resolve(e.data); + }; + + nestedWorker.postMessage("Hello World"); + assertEquals(await promise, { type: "msg", text: "Hello World" }); + nestedWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker throws when executing", + fn: async function () { + const throwingWorker = new Worker( + resolveWorker("throwing_worker.js"), + { type: "module" }, + ); + + const { promise, resolve } = Promise.withResolvers(); + // deno-lint-ignore no-explicit-any + throwingWorker.onerror = (e: any) => { + e.preventDefault(); + resolve(e.message); + }; + + assertMatch( + await promise as string, + /Uncaught \(in promise\) Error: Thrown error/, + ); + throwingWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker globals", + fn: async function () { + const workerOptions: WorkerOptions = { type: "module" }; + const w = new Worker( + resolveWorker("worker_globals.ts"), + workerOptions, + ); + + const { promise, resolve } = Promise.withResolvers(); + w.onmessage = (e) => { + resolve(e.data); + }; + + w.postMessage("Hello, world!"); + assertEquals(await promise, "true, true, true, true"); + w.terminate(); + }, +}); + +Deno.test({ + name: "worker navigator", + fn: async function () { + const workerOptions: WorkerOptions = { type: "module" }; + const w = new Worker( + resolveWorker("worker_navigator.ts"), + workerOptions, + ); + + const { promise, resolve } = Promise.withResolvers(); + w.onmessage = (e) => { + resolve(e.data); + }; + + w.postMessage("Hello, world!"); + assertEquals(await promise, "string, object, string, number"); + w.terminate(); + }, +}); + +Deno.test({ + name: "worker fetch API", + fn: async function () { + const fetchingWorker = new Worker( + resolveWorker("fetching_worker.js"), + { type: "module" }, + ); + + const { promise, resolve, reject } = Promise.withResolvers(); + // deno-lint-ignore no-explicit-any + fetchingWorker.onerror = (e: any) => { + e.preventDefault(); + reject(e.message); + }; + // Defer promise.resolve() to allow worker to shut down + fetchingWorker.onmessage = (e) => { + resolve(e.data); + }; + + assertEquals(await promise, "Done!"); + fetchingWorker.terminate(); + }, +}); + +Deno.test({ + name: "worker terminate busy loop", + fn: async function () { + const { promise, resolve } = Promise.withResolvers(); + + const busyWorker = new Worker( + resolveWorker("busy_worker.js"), + { type: "module" }, + ); + + let testResult = 0; + + busyWorker.onmessage = (e) => { + testResult = e.data; + if (testResult >= 10000) { + busyWorker.terminate(); + busyWorker.onmessage = (_e) => { + throw new Error("unreachable"); + }; + setTimeout(() => { + resolve(testResult); + }, 100); + } + }; + + busyWorker.postMessage("ping"); + assertEquals(await promise, 10000); + }, +}); + +Deno.test({ + name: "worker race condition", + fn: async function () { + // See issue for details + // https://github.com/denoland/deno/issues/4080 + const { promise, resolve } = Promise.withResolvers(); + + const racyWorker = new Worker( + resolveWorker("racy_worker.js"), + { type: "module" }, + ); + + racyWorker.onmessage = (_e) => { + setTimeout(() => { + resolve(); + }, 100); + }; + + racyWorker.postMessage("START"); + await promise; + }, +}); + +Deno.test({ + name: "worker is event listener", + fn: async function () { + let messageHandlersCalled = 0; + let errorHandlersCalled = 0; + + const deferred1 = Promise.withResolvers(); + const deferred2 = Promise.withResolvers(); + + const worker = new Worker( + resolveWorker("event_worker.js"), + { type: "module" }, + ); + + worker.onmessage = (_e: Event) => { + messageHandlersCalled++; + }; + worker.addEventListener("message", (_e: Event) => { + messageHandlersCalled++; + }); + worker.addEventListener("message", (_e: Event) => { + messageHandlersCalled++; + deferred1.resolve(); + }); + + worker.onerror = (e) => { + errorHandlersCalled++; + e.preventDefault(); + }; + worker.addEventListener("error", (_e: Event) => { + errorHandlersCalled++; + }); + worker.addEventListener("error", (_e: Event) => { + errorHandlersCalled++; + deferred2.resolve(); + }); + + worker.postMessage("ping"); + await deferred1.promise; + assertEquals(messageHandlersCalled, 3); + + worker.postMessage("boom"); + await deferred2.promise; + assertEquals(errorHandlersCalled, 3); + worker.terminate(); + }, +}); + +Deno.test({ + name: "worker scope is event listener", + fn: async function () { + const worker = new Worker( + resolveWorker("event_worker_scope.js"), + { type: "module" }, + ); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers(); + worker.onmessage = (e: MessageEvent) => { + resolve(e.data); + }; + worker.onerror = (_e) => { + throw new Error("unreachable"); + }; + + worker.postMessage("boom"); + worker.postMessage("ping"); + assertEquals(await promise, { + messageHandlersCalled: 4, + errorHandlersCalled: 4, + }); + worker.terminate(); + }, +}); + +Deno.test({ + name: "worker with Deno namespace", + fn: async function () { + const denoWorker = new Worker( + resolveWorker("deno_worker.ts"), + { type: "module", deno: { permissions: "inherit" } }, + ); + + const { promise, resolve } = Promise.withResolvers(); + denoWorker.onmessage = (e) => { + denoWorker.terminate(); + resolve(e.data); + }; + + denoWorker.postMessage("Hello World"); + assertEquals(await promise, "Hello World"); + }, +}); + +Deno.test({ + name: "worker with crypto in scope", + fn: async function () { + const w = new Worker( + resolveWorker("worker_crypto.js"), + { type: "module" }, + ); + + const { promise, resolve } = Promise.withResolvers(); + w.onmessage = (e) => { + resolve(e.data); + }; + + w.postMessage(null); + assertEquals(await promise, true); + w.terminate(); + }, +}); + +Deno.test({ + name: "Worker event handler order", + fn: async function () { + const { promise, resolve } = Promise.withResolvers(); + const w = new Worker( + resolveWorker("test_worker.ts"), + { type: "module", name: "tsWorker" }, + ); + const arr: number[] = []; + w.addEventListener("message", () => arr.push(1)); + w.onmessage = (_e) => { + arr.push(2); + }; + w.addEventListener("message", () => arr.push(3)); + w.addEventListener("message", () => { + resolve(); + }); + w.postMessage("Hello World"); + await promise; + assertEquals(arr, [1, 2, 3]); + w.terminate(); + }, +}); + +Deno.test({ + name: "Worker immediate close", + fn: async function () { + const { promise, resolve } = Promise.withResolvers(); + const w = new Worker( + resolveWorker("immediately_close_worker.js"), + { type: "module" }, + ); + setTimeout(() => { + resolve(); + }, 1000); + await promise; + w.terminate(); + }, +}); + +Deno.test({ + name: "Worker post undefined", + fn: async function () { + const { promise, resolve } = Promise.withResolvers(); + const worker = new Worker( + resolveWorker("post_undefined.ts"), + { type: "module" }, + ); + + const handleWorkerMessage = (e: MessageEvent) => { + console.log("main <- worker:", e.data); + worker.terminate(); + resolve(); + }; + + worker.addEventListener("messageerror", () => console.log("message error")); + worker.addEventListener("error", () => console.log("error")); + worker.addEventListener("message", handleWorkerMessage); + + console.log("\npost from parent"); + worker.postMessage(undefined); + await promise; + }, +}); + +Deno.test("Worker inherits permissions", async function () { + const worker = new Worker( + resolveWorker("read_check_worker.js"), + { type: "module", deno: { permissions: "inherit" } }, + ); + + const { promise, resolve } = Promise.withResolvers(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage(null); + assertEquals(await promise, true); + worker.terminate(); +}); + +Deno.test("Worker limit children permissions", async function () { + const worker = new Worker( + resolveWorker("read_check_worker.js"), + { type: "module", deno: { permissions: { read: false } } }, + ); + + const { promise, resolve } = Promise.withResolvers(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage(null); + assertEquals(await promise, false); + worker.terminate(); +}); + +Deno.test("Worker limit children permissions granularly", async function () { + const workerUrl = resolveWorker("read_check_granular_worker.js"); + const worker = new Worker( + workerUrl, + { + type: "module", + deno: { + permissions: { + env: ["foo"], + hrtime: true, + net: ["foo", "bar:8000"], + ffi: [new URL("foo", workerUrl), "bar"], + read: [new URL("foo", workerUrl), "bar"], + run: [new URL("foo", workerUrl), "bar", "./baz"], + write: [new URL("foo", workerUrl), "bar"], + }, + }, + }, + ); + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers(); + worker.onmessage = ({ data }) => resolve(data); + assertEquals(await promise, { + envGlobal: "prompt", + envFoo: "granted", + envAbsent: "prompt", + hrtime: "granted", + netGlobal: "prompt", + netFoo: "granted", + netFoo8000: "granted", + netBar: "prompt", + netBar8000: "granted", + ffiGlobal: "prompt", + ffiFoo: "granted", + ffiBar: "granted", + ffiAbsent: "prompt", + readGlobal: "prompt", + readFoo: "granted", + readBar: "granted", + readAbsent: "prompt", + runGlobal: "prompt", + runFoo: "granted", + runBar: "granted", + runBaz: "granted", + runAbsent: "prompt", + writeGlobal: "prompt", + writeFoo: "granted", + writeBar: "granted", + writeAbsent: "prompt", + }); + worker.terminate(); +}); + +Deno.test("Nested worker limit children permissions", async function () { + /** This worker has permissions but doesn't grant them to its children */ + const worker = new Worker( + resolveWorker("parent_read_check_worker.js"), + { type: "module", deno: { permissions: "inherit" } }, + ); + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers(); + worker.onmessage = ({ data }) => resolve(data); + assertEquals(await promise, { + envGlobal: "prompt", + envFoo: "prompt", + envAbsent: "prompt", + hrtime: "prompt", + netGlobal: "prompt", + netFoo: "prompt", + netFoo8000: "prompt", + netBar: "prompt", + netBar8000: "prompt", + ffiGlobal: "prompt", + ffiFoo: "prompt", + ffiBar: "prompt", + ffiAbsent: "prompt", + readGlobal: "prompt", + readFoo: "prompt", + readBar: "prompt", + readAbsent: "prompt", + runGlobal: "prompt", + runFoo: "prompt", + runBar: "prompt", + runBaz: "prompt", + runAbsent: "prompt", + writeGlobal: "prompt", + writeFoo: "prompt", + writeBar: "prompt", + writeAbsent: "prompt", + }); + worker.terminate(); +}); + +// This test relies on env permissions not being granted on main thread +Deno.test({ + name: + "Worker initialization throws on worker permissions greater than parent thread permissions", + permissions: { env: false }, + fn: function () { + assertThrows( + () => { + const worker = new Worker( + resolveWorker("deno_worker.ts"), + { type: "module", deno: { permissions: { env: true } } }, + ); + worker.terminate(); + }, + Deno.errors.PermissionDenied, + "Can't escalate parent thread permissions", + ); + }, +}); + +Deno.test("Worker with disabled permissions", async function () { + const worker = new Worker( + resolveWorker("no_permissions_worker.js"), + { type: "module", deno: { permissions: "none" } }, + ); + + const { promise, resolve } = Promise.withResolvers(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage(null); + assertEquals(await promise, true); + worker.terminate(); +}); + +Deno.test("Worker permissions are not inherited with empty permission object", async function () { + const worker = new Worker( + resolveWorker("permission_echo.js"), + { type: "module", deno: { permissions: {} } }, + ); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage(null); + assertEquals(await promise, { + env: "prompt", + hrtime: "prompt", + net: "prompt", + ffi: "prompt", + read: "prompt", + run: "prompt", + write: "prompt", + }); + worker.terminate(); +}); + +Deno.test("Worker permissions are not inherited with single specified permission", async function () { + const worker = new Worker( + resolveWorker("permission_echo.js"), + { type: "module", deno: { permissions: { net: true } } }, + ); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage(null); + assertEquals(await promise, { + env: "prompt", + hrtime: "prompt", + net: "granted", + ffi: "prompt", + read: "prompt", + run: "prompt", + write: "prompt", + }); + worker.terminate(); +}); + +Deno.test("Worker with invalid permission arg", function () { + assertThrows( + () => + new Worker(`data:,close();`, { + type: "module", + // @ts-expect-error invalid env value + deno: { permissions: { env: "foo" } }, + }), + TypeError, + '(deno.permissions.env) invalid value: string "foo", expected "inherit" or boolean or string[]', + ); +}); + +Deno.test({ + name: "worker location", + fn: async function () { + const { promise, resolve } = Promise.withResolvers(); + const workerModuleHref = resolveWorker("worker_location.ts"); + const w = new Worker(workerModuleHref, { type: "module" }); + w.onmessage = (e) => { + resolve(e.data); + }; + w.postMessage("Hello, world!"); + assertEquals(await promise, `${workerModuleHref}, true`); + w.terminate(); + }, +}); + +Deno.test({ + name: "Worker with top-level-await", + fn: async function () { + const { promise, resolve, reject } = Promise.withResolvers(); + const worker = new Worker( + resolveWorker("worker_with_top_level_await.ts"), + { type: "module" }, + ); + worker.onmessage = (e) => { + if (e.data == "ready") { + worker.postMessage("trigger worker handler"); + } else if (e.data == "triggered worker handler") { + resolve(); + } else { + reject(new Error("Handler didn't run during top-level delay.")); + } + }; + await promise; + worker.terminate(); + }, +}); + +Deno.test({ + name: "Worker with native HTTP", + fn: async function () { + const { promise, resolve } = Promise.withResolvers(); + const worker = new Worker( + resolveWorker("http_worker.js"), + { type: "module", deno: { permissions: "inherit" } }, + ); + worker.onmessage = () => { + resolve(); + }; + await promise; + + assert(worker); + const response = await fetch("http://localhost:4506"); + assert(await response.arrayBuffer()); + worker.terminate(); + }, +}); + +Deno.test({ + name: "structured cloning postMessage", + fn: async function () { + const worker = new Worker( + resolveWorker("worker_structured_cloning.ts"), + { type: "module" }, + ); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers(); + worker.onmessage = (e) => { + resolve(e.data); + }; + + worker.postMessage("START"); + const data = await promise; + // self field should reference itself (circular ref) + assert(data === data.self); + // fields a and b refer to the same array + assertEquals(data.a, ["a", true, 432]); + assertEquals(data.b, ["a", true, 432]); + data.b[0] = "b"; + data.a[2] += 5; + assertEquals(data.a, ["b", true, 437]); + assertEquals(data.b, ["b", true, 437]); + // c is a set + const len = data.c.size; + data.c.add(1); // This value is already in the set. + data.c.add(2); + assertEquals(len + 1, data.c.size); + worker.terminate(); + }, +}); + +Deno.test({ + name: "worker with relative specifier", + fn: async function () { + assertEquals(location.href, "http://127.0.0.1:4545/"); + const w = new Worker( + "./workers/test_worker.ts", + { type: "module", name: "tsWorker" }, + ); + const { promise, resolve } = Promise.withResolvers(); + w.onmessage = (e) => { + resolve(e.data); + }; + w.postMessage("Hello, world!"); + assertEquals(await promise, "Hello, world!"); + w.terminate(); + }, +}); + +Deno.test({ + name: "worker SharedArrayBuffer", + fn: async function () { + const { promise, resolve } = Promise.withResolvers(); + const workerOptions: WorkerOptions = { type: "module" }; + const w = new Worker( + resolveWorker("shared_array_buffer.ts"), + workerOptions, + ); + const sab1 = new SharedArrayBuffer(1); + const sab2 = new SharedArrayBuffer(1); + const bytes1 = new Uint8Array(sab1); + const bytes2 = new Uint8Array(sab2); + assertEquals(bytes1[0], 0); + assertEquals(bytes2[0], 0); + w.onmessage = () => { + w.postMessage([sab1, sab2]); + w.onmessage = () => { + resolve(); + }; + }; + await promise; + assertEquals(bytes1[0], 1); + assertEquals(bytes2[0], 2); + w.terminate(); + }, +}); + +Deno.test({ + name: "Send MessagePorts from / to workers", + fn: async function () { + const worker = new Worker( + resolveWorker("message_port.ts"), + { type: "module" }, + ); + const channel = new MessageChannel(); + + // deno-lint-ignore no-explicit-any + const deferred1 = Promise.withResolvers(); + const deferred2 = Promise.withResolvers(); + const deferred3 = Promise.withResolvers(); + const result = Promise.withResolvers(); + worker.onmessage = (e) => { + deferred1.resolve([e.data, e.ports.length]); + const port1 = e.ports[0]; + port1.onmessage = (e) => { + deferred2.resolve(e.data); + port1.close(); + worker.postMessage("3", [channel.port1]); + }; + port1.postMessage("2"); + }; + channel.port2.onmessage = (e) => { + deferred3.resolve(e.data); + channel.port2.close(); + result.resolve(); + }; + + assertEquals(await deferred1.promise, ["1", 1]); + assertEquals(await deferred2.promise, true); + assertEquals(await deferred3.promise, true); + await result.promise; + worker.terminate(); + }, +}); + +Deno.test({ + name: "worker Deno.memoryUsage", + fn: async function () { + const w = new Worker( + /** + * Source code + * self.onmessage = function() {self.postMessage(Deno.memoryUsage())} + */ + "data:application/typescript;base64,c2VsZi5vbm1lc3NhZ2UgPSBmdW5jdGlvbigpIHtzZWxmLnBvc3RNZXNzYWdlKERlbm8ubWVtb3J5VXNhZ2UoKSl9", + { type: "module", name: "tsWorker" }, + ); + + w.postMessage(null); + + // deno-lint-ignore no-explicit-any + const { promise, resolve } = Promise.withResolvers(); + w.onmessage = function (evt) { + resolve(evt.data); + }; + + assertEquals( + Object.keys( + await promise as unknown as Record, + ), + ["rss", "heapTotal", "heapUsed", "external"], + ); + w.terminate(); + }, +}); diff --git a/cli/tests/unit/worker_types.ts b/cli/tests/unit/worker_types.ts deleted file mode 100644 index cb71418a4..000000000 --- a/cli/tests/unit/worker_types.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { assert } from "./test_util.ts"; - -Deno.test( - { permissions: { read: true } }, - function utimeSyncFileSuccess() { - const w = new Worker( - import.meta.resolve("../testdata/workers/worker_types.ts"), - { type: "module" }, - ); - assert(w); - w.terminate(); - }, -); -- cgit v1.2.3