summaryrefslogtreecommitdiff
path: root/tests/unit
diff options
context:
space:
mode:
authorMatt Mastracci <matthew@mastracci.com>2024-02-10 13:22:13 -0700
committerGitHub <noreply@github.com>2024-02-10 20:22:13 +0000
commitf5e46c9bf2f50d66a953fa133161fc829cecff06 (patch)
tree8faf2f5831c1c7b11d842cd9908d141082c869a5 /tests/unit
parentd2477f780630a812bfd65e3987b70c0d309385bb (diff)
chore: move cli/tests/ -> tests/ (#22369)
This looks like a massive PR, but it's only a move from cli/tests -> tests, and updates of relative paths for files. This is the first step towards aggregate all of the integration test files under tests/, which will lead to a set of integration tests that can run without the CLI binary being built. While we could leave these tests under `cli`, it would require us to keep a more complex directory structure for the various test runners. In addition, we have a lot of complexity to ignore various test files in the `cli` project itself (cargo publish exclusion rules, autotests = false, etc). And finally, the `tests/` folder will eventually house the `test_ffi`, `test_napi` and other testing code, reducing the size of the root repo directory. For easier review, the extremely large and noisy "move" is in the first commit (with no changes -- just a move), while the remainder of the changes to actual files is in the second commit.
Diffstat (limited to 'tests/unit')
-rw-r--r--tests/unit/README.md43
-rw-r--r--tests/unit/abort_controller_test.ts64
-rw-r--r--tests/unit/blob_test.ts115
-rw-r--r--tests/unit/body_test.ts189
-rw-r--r--tests/unit/broadcast_channel_test.ts34
-rw-r--r--tests/unit/buffer_test.ts461
-rw-r--r--tests/unit/build_test.ts10
-rw-r--r--tests/unit/cache_api_test.ts207
-rw-r--r--tests/unit/chmod_test.ts190
-rw-r--r--tests/unit/chown_test.ts190
-rw-r--r--tests/unit/command_test.ts967
-rw-r--r--tests/unit/console_test.ts2411
-rw-r--r--tests/unit/copy_file_test.ts249
-rw-r--r--tests/unit/cron_test.ts460
-rw-r--r--tests/unit/custom_event_test.ts27
-rw-r--r--tests/unit/dir_test.ts63
-rw-r--r--tests/unit/dom_exception_test.ts23
-rw-r--r--tests/unit/error_stack_test.ts54
-rw-r--r--tests/unit/error_test.ts33
-rw-r--r--tests/unit/esnext_test.ts43
-rw-r--r--tests/unit/event_target_test.ts295
-rw-r--r--tests/unit/event_test.ts143
-rw-r--r--tests/unit/fetch_test.ts2071
-rw-r--r--tests/unit/ffi_test.ts137
-rw-r--r--tests/unit/file_test.ts112
-rw-r--r--tests/unit/filereader_test.ts242
-rw-r--r--tests/unit/files_test.ts1095
-rw-r--r--tests/unit/flock_test.ts197
-rw-r--r--tests/unit/fs_events_test.ts139
-rw-r--r--tests/unit/get_random_values_test.ts63
-rw-r--r--tests/unit/globals_test.ts225
-rw-r--r--tests/unit/headers_test.ts416
-rw-r--r--tests/unit/http_test.ts2801
-rw-r--r--tests/unit/image_bitmap_test.ts92
-rw-r--r--tests/unit/image_data_test.ts53
-rw-r--r--tests/unit/internals_test.ts10
-rw-r--r--tests/unit/intl_test.ts7
-rw-r--r--tests/unit/io_test.ts77
-rw-r--r--tests/unit/jupyter_test.ts79
-rw-r--r--tests/unit/kv_queue_test.ts13
-rw-r--r--tests/unit/kv_queue_test_no_db_close.ts20
-rw-r--r--tests/unit/kv_queue_undelivered_test.ts59
-rw-r--r--tests/unit/kv_test.ts2321
-rw-r--r--tests/unit/link_test.ts195
-rw-r--r--tests/unit/make_temp_test.ts157
-rw-r--r--tests/unit/message_channel_test.ts55
-rw-r--r--tests/unit/mkdir_test.ts235
-rw-r--r--tests/unit/navigator_test.ts11
-rw-r--r--tests/unit/net_test.ts1274
-rw-r--r--tests/unit/network_interfaces_test.ts30
-rw-r--r--tests/unit/ops_test.ts17
-rw-r--r--tests/unit/os_test.ts304
-rw-r--r--tests/unit/path_from_url_test.ts41
-rw-r--r--tests/unit/performance_test.ts185
-rw-r--r--tests/unit/permissions_test.ts202
-rw-r--r--tests/unit/process_test.ts689
-rw-r--r--tests/unit/progressevent_test.ts18
-rw-r--r--tests/unit/promise_hooks_test.ts109
-rw-r--r--tests/unit/read_dir_test.ts113
-rw-r--r--tests/unit/read_file_test.ts182
-rw-r--r--tests/unit/read_link_test.ts99
-rw-r--r--tests/unit/read_text_file_test.ts208
-rw-r--r--tests/unit/real_path_test.ts114
-rw-r--r--tests/unit/ref_unref_test.ts12
-rw-r--r--tests/unit/remove_test.ts291
-rw-r--r--tests/unit/rename_test.ts274
-rw-r--r--tests/unit/request_test.ts77
-rw-r--r--tests/unit/resources_test.ts55
-rw-r--r--tests/unit/response_test.ts102
-rw-r--r--tests/unit/serve_test.ts3932
-rw-r--r--tests/unit/signal_test.ts296
-rw-r--r--tests/unit/stat_test.ts342
-rw-r--r--tests/unit/stdio_test.ts32
-rw-r--r--tests/unit/streams_test.ts478
-rw-r--r--tests/unit/structured_clone_test.ts55
-rw-r--r--tests/unit/symbol_test.ts11
-rw-r--r--tests/unit/symlink_test.ts140
-rw-r--r--tests/unit/sync_test.ts69
-rw-r--r--tests/unit/test_util.ts87
-rw-r--r--tests/unit/testing_test.ts154
-rw-r--r--tests/unit/text_encoding_test.ts326
-rw-r--r--tests/unit/timers_test.ts763
-rw-r--r--tests/unit/tls_test.ts1546
-rw-r--r--tests/unit/truncate_test.ts114
-rw-r--r--tests/unit/tty_color_test.ts55
-rw-r--r--tests/unit/tty_test.ts32
-rw-r--r--tests/unit/umask_test.ts15
-rw-r--r--tests/unit/url_search_params_test.ts356
-rw-r--r--tests/unit/url_test.ts529
-rw-r--r--tests/unit/urlpattern_test.ts65
-rw-r--r--tests/unit/utime_test.ts337
-rw-r--r--tests/unit/version_test.ts10
-rw-r--r--tests/unit/wasm_test.ts104
-rw-r--r--tests/unit/webcrypto_test.ts2047
-rw-r--r--tests/unit/webgpu_test.ts267
-rw-r--r--tests/unit/websocket_test.ts738
-rw-r--r--tests/unit/websocketstream_test.ts.disabled359
-rw-r--r--tests/unit/webstorage_test.ts52
-rw-r--r--tests/unit/worker_permissions_test.ts34
-rw-r--r--tests/unit/worker_test.ts843
-rw-r--r--tests/unit/write_file_test.ts427
-rw-r--r--tests/unit/write_text_file_test.ts218
102 files changed, 36382 insertions, 0 deletions
diff --git a/tests/unit/README.md b/tests/unit/README.md
new file mode 100644
index 000000000..af31c08fc
--- /dev/null
+++ b/tests/unit/README.md
@@ -0,0 +1,43 @@
+# Deno runtime tests
+
+Files in this directory are unit tests for Deno runtime.
+
+Testing Deno runtime code requires checking API under different runtime
+permissions. To accomplish this all tests exercised are created using
+`Deno.test()` function.
+
+```ts
+import {} from "./test_util.ts";
+
+Deno.test(function simpleTestFn(): void {
+ // test code here
+});
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ function complexTestFn(): void {
+ // test code here
+ },
+);
+```
+
+## Running tests
+
+There are two ways to run `unit_test_runner.ts`:
+
+```sh
+# Run all tests.
+cargo run --bin deno -- test --allow-all --unstable --location=http://js-unit-tests/foo/bar cli/tests/unit/
+
+# Run a specific test module
+cargo run --bin deno -- test --allow-all --unstable --location=http://js-unit-tests/foo/bar cli/tests/unit/files_test.ts
+```
+
+### Http server
+
+`target/debug/test_server` is required to run when one's running unit tests.
+During CI it's spawned automatically, but if you want to run tests manually make
+sure that server is spawned otherwise there'll be cascade of test failures.
diff --git a/tests/unit/abort_controller_test.ts b/tests/unit/abort_controller_test.ts
new file mode 100644
index 000000000..60ea6aa24
--- /dev/null
+++ b/tests/unit/abort_controller_test.ts
@@ -0,0 +1,64 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assert, assertEquals } from "./test_util.ts";
+
+Deno.test(function basicAbortController() {
+ const controller = new AbortController();
+ assert(controller);
+ const { signal } = controller;
+ assert(signal);
+ assertEquals(signal.aborted, false);
+ controller.abort();
+ assertEquals(signal.aborted, true);
+});
+
+Deno.test(function signalCallsOnabort() {
+ const controller = new AbortController();
+ const { signal } = controller;
+ let called = false;
+ signal.onabort = (evt) => {
+ assert(evt);
+ assertEquals(evt.type, "abort");
+ called = true;
+ };
+ controller.abort();
+ assert(called);
+});
+
+Deno.test(function signalEventListener() {
+ const controller = new AbortController();
+ const { signal } = controller;
+ let called = false;
+ signal.addEventListener("abort", function (ev) {
+ assert(this === signal);
+ assertEquals(ev.type, "abort");
+ called = true;
+ });
+ controller.abort();
+ assert(called);
+});
+
+Deno.test(function onlyAbortsOnce() {
+ const controller = new AbortController();
+ const { signal } = controller;
+ let called = 0;
+ signal.addEventListener("abort", () => called++);
+ signal.onabort = () => {
+ called++;
+ };
+ controller.abort();
+ assertEquals(called, 2);
+ controller.abort();
+ assertEquals(called, 2);
+});
+
+Deno.test(function controllerHasProperToString() {
+ const actual = Object.prototype.toString.call(new AbortController());
+ assertEquals(actual, "[object AbortController]");
+});
+
+Deno.test(function abortReason() {
+ const signal = AbortSignal.abort("hey!");
+ assertEquals(signal.aborted, true);
+ assertEquals(signal.reason, "hey!");
+});
diff --git a/tests/unit/blob_test.ts b/tests/unit/blob_test.ts
new file mode 100644
index 000000000..e6623a65c
--- /dev/null
+++ b/tests/unit/blob_test.ts
@@ -0,0 +1,115 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals, assertStringIncludes } from "./test_util.ts";
+import { concat } from "@test_util/std/bytes/concat.ts";
+
+Deno.test(function blobString() {
+ const b1 = new Blob(["Hello World"]);
+ const str = "Test";
+ const b2 = new Blob([b1, str]);
+ assertEquals(b2.size, b1.size + str.length);
+});
+
+Deno.test(function blobBuffer() {
+ const buffer = new ArrayBuffer(12);
+ const u8 = new Uint8Array(buffer);
+ const f1 = new Float32Array(buffer);
+ const b1 = new Blob([buffer, u8]);
+ assertEquals(b1.size, 2 * u8.length);
+ const b2 = new Blob([b1, f1]);
+ assertEquals(b2.size, 3 * u8.length);
+});
+
+Deno.test(function blobSlice() {
+ const blob = new Blob(["Deno", "Foo"]);
+ const b1 = blob.slice(0, 3, "Text/HTML");
+ assert(b1 instanceof Blob);
+ assertEquals(b1.size, 3);
+ assertEquals(b1.type, "text/html");
+ const b2 = blob.slice(-1, 3);
+ assertEquals(b2.size, 0);
+ const b3 = blob.slice(100, 3);
+ assertEquals(b3.size, 0);
+ const b4 = blob.slice(0, 10);
+ assertEquals(b4.size, blob.size);
+});
+
+Deno.test(function blobInvalidType() {
+ const blob = new Blob(["foo"], {
+ type: "\u0521",
+ });
+
+ assertEquals(blob.type, "");
+});
+
+Deno.test(function blobShouldNotThrowError() {
+ let hasThrown = false;
+
+ try {
+ // deno-lint-ignore no-explicit-any
+ const options1: any = {
+ ending: "utf8",
+ hasOwnProperty: "hasOwnProperty",
+ };
+ const options2 = Object.create(null);
+ new Blob(["Hello World"], options1);
+ new Blob(["Hello World"], options2);
+ } catch {
+ hasThrown = true;
+ }
+
+ assertEquals(hasThrown, false);
+});
+
+/* TODO https://github.com/denoland/deno/issues/7540
+Deno.test(function nativeEndLine() {
+ const options = {
+ ending: "native",
+ } as const;
+ const blob = new Blob(["Hello\nWorld"], options);
+
+ assertEquals(blob.size, Deno.build.os === "windows" ? 12 : 11);
+});
+*/
+
+Deno.test(async function blobText() {
+ const blob = new Blob(["Hello World"]);
+ assertEquals(await blob.text(), "Hello World");
+});
+
+Deno.test(async function blobStream() {
+ const blob = new Blob(["Hello World"]);
+ const stream = blob.stream();
+ assert(stream instanceof ReadableStream);
+ const reader = stream.getReader();
+ let bytes = new Uint8Array();
+ const read = async (): Promise<void> => {
+ const { done, value } = await reader.read();
+ if (!done && value) {
+ bytes = concat(bytes, value);
+ return read();
+ }
+ };
+ await read();
+ const decoder = new TextDecoder();
+ assertEquals(decoder.decode(bytes), "Hello World");
+});
+
+Deno.test(async function blobArrayBuffer() {
+ const uint = new Uint8Array([102, 111, 111]);
+ const blob = new Blob([uint]);
+ assertEquals(await blob.arrayBuffer(), uint.buffer);
+});
+
+Deno.test(function blobConstructorNameIsBlob() {
+ const blob = new Blob();
+ assertEquals(blob.constructor.name, "Blob");
+});
+
+Deno.test(function blobCustomInspectFunction() {
+ const blob = new Blob();
+ assertEquals(
+ Deno.inspect(blob),
+ `Blob { size: 0, type: "" }`,
+ );
+ assertStringIncludes(Deno.inspect(Blob.prototype), "Blob");
+});
diff --git a/tests/unit/body_test.ts b/tests/unit/body_test.ts
new file mode 100644
index 000000000..18cdb22be
--- /dev/null
+++ b/tests/unit/body_test.ts
@@ -0,0 +1,189 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals } from "./test_util.ts";
+
+// just a hack to get a body object
+// deno-lint-ignore no-explicit-any
+function buildBody(body: any, headers?: Headers): Body {
+ const stub = new Request("http://foo/", {
+ body: body,
+ headers,
+ method: "POST",
+ });
+ return stub as Body;
+}
+
+const intArrays = [
+ Int8Array,
+ Int16Array,
+ Int32Array,
+ Uint8Array,
+ Uint16Array,
+ Uint32Array,
+ Uint8ClampedArray,
+ Float32Array,
+ Float64Array,
+];
+Deno.test(async function arrayBufferFromByteArrays() {
+ const buffer = new TextEncoder().encode("ahoyhoy8").buffer;
+
+ for (const type of intArrays) {
+ const body = buildBody(new type(buffer));
+ const text = new TextDecoder("utf-8").decode(await body.arrayBuffer());
+ assertEquals(text, "ahoyhoy8");
+ }
+});
+
+//FormData
+Deno.test(
+ { permissions: { net: true } },
+ async function bodyMultipartFormData() {
+ const response = await fetch(
+ "http://localhost:4545/multipart_form_data.txt",
+ );
+ assert(response.body instanceof ReadableStream);
+
+ const text = await response.text();
+
+ const body = buildBody(text, response.headers);
+
+ const formData = await body.formData();
+ assert(formData.has("field_1"));
+ assertEquals(formData.get("field_1")!.toString(), "value_1 \r\n");
+ assert(formData.has("field_2"));
+ },
+);
+
+// FormData: non-ASCII names and filenames
+Deno.test(
+ { permissions: { net: true } },
+ async function bodyMultipartFormDataNonAsciiNames() {
+ const boundary = "----01230123";
+ const payload = [
+ `--${boundary}`,
+ `Content-Disposition: form-data; name="文字"`,
+ "",
+ "文字",
+ `--${boundary}`,
+ `Content-Disposition: form-data; name="file"; filename="文字"`,
+ "Content-Type: application/octet-stream",
+ "",
+ "",
+ `--${boundary}--`,
+ ].join("\r\n");
+
+ const body = buildBody(
+ new TextEncoder().encode(payload),
+ new Headers({
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
+ }),
+ );
+
+ const formData = await body.formData();
+ assert(formData.has("文字"));
+ assertEquals(formData.get("文字"), "文字");
+ assert(formData.has("file"));
+ assert(formData.get("file") instanceof File);
+ assertEquals((formData.get("file") as File).name, "文字");
+ },
+);
+
+// FormData: non-ASCII names and filenames roundtrip
+Deno.test(
+ { permissions: { net: true } },
+ async function bodyMultipartFormDataNonAsciiRoundtrip() {
+ const inFormData = new FormData();
+ inFormData.append("文字", "文字");
+ inFormData.append("file", new File([], "文字"));
+
+ const body = buildBody(inFormData);
+
+ const formData = await body.formData();
+ assert(formData.has("文字"));
+ assertEquals(formData.get("文字"), "文字");
+ assert(formData.has("file"));
+ assert(formData.get("file") instanceof File);
+ assertEquals((formData.get("file") as File).name, "文字");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function bodyURLEncodedFormData() {
+ const response = await fetch(
+ "http://localhost:4545/subdir/form_urlencoded.txt",
+ );
+ assert(response.body instanceof ReadableStream);
+
+ const text = await response.text();
+
+ const body = buildBody(text, response.headers);
+
+ const formData = await body.formData();
+ assert(formData.has("field_1"));
+ assertEquals(formData.get("field_1")!.toString(), "Hi");
+ assert(formData.has("field_2"));
+ assertEquals(formData.get("field_2")!.toString(), "<Deno>");
+ },
+);
+
+Deno.test({ permissions: {} }, async function bodyURLSearchParams() {
+ const body = buildBody(new URLSearchParams({ hello: "world" }));
+
+ const text = await body.text();
+ assertEquals(text, "hello=world");
+});
+
+Deno.test(async function bodyArrayBufferMultipleParts() {
+ const parts: Uint8Array[] = [];
+ let size = 0;
+ for (let i = 0; i <= 150000; i++) {
+ const part = new Uint8Array([1]);
+ parts.push(part);
+ size += part.length;
+ }
+
+ let offset = 0;
+ const stream = new ReadableStream({
+ pull(controller) {
+ // parts.shift() takes forever: https://github.com/denoland/deno/issues/5259
+ const chunk = parts[offset++];
+ if (!chunk) return controller.close();
+ controller.enqueue(chunk);
+ },
+ });
+
+ const body = buildBody(stream);
+ assertEquals((await body.arrayBuffer()).byteLength, size);
+});
+
+// https://github.com/denoland/deno/issues/20793
+Deno.test(
+ { permissions: { net: true } },
+ async function bodyMultipartFormDataMultipleHeaders() {
+ const boundary = "----formdata-polyfill-0.970665446687947";
+ const payload = [
+ "------formdata-polyfill-0.970665446687947",
+ 'Content-Disposition: form-data; name="x"; filename="blob"',
+ "Content-Length: 1",
+ "Content-Type: application/octet-stream",
+ "last-modified: Wed, 04 Oct 2023 20:28:45 GMT",
+ "",
+ "y",
+ "------formdata-polyfill-0.970665446687947--",
+ ].join("\r\n");
+
+ const body = buildBody(
+ new TextEncoder().encode(payload),
+ new Headers({
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
+ }),
+ );
+
+ const formData = await body.formData();
+ const file = formData.get("x");
+ assert(file instanceof File);
+ const text = await file.text();
+ assertEquals(text, "y");
+ assertEquals(file.size, 1);
+ },
+);
diff --git a/tests/unit/broadcast_channel_test.ts b/tests/unit/broadcast_channel_test.ts
new file mode 100644
index 000000000..c5d7f7e7f
--- /dev/null
+++ b/tests/unit/broadcast_channel_test.ts
@@ -0,0 +1,34 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "@test_util/std/assert/mod.ts";
+
+Deno.test("BroadcastChannel worker", async () => {
+ const intercom = new BroadcastChannel("intercom");
+ let count = 0;
+
+ const url = import.meta.resolve(
+ "../testdata/workers/broadcast_channel.ts",
+ );
+ const worker = new Worker(url, { type: "module", name: "worker" });
+ worker.onmessage = () => intercom.postMessage(++count);
+
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ intercom.onmessage = function (e) {
+ assertEquals(count, e.data);
+ if (count < 42) {
+ intercom.postMessage(++count);
+ } else {
+ worker.terminate();
+ intercom.close();
+ resolve();
+ }
+ };
+
+ await promise;
+});
+
+Deno.test("BroadcastChannel immediate close after post", () => {
+ const bc = new BroadcastChannel("internal_notification");
+ bc.postMessage("New listening connected!");
+ bc.close();
+});
diff --git a/tests/unit/buffer_test.ts b/tests/unit/buffer_test.ts
new file mode 100644
index 000000000..9d7e51a95
--- /dev/null
+++ b/tests/unit/buffer_test.ts
@@ -0,0 +1,461 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+// deno-lint-ignore-file no-deprecated-deno-api
+
+// This code has been ported almost directly from Go's src/bytes/buffer_test.go
+// Copyright 2009 The Go Authors. All rights reserved. BSD license.
+// https://github.com/golang/go/blob/master/LICENSE
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+} from "./test_util.ts";
+
+const MAX_SIZE = 2 ** 32 - 2;
+// N controls how many iterations of certain checks are performed.
+const N = 100;
+let testBytes: Uint8Array | null;
+let testString: string | null;
+
+const ignoreMaxSizeTests = true;
+
+function init() {
+ if (testBytes == null) {
+ testBytes = new Uint8Array(N);
+ for (let i = 0; i < N; i++) {
+ testBytes[i] = "a".charCodeAt(0) + (i % 26);
+ }
+ const decoder = new TextDecoder();
+ testString = decoder.decode(testBytes);
+ }
+}
+
+function check(buf: Deno.Buffer, s: string) {
+ const bytes = buf.bytes();
+ assertEquals(buf.length, bytes.byteLength);
+ const decoder = new TextDecoder();
+ const bytesStr = decoder.decode(bytes);
+ assertEquals(bytesStr, s);
+ assertEquals(buf.length, s.length);
+}
+
+// Fill buf through n writes of byte slice fub.
+// The initial contents of buf corresponds to the string s;
+// the result is the final contents of buf returned as a string.
+async function fillBytes(
+ buf: Deno.Buffer,
+ s: string,
+ n: number,
+ fub: Uint8Array,
+): Promise<string> {
+ check(buf, s);
+ for (; n > 0; n--) {
+ const m = await buf.write(fub);
+ assertEquals(m, fub.byteLength);
+ const decoder = new TextDecoder();
+ s += decoder.decode(fub);
+ check(buf, s);
+ }
+ return s;
+}
+
+// Empty buf through repeated reads into fub.
+// The initial contents of buf corresponds to the string s.
+async function empty(
+ buf: Deno.Buffer,
+ s: string,
+ fub: Uint8Array,
+) {
+ check(buf, s);
+ while (true) {
+ const r = await buf.read(fub);
+ if (r === null) {
+ break;
+ }
+ s = s.slice(r);
+ check(buf, s);
+ }
+ check(buf, "");
+}
+
+function repeat(c: string, bytes: number): Uint8Array {
+ assertEquals(c.length, 1);
+ const ui8 = new Uint8Array(bytes);
+ ui8.fill(c.charCodeAt(0));
+ return ui8;
+}
+
+Deno.test(function bufferNewBuffer() {
+ init();
+ assert(testBytes);
+ assert(testString);
+ const buf = new Deno.Buffer(testBytes.buffer as ArrayBuffer);
+ check(buf, testString);
+});
+
+Deno.test(async function bufferBasicOperations() {
+ init();
+ assert(testBytes);
+ assert(testString);
+ const buf = new Deno.Buffer();
+ for (let i = 0; i < 5; i++) {
+ check(buf, "");
+
+ buf.reset();
+ check(buf, "");
+
+ buf.truncate(0);
+ check(buf, "");
+
+ let n = await buf.write(testBytes.subarray(0, 1));
+ assertEquals(n, 1);
+ check(buf, "a");
+
+ n = await buf.write(testBytes.subarray(1, 2));
+ assertEquals(n, 1);
+ check(buf, "ab");
+
+ n = await buf.write(testBytes.subarray(2, 26));
+ assertEquals(n, 24);
+ check(buf, testString.slice(0, 26));
+
+ buf.truncate(26);
+ check(buf, testString.slice(0, 26));
+
+ buf.truncate(20);
+ check(buf, testString.slice(0, 20));
+
+ await empty(buf, testString.slice(0, 20), new Uint8Array(5));
+ await empty(buf, "", new Uint8Array(100));
+
+ // TODO(bartlomieju): buf.writeByte()
+ // TODO(bartlomieju): buf.readByte()
+ }
+});
+
+Deno.test(async function bufferReadEmptyAtEOF() {
+ // check that EOF of 'buf' is not reached (even though it's empty) if
+ // results are written to buffer that has 0 length (ie. it can't store any data)
+ const buf = new Deno.Buffer();
+ const zeroLengthTmp = new Uint8Array(0);
+ const result = await buf.read(zeroLengthTmp);
+ assertEquals(result, 0);
+});
+
+Deno.test(async function bufferLargeByteWrites() {
+ init();
+ const buf = new Deno.Buffer();
+ const limit = 9;
+ for (let i = 3; i < limit; i += 3) {
+ const s = await fillBytes(buf, "", 5, testBytes!);
+ await empty(buf, s, new Uint8Array(Math.floor(testString!.length / i)));
+ }
+ check(buf, "");
+});
+
+Deno.test(async function bufferTooLargeByteWrites() {
+ init();
+ const tmp = new Uint8Array(72);
+ const growLen = Number.MAX_VALUE;
+ const xBytes = repeat("x", 0);
+ const buf = new Deno.Buffer(xBytes.buffer as ArrayBuffer);
+ await buf.read(tmp);
+
+ assertThrows(
+ () => {
+ buf.grow(growLen);
+ },
+ Error,
+ "grown beyond the maximum size",
+ );
+});
+
+Deno.test(
+ { ignore: ignoreMaxSizeTests },
+ function bufferGrowWriteMaxBuffer() {
+ const bufSize = 16 * 1024;
+ const capacities = [MAX_SIZE, MAX_SIZE - 1];
+ for (const capacity of capacities) {
+ let written = 0;
+ const buf = new Deno.Buffer();
+ const writes = Math.floor(capacity / bufSize);
+ for (let i = 0; i < writes; i++) {
+ written += buf.writeSync(repeat("x", bufSize));
+ }
+
+ if (written < capacity) {
+ written += buf.writeSync(repeat("x", capacity - written));
+ }
+
+ assertEquals(written, capacity);
+ }
+ },
+);
+
+Deno.test(
+ { ignore: ignoreMaxSizeTests },
+ async function bufferGrowReadCloseMaxBufferPlus1() {
+ const reader = new Deno.Buffer(new ArrayBuffer(MAX_SIZE + 1));
+ const buf = new Deno.Buffer();
+
+ await assertRejects(
+ async () => {
+ await buf.readFrom(reader);
+ },
+ Error,
+ "grown beyond the maximum size",
+ );
+ },
+);
+
+Deno.test(
+ { ignore: ignoreMaxSizeTests },
+ function bufferGrowReadSyncCloseMaxBufferPlus1() {
+ const reader = new Deno.Buffer(new ArrayBuffer(MAX_SIZE + 1));
+ const buf = new Deno.Buffer();
+
+ assertThrows(
+ () => {
+ buf.readFromSync(reader);
+ },
+ Error,
+ "grown beyond the maximum size",
+ );
+ },
+);
+
+Deno.test(
+ { ignore: ignoreMaxSizeTests },
+ function bufferGrowReadSyncCloseToMaxBuffer() {
+ const capacities = [MAX_SIZE, MAX_SIZE - 1];
+ for (const capacity of capacities) {
+ const reader = new Deno.Buffer(new ArrayBuffer(capacity));
+ const buf = new Deno.Buffer();
+ buf.readFromSync(reader);
+
+ assertEquals(buf.length, capacity);
+ }
+ },
+);
+
+Deno.test(
+ { ignore: ignoreMaxSizeTests },
+ async function bufferGrowReadCloseToMaxBuffer() {
+ const capacities = [MAX_SIZE, MAX_SIZE - 1];
+ for (const capacity of capacities) {
+ const reader = new Deno.Buffer(new ArrayBuffer(capacity));
+ const buf = new Deno.Buffer();
+ await buf.readFrom(reader);
+ assertEquals(buf.length, capacity);
+ }
+ },
+);
+
+Deno.test(
+ { ignore: ignoreMaxSizeTests },
+ async function bufferReadCloseToMaxBufferWithInitialGrow() {
+ const capacities = [MAX_SIZE, MAX_SIZE - 1, MAX_SIZE - 512];
+ for (const capacity of capacities) {
+ const reader = new Deno.Buffer(new ArrayBuffer(capacity));
+ const buf = new Deno.Buffer();
+ buf.grow(MAX_SIZE);
+ await buf.readFrom(reader);
+ assertEquals(buf.length, capacity);
+ }
+ },
+);
+
+Deno.test(async function bufferLargeByteReads() {
+ init();
+ assert(testBytes);
+ assert(testString);
+ const buf = new Deno.Buffer();
+ for (let i = 3; i < 30; i += 3) {
+ const n = Math.floor(testBytes.byteLength / i);
+ const s = await fillBytes(buf, "", 5, testBytes.subarray(0, n));
+ await empty(buf, s, new Uint8Array(testString.length));
+ }
+ check(buf, "");
+});
+
+Deno.test(function bufferCapWithPreallocatedSlice() {
+ const buf = new Deno.Buffer(new ArrayBuffer(10));
+ assertEquals(buf.capacity, 10);
+});
+
+Deno.test(async function bufferReadFrom() {
+ init();
+ assert(testBytes);
+ assert(testString);
+ const buf = new Deno.Buffer();
+ for (let i = 3; i < 30; i += 3) {
+ const s = await fillBytes(
+ buf,
+ "",
+ 5,
+ testBytes.subarray(0, Math.floor(testBytes.byteLength / i)),
+ );
+ const b = new Deno.Buffer();
+ await b.readFrom(buf);
+ const fub = new Uint8Array(testString.length);
+ await empty(b, s, fub);
+ }
+ await assertRejects(async function () {
+ await new Deno.Buffer().readFrom(null!);
+ });
+});
+
+Deno.test(async function bufferReadFromSync() {
+ init();
+ assert(testBytes);
+ assert(testString);
+ const buf = new Deno.Buffer();
+ for (let i = 3; i < 30; i += 3) {
+ const s = await fillBytes(
+ buf,
+ "",
+ 5,
+ testBytes.subarray(0, Math.floor(testBytes.byteLength / i)),
+ );
+ const b = new Deno.Buffer();
+ b.readFromSync(buf);
+ const fub = new Uint8Array(testString.length);
+ await empty(b, s, fub);
+ }
+ assertThrows(function () {
+ new Deno.Buffer().readFromSync(null!);
+ });
+});
+
+Deno.test(async function bufferTestGrow() {
+ const tmp = new Uint8Array(72);
+ for (const startLen of [0, 100, 1000, 10000]) {
+ const xBytes = repeat("x", startLen);
+ for (const growLen of [0, 100, 1000, 10000]) {
+ const buf = new Deno.Buffer(xBytes.buffer as ArrayBuffer);
+ // If we read, this affects buf.off, which is good to test.
+ const nread = (await buf.read(tmp)) ?? 0;
+ buf.grow(growLen);
+ const yBytes = repeat("y", growLen);
+ await buf.write(yBytes);
+ // Check that buffer has correct data.
+ assertEquals(
+ buf.bytes().subarray(0, startLen - nread),
+ xBytes.subarray(nread),
+ );
+ assertEquals(
+ buf.bytes().subarray(startLen - nread, startLen - nread + growLen),
+ yBytes,
+ );
+ }
+ }
+});
+
+Deno.test(async function testReadAll() {
+ init();
+ assert(testBytes);
+ const reader = new Deno.Buffer(testBytes.buffer as ArrayBuffer);
+ const actualBytes = await Deno.readAll(reader);
+ assertEquals(testBytes.byteLength, actualBytes.byteLength);
+ for (let i = 0; i < testBytes.length; ++i) {
+ assertEquals(testBytes[i], actualBytes[i]);
+ }
+});
+
+Deno.test(function testReadAllSync() {
+ init();
+ assert(testBytes);
+ const reader = new Deno.Buffer(testBytes.buffer as ArrayBuffer);
+ const actualBytes = Deno.readAllSync(reader);
+ assertEquals(testBytes.byteLength, actualBytes.byteLength);
+ for (let i = 0; i < testBytes.length; ++i) {
+ assertEquals(testBytes[i], actualBytes[i]);
+ }
+});
+
+Deno.test(async function testWriteAll() {
+ init();
+ assert(testBytes);
+ const writer = new Deno.Buffer();
+ await Deno.writeAll(writer, testBytes);
+ const actualBytes = writer.bytes();
+ assertEquals(testBytes.byteLength, actualBytes.byteLength);
+ for (let i = 0; i < testBytes.length; ++i) {
+ assertEquals(testBytes[i], actualBytes[i]);
+ }
+});
+
+Deno.test(function testWriteAllSync() {
+ init();
+ assert(testBytes);
+ const writer = new Deno.Buffer();
+ Deno.writeAllSync(writer, testBytes);
+ const actualBytes = writer.bytes();
+ assertEquals(testBytes.byteLength, actualBytes.byteLength);
+ for (let i = 0; i < testBytes.length; ++i) {
+ assertEquals(testBytes[i], actualBytes[i]);
+ }
+});
+
+Deno.test(function testBufferBytesArrayBufferLength() {
+ // defaults to copy
+ const args = [{}, { copy: undefined }, undefined, { copy: true }];
+ for (const arg of args) {
+ const bufSize = 64 * 1024;
+ const bytes = new TextEncoder().encode("a".repeat(bufSize));
+ const reader = new Deno.Buffer();
+ Deno.writeAllSync(reader, bytes);
+
+ const writer = new Deno.Buffer();
+ writer.readFromSync(reader);
+ const actualBytes = writer.bytes(arg);
+
+ assertEquals(actualBytes.byteLength, bufSize);
+ assert(actualBytes.buffer !== writer.bytes(arg).buffer);
+ assertEquals(actualBytes.byteLength, actualBytes.buffer.byteLength);
+ }
+});
+
+Deno.test(function testBufferBytesCopyFalse() {
+ const bufSize = 64 * 1024;
+ const bytes = new TextEncoder().encode("a".repeat(bufSize));
+ const reader = new Deno.Buffer();
+ Deno.writeAllSync(reader, bytes);
+
+ const writer = new Deno.Buffer();
+ writer.readFromSync(reader);
+ const actualBytes = writer.bytes({ copy: false });
+
+ assertEquals(actualBytes.byteLength, bufSize);
+ assertEquals(actualBytes.buffer, writer.bytes({ copy: false }).buffer);
+ assert(actualBytes.buffer.byteLength > actualBytes.byteLength);
+});
+
+Deno.test(function testBufferBytesCopyFalseGrowExactBytes() {
+ const bufSize = 64 * 1024;
+ const bytes = new TextEncoder().encode("a".repeat(bufSize));
+ const reader = new Deno.Buffer();
+ Deno.writeAllSync(reader, bytes);
+
+ const writer = new Deno.Buffer();
+ writer.grow(bufSize);
+ writer.readFromSync(reader);
+ const actualBytes = writer.bytes({ copy: false });
+
+ assertEquals(actualBytes.byteLength, bufSize);
+ assertEquals(actualBytes.buffer.byteLength, actualBytes.byteLength);
+});
+
+Deno.test(function testThrowsErrorWhenBufferExceedsMaxLength() {
+ const kStringMaxLengthPlusOne = 536870888 + 1;
+ const bytes = new Uint8Array(kStringMaxLengthPlusOne);
+
+ assertThrows(
+ () => {
+ new TextDecoder().decode(bytes);
+ },
+ TypeError,
+ "buffer exceeds maximum length",
+ );
+});
diff --git a/tests/unit/build_test.ts b/tests/unit/build_test.ts
new file mode 100644
index 000000000..f697b64d3
--- /dev/null
+++ b/tests/unit/build_test.ts
@@ -0,0 +1,10 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert } from "./test_util.ts";
+
+Deno.test(function buildInfo() {
+ // Deno.build is injected by rollup at compile time. Here
+ // we check it has been properly transformed.
+ const { arch, os } = Deno.build;
+ assert(arch.length > 0);
+ assert(os === "darwin" || os === "windows" || os === "linux");
+});
diff --git a/tests/unit/cache_api_test.ts b/tests/unit/cache_api_test.ts
new file mode 100644
index 000000000..792929870
--- /dev/null
+++ b/tests/unit/cache_api_test.ts
@@ -0,0 +1,207 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertFalse,
+ assertRejects,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test(async function cacheStorage() {
+ const cacheName = "cache-v1";
+ const _cache = await caches.open(cacheName);
+ assert(await caches.has(cacheName));
+ assert(await caches.delete(cacheName));
+ assertFalse(await caches.has(cacheName));
+});
+
+Deno.test(async function cacheApi() {
+ const cacheName = "cache-v1";
+ const cache = await caches.open(cacheName);
+ // Test cache.put() with url string as key.
+ {
+ const req = "https://deno.com";
+ await cache.put(req, new Response("deno.com - key is string"));
+ const res = await cache.match(req);
+ assertEquals(await res?.text(), "deno.com - key is string");
+ assert(await cache.delete(req));
+ }
+ // Test cache.put() with url instance as key.
+ {
+ const req = new URL("https://deno.com");
+ await cache.put(req, new Response("deno.com - key is URL"));
+ const res = await cache.match(req);
+ assertEquals(await res?.text(), "deno.com - key is URL");
+ assert(await cache.delete(req));
+ }
+ // Test cache.put() with request instance as key.
+ {
+ const req = new Request("https://deno.com");
+ await cache.put(req, new Response("deno.com - key is Request"));
+ const res = await cache.match(req);
+ assertEquals(await res?.text(), "deno.com - key is Request");
+ assert(await cache.delete(req));
+ }
+
+ // Test cache.put() throws with response Vary header set to *.
+ {
+ const req = new Request("https://deno.com");
+ assertRejects(
+ async () => {
+ await cache.put(
+ req,
+ new Response("deno.com - key is Request", {
+ headers: { Vary: "*" },
+ }),
+ );
+ },
+ TypeError,
+ "Vary header must not contain '*'",
+ );
+ }
+
+ // Test cache.match() with same url but different values for Vary header.
+ {
+ await cache.put(
+ new Request("https://example.com/", {
+ headers: {
+ "Accept": "application/json",
+ },
+ }),
+ Response.json({ msg: "hello world" }, {
+ headers: {
+ "Content-Type": "application/json",
+ "Vary": "Accept",
+ },
+ }),
+ );
+ const res = await cache.match("https://example.com/");
+ assertEquals(res, undefined);
+ const res2 = await cache.match(
+ new Request("https://example.com/", {
+ headers: { "Accept": "text/html" },
+ }),
+ );
+ assertEquals(res2, undefined);
+
+ const res3 = await cache.match(
+ new Request("https://example.com/", {
+ headers: { "Accept": "application/json" },
+ }),
+ );
+ assertEquals(await res3?.json(), { msg: "hello world" });
+ }
+
+ assert(await caches.delete(cacheName));
+ assertFalse(await caches.has(cacheName));
+});
+
+Deno.test(function cacheIllegalConstructor() {
+ assertThrows(() => new Cache(), TypeError, "Illegal constructor");
+ // @ts-expect-error illegal constructor
+ assertThrows(() => new Cache("foo", "bar"), TypeError, "Illegal constructor");
+});
+
+Deno.test(async function cachePutReaderLock() {
+ const cacheName = "cache-v1";
+ const cache = await caches.open(cacheName);
+
+ const response = new Response("consumed");
+
+ const promise = cache.put(
+ new Request("https://example.com/"),
+ response,
+ );
+
+ await assertRejects(
+ async () => {
+ await response.arrayBuffer();
+ },
+ TypeError,
+ "Body already consumed.",
+ );
+
+ await promise;
+});
+
+Deno.test(async function cachePutResourceLeak() {
+ const cacheName = "cache-v1";
+ const cache = await caches.open(cacheName);
+
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.error(new Error("leak"));
+ },
+ });
+
+ await assertRejects(
+ async () => {
+ await cache.put(
+ new Request("https://example.com/leak"),
+ new Response(stream),
+ );
+ },
+ Error,
+ "leak",
+ );
+});
+
+Deno.test(async function cachePutFailedBody() {
+ const cacheName = "cache-v1";
+ const cache = await caches.open(cacheName);
+
+ const request = new Request("https://example.com/failed-body");
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.error(new Error("corrupt"));
+ },
+ });
+
+ await assertRejects(
+ async () => {
+ await cache.put(
+ request,
+ new Response(stream),
+ );
+ },
+ Error,
+ "corrupt",
+ );
+
+ const response = await cache.match(request);
+ // if it fails to read the body, the cache should be empty
+ assertEquals(response, undefined);
+});
+
+Deno.test(async function cachePutOverwrite() {
+ const cacheName = "cache-v1";
+ const cache = await caches.open(cacheName);
+
+ const request = new Request("https://example.com/overwrite");
+ const res1 = new Response("res1");
+ const res2 = new Response("res2");
+
+ await cache.put(request, res1);
+ const res = await cache.match(request);
+ assertEquals(await res?.text(), "res1");
+
+ await cache.put(request, res2);
+ const res_ = await cache.match(request);
+ assertEquals(await res_?.text(), "res2");
+});
+
+// Ensure that we can successfully put a response backed by a resource
+Deno.test(async function cachePutResource() {
+ const tempFile = Deno.makeTempFileSync({ prefix: "deno-", suffix: ".txt" });
+ Deno.writeTextFileSync(tempFile, "Contents".repeat(1024));
+
+ const file = Deno.openSync(tempFile);
+
+ const cacheName = "cache-v1";
+ const cache = await caches.open(cacheName);
+
+ const request = new Request("https://example.com/file");
+ await cache.put(request, new Response(file.readable));
+ const res = await cache.match(request);
+ assertEquals(await res?.text(), "Contents".repeat(1024));
+});
diff --git a/tests/unit/chmod_test.ts b/tests/unit/chmod_test.ts
new file mode 100644
index 000000000..df3771bbc
--- /dev/null
+++ b/tests/unit/chmod_test.ts
@@ -0,0 +1,190 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ function chmodSyncSuccess() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+ const filename = tempDir + "/test.txt";
+ Deno.writeFileSync(filename, data, { mode: 0o666 });
+
+ Deno.chmodSync(filename, 0o777);
+
+ const fileInfo = Deno.statSync(filename);
+ assert(fileInfo.mode);
+ assertEquals(fileInfo.mode & 0o777, 0o777);
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ function chmodSyncUrl() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+ const fileUrl = new URL(`file://${tempDir}/test.txt`);
+ Deno.writeFileSync(fileUrl, data, { mode: 0o666 });
+
+ Deno.chmodSync(fileUrl, 0o777);
+
+ const fileInfo = Deno.statSync(fileUrl);
+ assert(fileInfo.mode);
+ assertEquals(fileInfo.mode & 0o777, 0o777);
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+// Check symlink when not on windows
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ function chmodSyncSymlinkSuccess() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+
+ const filename = tempDir + "/test.txt";
+ Deno.writeFileSync(filename, data, { mode: 0o666 });
+ const symlinkName = tempDir + "/test_symlink.txt";
+ Deno.symlinkSync(filename, symlinkName);
+
+ let symlinkInfo = Deno.lstatSync(symlinkName);
+ assert(symlinkInfo.mode);
+ const symlinkMode = symlinkInfo.mode & 0o777; // platform dependent
+
+ Deno.chmodSync(symlinkName, 0o777);
+
+ // Change actual file mode, not symlink
+ const fileInfo = Deno.statSync(filename);
+ assert(fileInfo.mode);
+ assertEquals(fileInfo.mode & 0o777, 0o777);
+ symlinkInfo = Deno.lstatSync(symlinkName);
+ assert(symlinkInfo.mode);
+ assertEquals(symlinkInfo.mode & 0o777, symlinkMode);
+ },
+);
+
+Deno.test({ permissions: { write: true } }, function chmodSyncFailure() {
+ const filename = "/badfile.txt";
+ assertThrows(
+ () => {
+ Deno.chmodSync(filename, 0o777);
+ },
+ Deno.errors.NotFound,
+ `chmod '${filename}'`,
+ );
+});
+
+Deno.test({ permissions: { write: false } }, function chmodSyncPerm() {
+ assertThrows(() => {
+ Deno.chmodSync("/somefile.txt", 0o777);
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function chmodSuccess() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+ const filename = tempDir + "/test.txt";
+ Deno.writeFileSync(filename, data, { mode: 0o666 });
+
+ await Deno.chmod(filename, 0o777);
+
+ const fileInfo = Deno.statSync(filename);
+ assert(fileInfo.mode);
+ assertEquals(fileInfo.mode & 0o777, 0o777);
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function chmodUrl() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+ const fileUrl = new URL(`file://${tempDir}/test.txt`);
+ Deno.writeFileSync(fileUrl, data, { mode: 0o666 });
+
+ await Deno.chmod(fileUrl, 0o777);
+
+ const fileInfo = Deno.statSync(fileUrl);
+ assert(fileInfo.mode);
+ assertEquals(fileInfo.mode & 0o777, 0o777);
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+// Check symlink when not on windows
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function chmodSymlinkSuccess() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+
+ const filename = tempDir + "/test.txt";
+ Deno.writeFileSync(filename, data, { mode: 0o666 });
+ const symlinkName = tempDir + "/test_symlink.txt";
+ Deno.symlinkSync(filename, symlinkName);
+
+ let symlinkInfo = Deno.lstatSync(symlinkName);
+ assert(symlinkInfo.mode);
+ const symlinkMode = symlinkInfo.mode & 0o777; // platform dependent
+
+ await Deno.chmod(symlinkName, 0o777);
+
+ // Just change actual file mode, not symlink
+ const fileInfo = Deno.statSync(filename);
+ assert(fileInfo.mode);
+ assertEquals(fileInfo.mode & 0o777, 0o777);
+ symlinkInfo = Deno.lstatSync(symlinkName);
+ assert(symlinkInfo.mode);
+ assertEquals(symlinkInfo.mode & 0o777, symlinkMode);
+ },
+);
+
+Deno.test({ permissions: { write: true } }, async function chmodFailure() {
+ const filename = "/badfile.txt";
+ await assertRejects(
+ async () => {
+ await Deno.chmod(filename, 0o777);
+ },
+ Deno.errors.NotFound,
+ `chmod '${filename}'`,
+ );
+});
+
+Deno.test({ permissions: { write: false } }, async function chmodPerm() {
+ await assertRejects(async () => {
+ await Deno.chmod("/somefile.txt", 0o777);
+ }, Deno.errors.PermissionDenied);
+});
diff --git a/tests/unit/chown_test.ts b/tests/unit/chown_test.ts
new file mode 100644
index 000000000..033d4592d
--- /dev/null
+++ b/tests/unit/chown_test.ts
@@ -0,0 +1,190 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals, assertRejects, assertThrows } from "./test_util.ts";
+
+// chown on Windows is noop for now, so ignore its testing on Windows
+
+async function getUidAndGid(): Promise<{ uid: number; gid: number }> {
+ // get the user ID and group ID of the current process
+ const uidProc = await new Deno.Command("id", {
+ args: ["-u"],
+ }).output();
+ const gidProc = await new Deno.Command("id", {
+ args: ["-g"],
+ }).output();
+
+ assertEquals(uidProc.code, 0);
+ assertEquals(gidProc.code, 0);
+ const uid = parseInt(new TextDecoder("utf-8").decode(uidProc.stdout));
+ const gid = parseInt(new TextDecoder("utf-8").decode(gidProc.stdout));
+
+ return { uid, gid };
+}
+
+Deno.test(
+ { ignore: Deno.build.os == "windows", permissions: { write: false } },
+ async function chownNoWritePermission() {
+ const filePath = "chown_test_file.txt";
+ await assertRejects(async () => {
+ await Deno.chown(filePath, 1000, 1000);
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, write: true },
+ ignore: Deno.build.os == "windows",
+ },
+ async function chownSyncFileNotExist() {
+ const { uid, gid } = await getUidAndGid();
+ const filePath = Deno.makeTempDirSync() + "/chown_test_file.txt";
+
+ assertThrows(
+ () => {
+ Deno.chownSync(filePath, uid, gid);
+ },
+ Deno.errors.NotFound,
+ `chown '${filePath}'`,
+ );
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, write: true },
+ ignore: Deno.build.os == "windows",
+ },
+ async function chownFileNotExist() {
+ const { uid, gid } = await getUidAndGid();
+ const filePath = (await Deno.makeTempDir()) + "/chown_test_file.txt";
+
+ await assertRejects(
+ async () => {
+ await Deno.chown(filePath, uid, gid);
+ },
+ Deno.errors.NotFound,
+ `chown '${filePath}'`,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { write: true }, ignore: Deno.build.os == "windows" },
+ function chownSyncPermissionDenied() {
+ const dirPath = Deno.makeTempDirSync();
+ const filePath = dirPath + "/chown_test_file.txt";
+ Deno.writeTextFileSync(filePath, "Hello");
+
+ assertThrows(() => {
+ // try changing the file's owner to root
+ Deno.chownSync(filePath, 0, 0);
+ }, Deno.errors.PermissionDenied);
+ Deno.removeSync(dirPath, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { write: true }, ignore: Deno.build.os == "windows" },
+ async function chownPermissionDenied() {
+ const dirPath = await Deno.makeTempDir();
+ const filePath = dirPath + "/chown_test_file.txt";
+ await Deno.writeTextFile(filePath, "Hello");
+
+ await assertRejects(async () => {
+ // try changing the file's owner to root
+ await Deno.chown(filePath, 0, 0);
+ }, Deno.errors.PermissionDenied);
+ await Deno.remove(dirPath, { recursive: true });
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, write: true },
+ ignore: Deno.build.os == "windows",
+ },
+ async function chownSyncSucceed() {
+ // TODO(bartlomieju): when a file's owner is actually being changed,
+ // chown only succeeds if run under privileged user (root)
+ // The test script has no such privilege, so need to find a better way to test this case
+ const { uid, gid } = await getUidAndGid();
+
+ const dirPath = Deno.makeTempDirSync();
+ const filePath = dirPath + "/chown_test_file.txt";
+ Deno.writeTextFileSync(filePath, "Hello");
+
+ // the test script creates this file with the same uid and gid,
+ // here chown is a noop so it succeeds under non-privileged user
+ Deno.chownSync(filePath, uid, gid);
+
+ Deno.removeSync(dirPath, { recursive: true });
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, write: true },
+ ignore: Deno.build.os == "windows",
+ },
+ async function chownSyncWithUrl() {
+ const { uid, gid } = await getUidAndGid();
+ const dirPath = Deno.makeTempDirSync();
+ const fileUrl = new URL(`file://${dirPath}/chown_test_file.txt`);
+ Deno.writeTextFileSync(fileUrl, "Hello");
+ Deno.chownSync(fileUrl, uid, gid);
+ Deno.removeSync(dirPath, { recursive: true });
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, write: true },
+ ignore: Deno.build.os == "windows",
+ },
+ async function chownSucceed() {
+ const { uid, gid } = await getUidAndGid();
+ const dirPath = await Deno.makeTempDir();
+ const filePath = dirPath + "/chown_test_file.txt";
+ await Deno.writeTextFile(filePath, "Hello");
+ await Deno.chown(filePath, uid, gid);
+ Deno.removeSync(dirPath, { recursive: true });
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, write: true },
+ ignore: Deno.build.os == "windows",
+ },
+ async function chownUidOnly() {
+ const { uid } = await getUidAndGid();
+ const dirPath = await Deno.makeTempDir();
+ const filePath = dirPath + "/chown_test_file.txt";
+ await Deno.writeTextFile(filePath, "Foo");
+ await Deno.chown(filePath, uid, null);
+ Deno.removeSync(dirPath, { recursive: true });
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, write: true },
+ ignore: Deno.build.os == "windows",
+ },
+ async function chownWithUrl() {
+ // TODO(bartlomieju): same as chownSyncSucceed
+ const { uid, gid } = await getUidAndGid();
+
+ const enc = new TextEncoder();
+ const dirPath = await Deno.makeTempDir();
+ const fileUrl = new URL(`file://${dirPath}/chown_test_file.txt`);
+ const fileData = enc.encode("Hello");
+ await Deno.writeFile(fileUrl, fileData);
+
+ // the test script creates this file with the same uid and gid,
+ // here chown is a noop so it succeeds under non-privileged user
+ await Deno.chown(fileUrl, uid, gid);
+
+ Deno.removeSync(dirPath, { recursive: true });
+ },
+);
diff --git a/tests/unit/command_test.ts b/tests/unit/command_test.ts
new file mode 100644
index 000000000..cbb1c4921
--- /dev/null
+++ b/tests/unit/command_test.ts
@@ -0,0 +1,967 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertStringIncludes,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test(
+ { permissions: { write: true, run: true, read: true } },
+ async function commandWithCwdIsAsync() {
+ const enc = new TextEncoder();
+ const cwd = await Deno.makeTempDir({ prefix: "deno_command_test" });
+
+ const exitCodeFile = "deno_was_here";
+ const programFile = "poll_exit.ts";
+ const program = `
+async function tryExit() {
+ try {
+ const code = parseInt(await Deno.readTextFile("${exitCodeFile}"));
+ Deno.exit(code);
+ } catch {
+ // Retry if we got here before deno wrote the file.
+ setTimeout(tryExit, 0.01);
+ }
+}
+
+tryExit();
+`;
+
+ Deno.writeFileSync(`${cwd}/${programFile}`, enc.encode(program));
+
+ const command = new Deno.Command(Deno.execPath(), {
+ cwd,
+ args: ["run", "--allow-read", programFile],
+ stdout: "inherit",
+ stderr: "inherit",
+ });
+ const child = command.spawn();
+
+ // Write the expected exit code *after* starting deno.
+ // This is how we verify that `Child` is actually asynchronous.
+ const code = 84;
+ Deno.writeFileSync(`${cwd}/${exitCodeFile}`, enc.encode(`${code}`));
+
+ const status = await child.status;
+ await Deno.remove(cwd, { recursive: true });
+ assertEquals(status.success, false);
+ assertEquals(status.code, code);
+ assertEquals(status.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandStdinPiped() {
+ const command = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')",
+ ],
+ stdin: "piped",
+ stdout: "null",
+ stderr: "null",
+ });
+ const child = command.spawn();
+
+ assertThrows(() => child.stdout, TypeError, "stdout is not piped");
+ assertThrows(() => child.stderr, TypeError, "stderr is not piped");
+
+ const msg = new TextEncoder().encode("hello");
+ const writer = child.stdin.getWriter();
+ await writer.write(msg);
+ writer.releaseLock();
+
+ await child.stdin.close();
+ const status = await child.status;
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandStdinPiped() {
+ const command = new Deno.Command(Deno.execPath(), {
+ args: ["info"],
+ stdout: "null",
+ stderr: "null",
+ });
+ const child = command.spawn();
+
+ assertThrows(() => child.stdin, TypeError, "stdin is not piped");
+ assertThrows(() => child.stdout, TypeError, "stdout is not piped");
+ assertThrows(() => child.stderr, TypeError, "stderr is not piped");
+
+ await child.status;
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandStdoutPiped() {
+ const command = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stdout.write(new TextEncoder().encode('hello'))",
+ ],
+ stderr: "null",
+ stdout: "piped",
+ });
+ const child = command.spawn();
+
+ assertThrows(() => child.stdin, TypeError, "stdin is not piped");
+ assertThrows(() => child.stderr, TypeError, "stderr is not piped");
+
+ const readable = child.stdout.pipeThrough(new TextDecoderStream());
+ const reader = readable.getReader();
+ const res = await reader.read();
+ assert(!res.done);
+ assertEquals(res.value, "hello");
+
+ const resEnd = await reader.read();
+ assert(resEnd.done);
+ assertEquals(resEnd.value, undefined);
+ reader.releaseLock();
+
+ const status = await child.status;
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandStderrPiped() {
+ const command = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stderr.write(new TextEncoder().encode('hello'))",
+ ],
+ stdout: "null",
+ stderr: "piped",
+ });
+ const child = command.spawn();
+
+ assertThrows(() => child.stdin, TypeError, "stdin is not piped");
+ assertThrows(() => child.stdout, TypeError, "stdout is not piped");
+
+ const readable = child.stderr.pipeThrough(new TextDecoderStream());
+ const reader = readable.getReader();
+ const res = await reader.read();
+ assert(!res.done);
+ assertEquals(res.value, "hello");
+
+ const resEnd = await reader.read();
+ assert(resEnd.done);
+ assertEquals(resEnd.value, undefined);
+ reader.releaseLock();
+
+ const status = await child.status;
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, write: true, read: true } },
+ async function commandRedirectStdoutStderr() {
+ const tempDir = await Deno.makeTempDir();
+ const fileName = tempDir + "/redirected_stdio.txt";
+ const file = await Deno.open(fileName, {
+ create: true,
+ write: true,
+ });
+
+ const command = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "Deno.stderr.write(new TextEncoder().encode('error\\n')); Deno.stdout.write(new TextEncoder().encode('output\\n'));",
+ ],
+ stdout: "piped",
+ stderr: "piped",
+ });
+ const child = command.spawn();
+ await child.stdout.pipeTo(file.writable, {
+ preventClose: true,
+ });
+ await child.stderr.pipeTo(file.writable);
+ await child.status;
+
+ const fileContents = await Deno.readFile(fileName);
+ const decoder = new TextDecoder();
+ const text = decoder.decode(fileContents);
+
+ assertStringIncludes(text, "error");
+ assertStringIncludes(text, "output");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, write: true, read: true } },
+ async function commandRedirectStdin() {
+ const tempDir = await Deno.makeTempDir();
+ const fileName = tempDir + "/redirected_stdio.txt";
+ await Deno.writeTextFile(fileName, "hello");
+ const file = await Deno.open(fileName);
+
+ const command = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')",
+ ],
+ stdin: "piped",
+ stdout: "null",
+ stderr: "null",
+ });
+ const child = command.spawn();
+ await file.readable.pipeTo(child.stdin, {
+ preventClose: true,
+ });
+
+ await child.stdin.close();
+ const status = await child.status;
+ assertEquals(status.code, 0);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandKillSuccess() {
+ const command = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "setTimeout(() => {}, 10000)"],
+ stdout: "null",
+ stderr: "null",
+ });
+ const child = command.spawn();
+
+ child.kill("SIGKILL");
+ const status = await child.status;
+
+ assertEquals(status.success, false);
+ if (Deno.build.os === "windows") {
+ assertEquals(status.code, 1);
+ assertEquals(status.signal, null);
+ } else {
+ assertEquals(status.code, 137);
+ assertEquals(status.signal, "SIGKILL");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ // deno lint bug, see https://github.com/denoland/deno_lint/issues/1206
+ // deno-lint-ignore require-await
+ async function childProcessExplicitResourceManagement() {
+ let dead = undefined;
+ {
+ const command = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "setTimeout(() => {}, 10000)"],
+ stdout: "null",
+ stderr: "null",
+ });
+ await using child = command.spawn();
+ child.status.then(({ signal }) => {
+ dead = signal;
+ });
+ }
+
+ if (Deno.build.os == "windows") {
+ assertEquals(dead, null);
+ } else {
+ assertEquals(dead, "SIGTERM");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function childProcessExplicitResourceManagementManualClose() {
+ const command = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "setTimeout(() => {}, 10000)"],
+ stdout: "null",
+ stderr: "null",
+ });
+ await using child = command.spawn();
+ child.kill("SIGTERM");
+ await child.status;
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandKillFailed() {
+ const command = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "setTimeout(() => {}, 5000)"],
+ stdout: "null",
+ stderr: "null",
+ });
+ const child = command.spawn();
+
+ assertThrows(() => {
+ // @ts-expect-error testing runtime error of bad signal
+ child.kill("foobar");
+ }, TypeError);
+
+ await child.status;
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandKillOptional() {
+ const command = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "setTimeout(() => {}, 10000)"],
+ stdout: "null",
+ stderr: "null",
+ });
+ const child = command.spawn();
+
+ child.kill();
+ const status = await child.status;
+
+ assertEquals(status.success, false);
+ if (Deno.build.os === "windows") {
+ assertEquals(status.code, 1);
+ assertEquals(status.signal, null);
+ } else {
+ assertEquals(status.code, 143);
+ assertEquals(status.signal, "SIGTERM");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandAbort() {
+ const ac = new AbortController();
+ const command = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "setTimeout(console.log, 1e8)",
+ ],
+ signal: ac.signal,
+ stdout: "null",
+ stderr: "null",
+ });
+ const child = command.spawn();
+ queueMicrotask(() => ac.abort());
+ const status = await child.status;
+ assertEquals(status.success, false);
+ if (Deno.build.os === "windows") {
+ assertEquals(status.code, 1);
+ assertEquals(status.signal, null);
+ } else {
+ assertEquals(status.success, false);
+ assertEquals(status.code, 143);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: false } },
+ async function commandPermissions() {
+ await assertRejects(async () => {
+ await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log('hello world')"],
+ }).output();
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: false } },
+ function commandSyncPermissions() {
+ assertThrows(() => {
+ new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log('hello world')"],
+ }).outputSync();
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandSuccess() {
+ const output = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log('hello world')"],
+ }).output();
+
+ assertEquals(output.success, true);
+ assertEquals(output.code, 0);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncSuccess() {
+ const output = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log('hello world')"],
+ }).outputSync();
+
+ assertEquals(output.success, true);
+ assertEquals(output.code, 0);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandUrl() {
+ const output = await new Deno.Command(
+ new URL(`file:///${Deno.execPath()}`),
+ {
+ args: ["eval", "console.log('hello world')"],
+ },
+ ).output();
+
+ assertEquals(new TextDecoder().decode(output.stdout), "hello world\n");
+
+ assertEquals(output.success, true);
+ assertEquals(output.code, 0);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncUrl() {
+ const output = new Deno.Command(
+ new URL(`file:///${Deno.execPath()}`),
+ {
+ args: ["eval", "console.log('hello world')"],
+ },
+ ).outputSync();
+
+ assertEquals(new TextDecoder().decode(output.stdout), "hello world\n");
+
+ assertEquals(output.success, true);
+ assertEquals(output.code, 0);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test({ permissions: { run: true } }, function commandNotFound() {
+ assertThrows(
+ () => new Deno.Command("this file hopefully doesn't exist").output(),
+ Deno.errors.NotFound,
+ );
+});
+
+Deno.test({ permissions: { run: true } }, function commandSyncNotFound() {
+ assertThrows(
+ () => new Deno.Command("this file hopefully doesn't exist").outputSync(),
+ Deno.errors.NotFound,
+ );
+});
+
+Deno.test({ permissions: { run: true, read: true } }, function cwdNotFound() {
+ assertThrows(
+ () =>
+ new Deno.Command(Deno.execPath(), {
+ cwd: Deno.cwd() + "/non-existent-directory",
+ }).output(),
+ Deno.errors.NotFound,
+ "No such cwd",
+ );
+});
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function cwdNotDirectory() {
+ assertThrows(
+ () =>
+ new Deno.Command(Deno.execPath(), {
+ cwd: Deno.execPath(),
+ }).output(),
+ Deno.errors.NotFound,
+ "cwd is not a directory",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandFailedWithCode() {
+ const output = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "Deno.exit(41 + 1)"],
+ }).output();
+ assertEquals(output.success, false);
+ assertEquals(output.code, 42);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncFailedWithCode() {
+ const output = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "Deno.exit(41 + 1)"],
+ }).outputSync();
+ assertEquals(output.success, false);
+ assertEquals(output.code, 42);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ },
+ async function commandFailedWithSignal() {
+ const output = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "--unstable", "Deno.kill(Deno.pid, 'SIGKILL')"],
+ }).output();
+ assertEquals(output.success, false);
+ if (Deno.build.os === "windows") {
+ assertEquals(output.code, 1);
+ assertEquals(output.signal, null);
+ } else {
+ assertEquals(output.code, 128 + 9);
+ assertEquals(output.signal, "SIGKILL");
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ },
+ function commandSyncFailedWithSignal() {
+ const output = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "--unstable", "Deno.kill(Deno.pid, 'SIGKILL')"],
+ }).outputSync();
+ assertEquals(output.success, false);
+ if (Deno.build.os === "windows") {
+ assertEquals(output.code, 1);
+ assertEquals(output.signal, null);
+ } else {
+ assertEquals(output.code, 128 + 9);
+ assertEquals(output.signal, "SIGKILL");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandOutput() {
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stdout.write(new TextEncoder().encode('hello'))",
+ ],
+ }).output();
+
+ const s = new TextDecoder().decode(stdout);
+ assertEquals(s, "hello");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncOutput() {
+ const { stdout } = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stdout.write(new TextEncoder().encode('hello'))",
+ ],
+ }).outputSync();
+
+ const s = new TextDecoder().decode(stdout);
+ assertEquals(s, "hello");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandStderrOutput() {
+ const { stderr } = await new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stderr.write(new TextEncoder().encode('error'))",
+ ],
+ }).output();
+
+ const s = new TextDecoder().decode(stderr);
+ assertEquals(s, "error");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncStderrOutput() {
+ const { stderr } = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stderr.write(new TextEncoder().encode('error'))",
+ ],
+ }).outputSync();
+
+ const s = new TextDecoder().decode(stderr);
+ assertEquals(s, "error");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandEnv() {
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "Deno.stdout.write(new TextEncoder().encode(Deno.env.get('FOO') + Deno.env.get('BAR')))",
+ ],
+ env: {
+ FOO: "0123",
+ BAR: "4567",
+ },
+ }).output();
+ const s = new TextDecoder().decode(stdout);
+ assertEquals(s, "01234567");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncEnv() {
+ const { stdout } = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "Deno.stdout.write(new TextEncoder().encode(Deno.env.get('FOO') + Deno.env.get('BAR')))",
+ ],
+ env: {
+ FOO: "0123",
+ BAR: "4567",
+ },
+ }).outputSync();
+ const s = new TextDecoder().decode(stdout);
+ assertEquals(s, "01234567");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true, env: true } },
+ async function commandClearEnv() {
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "-p",
+ "JSON.stringify(Deno.env.toObject())",
+ ],
+ clearEnv: true,
+ env: {
+ FOO: "23147",
+ },
+ }).output();
+
+ const obj = JSON.parse(new TextDecoder().decode(stdout));
+
+ // can't check for object equality because the OS may set additional env
+ // vars for processes, so we check if PATH isn't present as that is a common
+ // env var across OS's and isn't set for processes.
+ assertEquals(obj.FOO, "23147");
+ assert(!("PATH" in obj));
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true, env: true } },
+ function commandSyncClearEnv() {
+ const { stdout } = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "-p",
+ "JSON.stringify(Deno.env.toObject())",
+ ],
+ clearEnv: true,
+ env: {
+ FOO: "23147",
+ },
+ }).outputSync();
+
+ const obj = JSON.parse(new TextDecoder().decode(stdout));
+
+ // can't check for object equality because the OS may set additional env
+ // vars for processes, so we check if PATH isn't present as that is a common
+ // env var across OS's and isn't set for processes.
+ assertEquals(obj.FOO, "23147");
+ assert(!("PATH" in obj));
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ ignore: Deno.build.os === "windows",
+ },
+ async function commandUid() {
+ const { stdout } = await new Deno.Command("id", {
+ args: ["-u"],
+ }).output();
+
+ const currentUid = new TextDecoder().decode(stdout);
+
+ if (currentUid !== "0") {
+ await assertRejects(async () => {
+ await new Deno.Command("echo", {
+ args: ["fhqwhgads"],
+ uid: 0,
+ }).output();
+ }, Deno.errors.PermissionDenied);
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ ignore: Deno.build.os === "windows",
+ },
+ function commandSyncUid() {
+ const { stdout } = new Deno.Command("id", {
+ args: ["-u"],
+ }).outputSync();
+
+ const currentUid = new TextDecoder().decode(stdout);
+
+ if (currentUid !== "0") {
+ assertThrows(() => {
+ new Deno.Command("echo", {
+ args: ["fhqwhgads"],
+ uid: 0,
+ }).outputSync();
+ }, Deno.errors.PermissionDenied);
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ ignore: Deno.build.os === "windows",
+ },
+ async function commandGid() {
+ const { stdout } = await new Deno.Command("id", {
+ args: ["-g"],
+ }).output();
+
+ const currentGid = new TextDecoder().decode(stdout);
+
+ if (currentGid !== "0") {
+ await assertRejects(async () => {
+ await new Deno.Command("echo", {
+ args: ["fhqwhgads"],
+ gid: 0,
+ }).output();
+ }, Deno.errors.PermissionDenied);
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ ignore: Deno.build.os === "windows",
+ },
+ function commandSyncGid() {
+ const { stdout } = new Deno.Command("id", {
+ args: ["-g"],
+ }).outputSync();
+
+ const currentGid = new TextDecoder().decode(stdout);
+
+ if (currentGid !== "0") {
+ assertThrows(() => {
+ new Deno.Command("echo", {
+ args: ["fhqwhgads"],
+ gid: 0,
+ }).outputSync();
+ }, Deno.errors.PermissionDenied);
+ }
+ },
+);
+
+Deno.test(function commandStdinPipedFails() {
+ assertThrows(
+ () =>
+ new Deno.Command("id", {
+ stdin: "piped",
+ }).output(),
+ TypeError,
+ "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead",
+ );
+});
+
+Deno.test(function spawnSyncStdinPipedFails() {
+ assertThrows(
+ () =>
+ new Deno.Command("id", {
+ stdin: "piped",
+ }).outputSync(),
+ TypeError,
+ "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead",
+ );
+});
+
+Deno.test(
+ // FIXME(bartlomieju): this test is very flaky on CI, fix it
+ {
+ ignore: true,
+ permissions: { write: true, run: true, read: true },
+ },
+ async function commandChildUnref() {
+ const enc = new TextEncoder();
+ const cwd = await Deno.makeTempDir({ prefix: "deno_command_test" });
+
+ const programFile = "unref.ts";
+ const program = `
+const command = await new Deno.Command(Deno.execPath(), {
+ cwd: Deno.args[0],
+ stdout: "piped",
+ args: ["run", "-A", "--unstable", Deno.args[1]],
+});
+const child = command.spawn();
+const readable = child.stdout.pipeThrough(new TextDecoderStream());
+const reader = readable.getReader();
+// set up an interval that will end after reading a few messages from stdout,
+// to verify that stdio streams are properly unrefed
+let count = 0;
+let interval;
+interval = setInterval(async () => {
+ count += 1;
+ if (count > 10) {
+ clearInterval(interval);
+ console.log("cleared interval");
+ }
+ const res = await reader.read();
+ if (res.done) {
+ throw new Error("stream shouldn't be done");
+ }
+ if (res.value.trim() != "hello from interval") {
+ throw new Error("invalid message received");
+ }
+}, 120);
+console.log("spawned pid", child.pid);
+child.unref();
+`;
+
+ const childProgramFile = "unref_child.ts";
+ const childProgram = `
+setInterval(() => {
+ console.log("hello from interval");
+}, 100);
+`;
+ Deno.writeFileSync(`${cwd}/${programFile}`, enc.encode(program));
+ Deno.writeFileSync(`${cwd}/${childProgramFile}`, enc.encode(childProgram));
+ // In this subprocess we are spawning another subprocess which has
+ // an infinite interval set. Following call would never resolve unless
+ // child process gets unrefed.
+ const { success, stdout, stderr } = await new Deno.Command(
+ Deno.execPath(),
+ {
+ cwd,
+ args: ["run", "-A", "--unstable", programFile, cwd, childProgramFile],
+ },
+ ).output();
+
+ assert(success);
+ const stdoutText = new TextDecoder().decode(stdout);
+ const stderrText = new TextDecoder().decode(stderr);
+ assert(stderrText.length == 0);
+ const [line1, line2] = stdoutText.split("\n");
+ const pidStr = line1.split(" ").at(-1);
+ assert(pidStr);
+ assertEquals(line2, "cleared interval");
+ const pid = Number.parseInt(pidStr, 10);
+ await Deno.remove(cwd, { recursive: true });
+ // Child process should have been killed when parent process exits.
+ assertThrows(() => {
+ Deno.kill(pid, "SIGTERM");
+ }, Deno.errors.NotFound);
+ },
+);
+
+Deno.test(
+ { ignore: Deno.build.os !== "windows" },
+ async function commandWindowsRawArguments() {
+ let { success, stdout } = await new Deno.Command("cmd", {
+ args: ["/d", "/s", "/c", '"deno ^"--version^""'],
+ windowsRawArguments: true,
+ }).output();
+ assert(success);
+ let stdoutText = new TextDecoder().decode(stdout);
+ assertStringIncludes(stdoutText, "deno");
+ assertStringIncludes(stdoutText, "v8");
+ assertStringIncludes(stdoutText, "typescript");
+
+ ({ success, stdout } = new Deno.Command("cmd", {
+ args: ["/d", "/s", "/c", '"deno ^"--version^""'],
+ windowsRawArguments: true,
+ }).outputSync());
+ assert(success);
+ stdoutText = new TextDecoder().decode(stdout);
+ assertStringIncludes(stdoutText, "deno");
+ assertStringIncludes(stdoutText, "v8");
+ assertStringIncludes(stdoutText, "typescript");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: true } },
+ async function commandWithPrototypePollution() {
+ const originalThen = Promise.prototype.then;
+ const originalSymbolIterator = Array.prototype[Symbol.iterator];
+ try {
+ Promise.prototype.then = Array.prototype[Symbol.iterator] = () => {
+ throw new Error();
+ };
+ await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log('hello world')"],
+ }).output();
+ } finally {
+ Promise.prototype.then = originalThen;
+ Array.prototype[Symbol.iterator] = originalSymbolIterator;
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandKillAfterStatus() {
+ const command = new Deno.Command(Deno.execPath(), {
+ args: ["help"],
+ stdout: "null",
+ stderr: "null",
+ });
+ const child = command.spawn();
+ await child.status;
+ assertThrows(
+ () => child.kill(),
+ TypeError,
+ "Child process has already terminated.",
+ );
+ },
+);
+
+Deno.test(
+ "process that fails to spawn, prints its name in error",
+ async () => {
+ assertThrows(
+ () => new Deno.Command("doesntexist").outputSync(),
+ Error,
+ "Failed to spawn 'doesntexist'",
+ );
+ await assertRejects(
+ async () => await new Deno.Command("doesntexist").output(),
+ Error,
+ "Failed to spawn 'doesntexist'",
+ );
+ },
+);
diff --git a/tests/unit/console_test.ts b/tests/unit/console_test.ts
new file mode 100644
index 000000000..2f24b2c4e
--- /dev/null
+++ b/tests/unit/console_test.ts
@@ -0,0 +1,2411 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+// TODO(ry) The unit test functions in this module are too coarse. They should
+// be broken up into smaller bits.
+
+// TODO(ry) These tests currently strip all the ANSI colors out. We don't have a
+// good way to control whether we produce color output or not since
+// std/fmt/colors auto determines whether to put colors in or not. We need
+// better infrastructure here so we can properly test the colors.
+
+import {
+ assert,
+ assertEquals,
+ assertStringIncludes,
+ assertThrows,
+} from "./test_util.ts";
+import { stripColor } from "@test_util/std/fmt/colors.ts";
+
+const customInspect = Symbol.for("Deno.customInspect");
+const {
+ Console,
+ cssToAnsi: cssToAnsi_,
+ inspectArgs,
+ parseCss: parseCss_,
+ parseCssColor: parseCssColor_,
+ // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
+} = Deno[Deno.internal];
+
+function stringify(...args: unknown[]): string {
+ return stripColor(inspectArgs(args).replace(/\n$/, ""));
+}
+
+interface Css {
+ backgroundColor: [number, number, number] | string | null;
+ color: [number, number, number] | string | null;
+ fontWeight: string | null;
+ fontStyle: string | null;
+ textDecorationColor: [number, number, number] | null;
+ textDecorationLine: string[];
+}
+
+const DEFAULT_CSS: Css = {
+ backgroundColor: null,
+ color: null,
+ fontWeight: null,
+ fontStyle: null,
+ textDecorationColor: null,
+ textDecorationLine: [],
+};
+
+function parseCss(cssString: string): Css {
+ return parseCss_(cssString);
+}
+
+function parseCssColor(colorString: string): [number, number, number] | null {
+ return parseCssColor_(colorString);
+}
+
+/** ANSI-fy the CSS, replace "\x1b" with "_". */
+function cssToAnsiEsc(css: Css, prevCss: Css | null = null): string {
+ return cssToAnsi_(css, prevCss).replaceAll("\x1b", "_");
+}
+
+// test cases from web-platform-tests
+// via https://github.com/web-platform-tests/wpt/blob/master/console/console-is-a-namespace.any.js
+Deno.test(function consoleShouldBeANamespace() {
+ const prototype1 = Object.getPrototypeOf(console);
+ const prototype2 = Object.getPrototypeOf(prototype1);
+
+ assertEquals(Object.getOwnPropertyNames(prototype1).length, 0);
+ assertEquals(prototype2, Object.prototype);
+});
+
+Deno.test(function consoleHasRightInstance() {
+ assert(console instanceof Console);
+ assertEquals({} instanceof Console, false);
+});
+
+Deno.test(function consoleTestAssertShouldNotThrowError() {
+ mockConsole((console) => {
+ console.assert(true);
+ let hasThrown = undefined;
+ try {
+ console.assert(false);
+ hasThrown = false;
+ } catch {
+ hasThrown = true;
+ }
+ assertEquals(hasThrown, false);
+ });
+});
+
+Deno.test(function consoleTestStringifyComplexObjects() {
+ assertEquals(stringify("foo"), "foo");
+ assertEquals(stringify(["foo", "bar"]), `[ "foo", "bar" ]`);
+ assertEquals(stringify({ foo: "bar" }), `{ foo: "bar" }`);
+});
+
+Deno.test(
+ function consoleTestStringifyComplexObjectsWithEscapedSequences() {
+ assertEquals(
+ stringify(
+ ["foo\b", "foo\f", "foo\n", "foo\r", "foo\t", "foo\v", "foo\0"],
+ ),
+ `[
+ "foo\\b", "foo\\f",
+ "foo\\n", "foo\\r",
+ "foo\\t", "foo\\v",
+ "foo\\x00"
+]`,
+ );
+ assertEquals(
+ stringify(
+ [
+ Symbol(),
+ Symbol(""),
+ Symbol("foo\b"),
+ Symbol("foo\f"),
+ Symbol("foo\n"),
+ Symbol("foo\r"),
+ Symbol("foo\t"),
+ Symbol("foo\v"),
+ Symbol("foo\0"),
+ ],
+ ),
+ `[
+ Symbol(),
+ Symbol(""),
+ Symbol("foo\\b"),
+ Symbol("foo\\f"),
+ Symbol("foo\\n"),
+ Symbol("foo\\r"),
+ Symbol("foo\\t"),
+ Symbol("foo\\v"),
+ Symbol("foo\\x00")
+]`,
+ );
+ assertEquals(
+ stringify(
+ { "foo\b": "bar\n", "bar\r": "baz\t", "qux\0": "qux\0" },
+ ),
+ `{ "foo\\b": "bar\\n", "bar\\r": "baz\\t", "qux\\x00": "qux\\x00" }`,
+ );
+ assertEquals(
+ stringify(
+ {
+ [Symbol("foo\b")]: `Symbol("foo\n")`,
+ [Symbol("bar\n")]: `Symbol("bar\n")`,
+ [Symbol("bar\r")]: `Symbol("bar\r")`,
+ [Symbol("baz\t")]: `Symbol("baz\t")`,
+ [Symbol("qux\0")]: `Symbol("qux\0")`,
+ },
+ ),
+ `{
+ [Symbol("foo\\b")]: 'Symbol("foo\\n")',
+ [Symbol("bar\\n")]: 'Symbol("bar\\n")',
+ [Symbol("bar\\r")]: 'Symbol("bar\\r")',
+ [Symbol("baz\\t")]: 'Symbol("baz\\t")',
+ [Symbol("qux\\x00")]: 'Symbol("qux\\x00")'
+}`,
+ );
+ assertEquals(
+ stringify(new Set(["foo\n", "foo\r", "foo\0"])),
+ `Set(3) { "foo\\n", "foo\\r", "foo\\x00" }`,
+ );
+ },
+);
+
+Deno.test(function consoleTestStringifyQuotes() {
+ assertEquals(stringify(["\\"]), `[ "\\\\" ]`);
+ assertEquals(stringify(['\\,"']), `[ '\\\\,"' ]`);
+ assertEquals(stringify([`\\,",'`]), `[ \`\\\\,",'\` ]`);
+ assertEquals(stringify(["\\,\",',`"]), `[ "\\\\,\\",',\`" ]`);
+});
+
+Deno.test(function consoleTestStringifyLongStrings() {
+ const veryLongString = "a".repeat(200);
+ // If we stringify an object containing the long string, it gets abbreviated.
+ let actual = stringify({ veryLongString });
+ assert(actual.includes("..."));
+ assert(actual.length < 200);
+ // However if we stringify the string itself, we get it exactly.
+ actual = stringify(veryLongString);
+ assertEquals(actual, veryLongString);
+});
+
+Deno.test(function consoleTestStringifyCircular() {
+ class Base {
+ a = 1;
+ m1() {}
+ }
+
+ class Extended extends Base {
+ b = 2;
+ m2() {}
+ }
+
+ // deno-lint-ignore no-explicit-any
+ const nestedObj: any = {
+ num: 1,
+ bool: true,
+ str: "a",
+ method() {},
+ async asyncMethod() {},
+ *generatorMethod() {},
+ un: undefined,
+ nu: null,
+ arrowFunc: () => {},
+ extendedClass: new Extended(),
+ nFunc: new Function(),
+ extendedCstr: Extended,
+ };
+
+ const circularObj = {
+ num: 2,
+ bool: false,
+ str: "b",
+ method() {},
+ un: undefined,
+ nu: null,
+ nested: nestedObj,
+ emptyObj: {},
+ arr: [1, "s", false, null, nestedObj],
+ baseClass: new Base(),
+ };
+
+ nestedObj.o = circularObj;
+ const nestedObjExpected = `<ref *1> {
+ num: 1,
+ bool: true,
+ str: "a",
+ method: [Function: method],
+ asyncMethod: [AsyncFunction: asyncMethod],
+ generatorMethod: [GeneratorFunction: generatorMethod],
+ un: undefined,
+ nu: null,
+ arrowFunc: [Function: arrowFunc],
+ extendedClass: Extended { a: 1, b: 2 },
+ nFunc: [Function: anonymous],
+ extendedCstr: [class Extended extends Base],
+ o: {
+ num: 2,
+ bool: false,
+ str: "b",
+ method: [Function: method],
+ un: undefined,
+ nu: null,
+ nested: [Circular *1],
+ emptyObj: {},
+ arr: [ 1, "s", false, null, [Circular *1] ],
+ baseClass: Base { a: 1 }
+ }
+}`;
+
+ assertEquals(stringify(1), "1");
+ assertEquals(stringify(-0), "-0");
+ assertEquals(stringify(1n), "1n");
+ assertEquals(stringify("s"), "s");
+ assertEquals(stringify(false), "false");
+ assertEquals(stringify(new Number(1)), "[Number: 1]");
+ assertEquals(stringify(new Number(-0)), "[Number: -0]");
+ assertEquals(stringify(Object(1n)), "[BigInt: 1n]");
+ assertEquals(stringify(new Boolean(true)), "[Boolean: true]");
+ assertEquals(stringify(new String("deno")), `[String: "deno"]`);
+ assertEquals(stringify(/[0-9]*/), "/[0-9]*/");
+ assertEquals(
+ stringify(new Date("2018-12-10T02:26:59.002Z")),
+ "2018-12-10T02:26:59.002Z",
+ );
+ assertEquals(stringify(new Set([1, 2, 3])), "Set(3) { 1, 2, 3 }");
+ assertEquals(
+ stringify(new Set([1, 2, 3]).values()),
+ "[Set Iterator] { 1, 2, 3 }",
+ );
+ assertEquals(
+ stringify(new Set([1, 2, 3]).entries()),
+ "[Set Entries] { [ 1, 1 ], [ 2, 2 ], [ 3, 3 ] }",
+ );
+ assertEquals(
+ stringify(
+ new Map([
+ [1, "one"],
+ [2, "two"],
+ ]),
+ ),
+ `Map(2) { 1 => "one", 2 => "two" }`,
+ );
+ assertEquals(
+ stringify(new Map([[1, "one"], [2, "two"]]).values()),
+ `[Map Iterator] { "one", "two" }`,
+ );
+ assertEquals(
+ stringify(new Map([[1, "one"], [2, "two"]]).entries()),
+ `[Map Entries] { [ 1, "one" ], [ 2, "two" ] }`,
+ );
+ assertEquals(stringify(new WeakSet()), "WeakSet { <items unknown> }");
+ assertEquals(stringify(new WeakMap()), "WeakMap { <items unknown> }");
+ assertEquals(stringify(Symbol(1)), `Symbol("1")`);
+ assertEquals(stringify(Object(Symbol(1))), `[Symbol: Symbol("1")]`);
+ assertEquals(stringify(null), "null");
+ assertEquals(stringify(undefined), "undefined");
+ assertEquals(stringify(new Extended()), "Extended { a: 1, b: 2 }");
+ assertEquals(
+ stringify(function f() {}),
+ "[Function: f]",
+ );
+ assertEquals(
+ stringify(async function af() {}),
+ "[AsyncFunction: af]",
+ );
+ assertEquals(
+ stringify(function* gf() {}),
+ "[GeneratorFunction: gf]",
+ );
+ assertEquals(
+ stringify(async function* agf() {}),
+ "[AsyncGeneratorFunction: agf]",
+ );
+ assertEquals(
+ stringify(new Uint8Array([1, 2, 3])),
+ "Uint8Array(3) [ 1, 2, 3 ]",
+ );
+ assertEquals(stringify(Uint8Array.prototype), "TypedArray {}");
+ assertEquals(
+ stringify({ a: { b: { c: { d: new Set([1]) } } } }),
+ `{
+ a: {
+ b: { c: { d: Set(1) { 1 } } }
+ }
+}`,
+ );
+ assertEquals(stringify(nestedObj), nestedObjExpected);
+ assertEquals(
+ stringify(JSON),
+ "Object [JSON] {}",
+ );
+ assertEquals(
+ stringify(new Console(() => {})),
+ `Object [console] {
+ log: [Function: log],
+ debug: [Function: debug],
+ info: [Function: info],
+ dir: [Function: dir],
+ dirxml: [Function: dir],
+ warn: [Function: warn],
+ error: [Function: error],
+ assert: [Function: assert],
+ count: [Function: count],
+ countReset: [Function: countReset],
+ table: [Function: table],
+ time: [Function: time],
+ timeLog: [Function: timeLog],
+ timeEnd: [Function: timeEnd],
+ group: [Function: group],
+ groupCollapsed: [Function: group],
+ groupEnd: [Function: groupEnd],
+ clear: [Function: clear],
+ trace: [Function: trace],
+ profile: [Function: profile],
+ profileEnd: [Function: profileEnd],
+ timeStamp: [Function: timeStamp],
+ indentLevel: 0,
+ [Symbol(isConsoleInstance)]: true
+}`,
+ );
+ assertEquals(
+ stringify({ str: 1, [Symbol.for("sym")]: 2, [Symbol.toStringTag]: "TAG" }),
+ `Object [TAG] {
+ str: 1,
+ [Symbol(sym)]: 2,
+ [Symbol(Symbol.toStringTag)]: "TAG"
+}`,
+ );
+ // test inspect is working the same
+ assertEquals(stripColor(Deno.inspect(nestedObj)), nestedObjExpected);
+});
+
+Deno.test(function consoleTestStringifyMultipleCircular() {
+ const y = { a: { b: {} }, foo: { bar: {} } };
+ y.a.b = y.a;
+ y.foo.bar = y.foo;
+ assertEquals(
+ stringify(y),
+ "{\n" +
+ " a: <ref *1> { b: [Circular *1] },\n" +
+ " foo: <ref *2> { bar: [Circular *2] }\n" +
+ "}",
+ );
+});
+
+Deno.test(function consoleTestStringifyFunctionWithPrototypeRemoved() {
+ const f = function f() {};
+ Reflect.setPrototypeOf(f, null);
+ assertEquals(stringify(f), "[Function (null prototype): f]");
+ const af = async function af() {};
+ Reflect.setPrototypeOf(af, null);
+ assertEquals(stringify(af), "[AsyncFunction (null prototype): af]");
+ const gf = function* gf() {};
+ Reflect.setPrototypeOf(gf, null);
+ assertEquals(stringify(gf), "[GeneratorFunction (null prototype): gf]");
+ const agf = async function* agf() {};
+ Reflect.setPrototypeOf(agf, null);
+ assertEquals(
+ stringify(agf),
+ "[AsyncGeneratorFunction (null prototype): agf]",
+ );
+});
+
+Deno.test(function consoleTestStringifyFunctionWithProperties() {
+ const f = () => "test";
+ f.x = () => "foo";
+ f.y = 3;
+ f.z = () => "baz";
+ f.b = function bar() {};
+ f.a = new Map();
+ assertEquals(
+ stringify({ f }),
+ `{
+ f: [Function: f] {
+ x: [Function (anonymous)],
+ y: 3,
+ z: [Function (anonymous)],
+ b: [Function: bar],
+ a: Map(0) {}
+ }
+}`,
+ );
+
+ const t = () => {};
+ t.x = f;
+ f.s = f;
+ f.t = t;
+ assertEquals(
+ stringify({ f }),
+ `{
+ f: <ref *1> [Function: f] {
+ x: [Function (anonymous)],
+ y: 3,
+ z: [Function (anonymous)],
+ b: [Function: bar],
+ a: Map(0) {},
+ s: [Circular *1],
+ t: [Function: t] { x: [Circular *1] }
+ }
+}`,
+ );
+
+ assertEquals(
+ stringify(Array),
+ `[Function: Array]`,
+ );
+
+ assertEquals(
+ stripColor(Deno.inspect(Array, { showHidden: true })),
+ `<ref *1> [Function: Array] {
+ [length]: 1,
+ [name]: "Array",
+ [prototype]: Object(0) [
+ [length]: 0,
+ [constructor]: [Circular *1],
+ [at]: [Function: at] { [length]: 1, [name]: "at" },
+ [concat]: [Function: concat] { [length]: 1, [name]: "concat" },
+ [copyWithin]: [Function: copyWithin] { [length]: 2, [name]: "copyWithin" },
+ [fill]: [Function: fill] { [length]: 1, [name]: "fill" },
+ [find]: [Function: find] { [length]: 1, [name]: "find" },
+ [findIndex]: [Function: findIndex] { [length]: 1, [name]: "findIndex" },
+ [findLast]: [Function: findLast] { [length]: 1, [name]: "findLast" },
+ [findLastIndex]: [Function: findLastIndex] { [length]: 1, [name]: "findLastIndex" },
+ [lastIndexOf]: [Function: lastIndexOf] { [length]: 1, [name]: "lastIndexOf" },
+ [pop]: [Function: pop] { [length]: 0, [name]: "pop" },
+ [push]: [Function: push] { [length]: 1, [name]: "push" },
+ [reverse]: [Function: reverse] { [length]: 0, [name]: "reverse" },
+ [shift]: [Function: shift] { [length]: 0, [name]: "shift" },
+ [unshift]: [Function: unshift] { [length]: 1, [name]: "unshift" },
+ [slice]: [Function: slice] { [length]: 2, [name]: "slice" },
+ [sort]: [Function: sort] { [length]: 1, [name]: "sort" },
+ [splice]: [Function: splice] { [length]: 2, [name]: "splice" },
+ [includes]: [Function: includes] { [length]: 1, [name]: "includes" },
+ [indexOf]: [Function: indexOf] { [length]: 1, [name]: "indexOf" },
+ [join]: [Function: join] { [length]: 1, [name]: "join" },
+ [keys]: [Function: keys] { [length]: 0, [name]: "keys" },
+ [entries]: [Function: entries] { [length]: 0, [name]: "entries" },
+ [values]: [Function: values] { [length]: 0, [name]: "values" },
+ [forEach]: [Function: forEach] { [length]: 1, [name]: "forEach" },
+ [filter]: [Function: filter] { [length]: 1, [name]: "filter" },
+ [flat]: [Function: flat] { [length]: 0, [name]: "flat" },
+ [flatMap]: [Function: flatMap] { [length]: 1, [name]: "flatMap" },
+ [map]: [Function: map] { [length]: 1, [name]: "map" },
+ [every]: [Function: every] { [length]: 1, [name]: "every" },
+ [some]: [Function: some] { [length]: 1, [name]: "some" },
+ [reduce]: [Function: reduce] { [length]: 1, [name]: "reduce" },
+ [reduceRight]: [Function: reduceRight] { [length]: 1, [name]: "reduceRight" },
+ [toReversed]: [Function: toReversed] { [length]: 0, [name]: "toReversed" },
+ [toSorted]: [Function: toSorted] { [length]: 1, [name]: "toSorted" },
+ [toSpliced]: [Function: toSpliced] { [length]: 2, [name]: "toSpliced" },
+ [with]: [Function: with] { [length]: 2, [name]: "with" },
+ [toLocaleString]: [Function: toLocaleString] { [length]: 0, [name]: "toLocaleString" },
+ [toString]: [Function: toString] { [length]: 0, [name]: "toString" },
+ [Symbol(Symbol.iterator)]: [Function: values] { [length]: 0, [name]: "values" },
+ [Symbol(Symbol.unscopables)]: [Object: null prototype] {
+ at: true,
+ copyWithin: true,
+ entries: true,
+ fill: true,
+ find: true,
+ findIndex: true,
+ findLast: true,
+ findLastIndex: true,
+ flat: true,
+ flatMap: true,
+ includes: true,
+ keys: true,
+ toReversed: true,
+ toSorted: true,
+ toSpliced: true,
+ values: true
+ }
+ ],
+ [isArray]: [Function: isArray] { [length]: 1, [name]: "isArray" },
+ [from]: [Function: from] { [length]: 1, [name]: "from" },
+ [of]: [Function: of] { [length]: 0, [name]: "of" },
+ [fromAsync]: [Function: fromAsync] { [length]: 1, [name]: "fromAsync" },
+ [Symbol(Symbol.species)]: [Getter]
+}`,
+ );
+});
+
+Deno.test(function consoleTestStringifyWithDepth() {
+ // deno-lint-ignore no-explicit-any
+ const nestedObj: any = { a: { b: { c: { d: { e: { f: 42 } } } } } };
+ assertEquals(
+ stripColor(inspectArgs([nestedObj], { depth: 3 })),
+ "{\n a: { b: { c: { d: [Object] } } }\n}",
+ );
+ assertEquals(
+ stripColor(inspectArgs([nestedObj], { depth: 4 })),
+ "{\n a: {\n b: { c: { d: { e: [Object] } } }\n }\n}",
+ );
+ assertEquals(
+ stripColor(inspectArgs([nestedObj], { depth: 0 })),
+ "{ a: [Object] }",
+ );
+ assertEquals(
+ stripColor(inspectArgs([nestedObj])),
+ "{\n a: {\n b: { c: { d: { e: [Object] } } }\n }\n}",
+ );
+ // test inspect is working the same way
+ assertEquals(
+ stripColor(Deno.inspect(nestedObj, { depth: 4 })),
+ "{\n a: {\n b: { c: { d: { e: [Object] } } }\n }\n}",
+ );
+});
+
+Deno.test(function consoleTestStringifyLargeObject() {
+ const obj = {
+ a: 2,
+ o: {
+ a: "1",
+ b: "2",
+ c: "3",
+ d: "4",
+ e: "5",
+ f: "6",
+ g: 10,
+ asd: 2,
+ asda: 3,
+ x: { a: "asd", x: 3 },
+ },
+ };
+ assertEquals(
+ stringify(obj),
+ `{
+ a: 2,
+ o: {
+ a: "1",
+ b: "2",
+ c: "3",
+ d: "4",
+ e: "5",
+ f: "6",
+ g: 10,
+ asd: 2,
+ asda: 3,
+ x: { a: "asd", x: 3 }
+ }
+}`,
+ );
+});
+
+Deno.test(function consoleTestStringifyIterable() {
+ const shortArray = [1, 2, 3, 4, 5];
+ assertEquals(stringify(shortArray), "[ 1, 2, 3, 4, 5 ]");
+
+ const longArray = new Array(200).fill(0);
+ assertEquals(
+ stringify(longArray),
+ `[
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ ... 100 more items
+]`,
+ );
+
+ const obj = { a: "a", longArray };
+ assertEquals(
+ stringify(obj),
+ `{
+ a: "a",
+ longArray: [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ ... 100 more items
+ ]
+}`,
+ );
+
+ const shortMap = new Map([
+ ["a", 0],
+ ["b", 1],
+ ]);
+ assertEquals(stringify(shortMap), `Map(2) { "a" => 0, "b" => 1 }`);
+
+ const longMap = new Map();
+ for (const key of Array(200).keys()) {
+ longMap.set(`${key}`, key);
+ }
+ assertEquals(
+ stringify(longMap),
+ `Map(200) {
+ "0" => 0,
+ "1" => 1,
+ "2" => 2,
+ "3" => 3,
+ "4" => 4,
+ "5" => 5,
+ "6" => 6,
+ "7" => 7,
+ "8" => 8,
+ "9" => 9,
+ "10" => 10,
+ "11" => 11,
+ "12" => 12,
+ "13" => 13,
+ "14" => 14,
+ "15" => 15,
+ "16" => 16,
+ "17" => 17,
+ "18" => 18,
+ "19" => 19,
+ "20" => 20,
+ "21" => 21,
+ "22" => 22,
+ "23" => 23,
+ "24" => 24,
+ "25" => 25,
+ "26" => 26,
+ "27" => 27,
+ "28" => 28,
+ "29" => 29,
+ "30" => 30,
+ "31" => 31,
+ "32" => 32,
+ "33" => 33,
+ "34" => 34,
+ "35" => 35,
+ "36" => 36,
+ "37" => 37,
+ "38" => 38,
+ "39" => 39,
+ "40" => 40,
+ "41" => 41,
+ "42" => 42,
+ "43" => 43,
+ "44" => 44,
+ "45" => 45,
+ "46" => 46,
+ "47" => 47,
+ "48" => 48,
+ "49" => 49,
+ "50" => 50,
+ "51" => 51,
+ "52" => 52,
+ "53" => 53,
+ "54" => 54,
+ "55" => 55,
+ "56" => 56,
+ "57" => 57,
+ "58" => 58,
+ "59" => 59,
+ "60" => 60,
+ "61" => 61,
+ "62" => 62,
+ "63" => 63,
+ "64" => 64,
+ "65" => 65,
+ "66" => 66,
+ "67" => 67,
+ "68" => 68,
+ "69" => 69,
+ "70" => 70,
+ "71" => 71,
+ "72" => 72,
+ "73" => 73,
+ "74" => 74,
+ "75" => 75,
+ "76" => 76,
+ "77" => 77,
+ "78" => 78,
+ "79" => 79,
+ "80" => 80,
+ "81" => 81,
+ "82" => 82,
+ "83" => 83,
+ "84" => 84,
+ "85" => 85,
+ "86" => 86,
+ "87" => 87,
+ "88" => 88,
+ "89" => 89,
+ "90" => 90,
+ "91" => 91,
+ "92" => 92,
+ "93" => 93,
+ "94" => 94,
+ "95" => 95,
+ "96" => 96,
+ "97" => 97,
+ "98" => 98,
+ "99" => 99,
+ ... 100 more items
+}`,
+ );
+
+ const shortSet = new Set([1, 2, 3]);
+ assertEquals(stringify(shortSet), `Set(3) { 1, 2, 3 }`);
+ const longSet = new Set();
+ for (const key of Array(200).keys()) {
+ longSet.add(key);
+ }
+ assertEquals(
+ stringify(longSet),
+ `Set(200) {
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 42,
+ 43,
+ 44,
+ 45,
+ 46,
+ 47,
+ 48,
+ 49,
+ 50,
+ 51,
+ 52,
+ 53,
+ 54,
+ 55,
+ 56,
+ 57,
+ 58,
+ 59,
+ 60,
+ 61,
+ 62,
+ 63,
+ 64,
+ 65,
+ 66,
+ 67,
+ 68,
+ 69,
+ 70,
+ 71,
+ 72,
+ 73,
+ 74,
+ 75,
+ 76,
+ 77,
+ 78,
+ 79,
+ 80,
+ 81,
+ 82,
+ 83,
+ 84,
+ 85,
+ 86,
+ 87,
+ 88,
+ 89,
+ 90,
+ 91,
+ 92,
+ 93,
+ 94,
+ 95,
+ 96,
+ 97,
+ 98,
+ 99,
+ ... 100 more items
+}`,
+ );
+
+ const withEmptyEl = Array(10);
+ withEmptyEl.fill(0, 4, 6);
+ assertEquals(
+ stringify(withEmptyEl),
+ `[ <4 empty items>, 0, 0, <4 empty items> ]`,
+ );
+
+ const emptyArray = Array(5000);
+ assertEquals(
+ stringify(emptyArray),
+ `[ <5000 empty items> ]`,
+ );
+
+ assertEquals(
+ stringify(Array(1)),
+ `[ <1 empty item> ]`,
+ );
+
+ assertEquals(
+ stringify([, , 1]),
+ `[ <2 empty items>, 1 ]`,
+ );
+
+ assertEquals(
+ stringify([1, , , 1]),
+ `[ 1, <2 empty items>, 1 ]`,
+ );
+
+ const withEmptyElAndMoreItems = Array(500);
+ withEmptyElAndMoreItems.fill(0, 50, 80);
+ withEmptyElAndMoreItems.fill(2, 100, 120);
+ withEmptyElAndMoreItems.fill(3, 140, 160);
+ withEmptyElAndMoreItems.fill(4, 180);
+ assertEquals(
+ stringify(withEmptyElAndMoreItems),
+ `[
+ <50 empty items>, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, <20 empty items>,
+ 2, 2, 2, 2,
+ 2, 2, 2, 2,
+ 2, 2, 2, 2,
+ 2, 2, 2, 2,
+ 2, 2, 2, 2,
+ <20 empty items>, 3, 3, 3,
+ 3, 3, 3, 3,
+ 3, 3, 3, 3,
+ 3, 3, 3, 3,
+ 3, 3, 3, 3,
+ 3, <20 empty items>, 4, 4,
+ 4, 4, 4, 4,
+ 4, 4, 4, 4,
+ 4, 4, 4, 4,
+ 4, 4, 4, 4,
+ 4, 4, 4, 4,
+ 4, 4, 4, 4,
+ ... 294 more items
+]`,
+ );
+
+ const lWithEmptyEl = Array(200);
+ lWithEmptyEl.fill(0, 50, 80);
+ assertEquals(
+ stringify(lWithEmptyEl),
+ `[
+ <50 empty items>, 0, 0,
+ 0, 0, 0,
+ 0, 0, 0,
+ 0, 0, 0,
+ 0, 0, 0,
+ 0, 0, 0,
+ 0, 0, 0,
+ 0, 0, 0,
+ 0, 0, 0,
+ 0, 0, 0,
+ 0, <120 empty items>
+]`,
+ );
+});
+
+Deno.test(function consoleTestStringifyIterableWhenGrouped() {
+ const withOddNumberOfEls = new Float64Array(
+ [
+ 2.1,
+ 2.01,
+ 2.001,
+ 2.0001,
+ 2.00001,
+ 2.000001,
+ 2.0000001,
+ 2.00000001,
+ 2.000000001,
+ 2.0000000001,
+ 2,
+ ],
+ );
+ assertEquals(
+ stringify(withOddNumberOfEls),
+ `Float64Array(11) [
+ 2.1, 2.01,
+ 2.001, 2.0001,
+ 2.00001, 2.000001,
+ 2.0000001, 2.00000001,
+ 2.000000001, 2.0000000001,
+ 2
+]`,
+ );
+ const withEvenNumberOfEls = new Float64Array(
+ [
+ 2.1,
+ 2.01,
+ 2.001,
+ 2.0001,
+ 2.00001,
+ 2.000001,
+ 2.0000001,
+ 2.00000001,
+ 2.000000001,
+ 2.0000000001,
+ 2,
+ 2,
+ ],
+ );
+ assertEquals(
+ stringify(withEvenNumberOfEls),
+ `Float64Array(12) [
+ 2.1, 2.01,
+ 2.001, 2.0001,
+ 2.00001, 2.000001,
+ 2.0000001, 2.00000001,
+ 2.000000001, 2.0000000001,
+ 2, 2
+]`,
+ );
+ const withThreeColumns = [
+ 2,
+ 2.1,
+ 2.11,
+ 2,
+ 2.111,
+ 2.1111,
+ 2,
+ 2.1,
+ 2.11,
+ 2,
+ 2.1,
+ ];
+ assertEquals(
+ stringify(withThreeColumns),
+ `[
+ 2, 2.1, 2.11,
+ 2, 2.111, 2.1111,
+ 2, 2.1, 2.11,
+ 2, 2.1
+]`,
+ );
+});
+
+Deno.test(function consoleTestIteratorValueAreNotConsumed() {
+ const setIterator = new Set([1, 2, 3]).values();
+ assertEquals(
+ stringify(setIterator),
+ "[Set Iterator] { 1, 2, 3 }",
+ );
+ assertEquals([...setIterator], [1, 2, 3]);
+});
+
+Deno.test(function consoleTestWeakSetAndWeakMapWithShowHidden() {
+ assertEquals(
+ stripColor(Deno.inspect(new WeakSet([{}]), { showHidden: true })),
+ "WeakSet { {} }",
+ );
+ assertEquals(
+ stripColor(Deno.inspect(new WeakMap([[{}, "foo"]]), { showHidden: true })),
+ 'WeakMap { {} => "foo" }',
+ );
+});
+
+Deno.test(async function consoleTestStringifyPromises() {
+ const pendingPromise = new Promise((_res, _rej) => {});
+ assertEquals(stringify(pendingPromise), "Promise { <pending> }");
+
+ const resolvedPromise = new Promise((res, _rej) => {
+ res("Resolved!");
+ });
+ assertEquals(stringify(resolvedPromise), `Promise { "Resolved!" }`);
+
+ let rejectedPromise;
+ try {
+ rejectedPromise = new Promise((_, rej) => {
+ rej(Error("Whoops"));
+ });
+ await rejectedPromise;
+ } catch (_err) {
+ // pass
+ }
+ const strLines = stringify(rejectedPromise).split("\n");
+ assertEquals(strLines[0], "Promise {");
+ assertEquals(strLines[1], " <rejected> Error: Whoops");
+});
+
+Deno.test(function consoleTestWithCustomInspector() {
+ class A {
+ [customInspect](
+ inspect: unknown,
+ options: Deno.InspectOptions,
+ ): string {
+ assertEquals(typeof inspect, "function");
+ assertEquals(typeof options, "object");
+ return "b";
+ }
+ }
+
+ assertEquals(stringify(new A()), "b");
+});
+
+Deno.test(function consoleTestWithCustomInspectorUsingInspectFunc() {
+ class A {
+ [customInspect](
+ inspect: (v: unknown, opts?: Deno.InspectOptions) => string,
+ ): string {
+ return "b " + inspect({ c: 1 });
+ }
+ }
+
+ assertEquals(stringify(new A()), "b { c: 1 }");
+});
+
+Deno.test(function consoleTestWithConstructorError() {
+ const obj = new Proxy({}, {
+ getOwnPropertyDescriptor(_target, name) {
+ if (name == "constructor") {
+ throw "yikes";
+ }
+ return undefined;
+ },
+ });
+ assertEquals(Deno.inspect(obj), "{}");
+});
+
+Deno.test(function consoleTestWithCustomInspectorError() {
+ class A {
+ [customInspect](): never {
+ throw new Error("BOOM");
+ }
+ }
+
+ const a = new A();
+ assertThrows(
+ () => stringify(a),
+ Error,
+ "BOOM",
+ "Custom inspect won't attempt to parse if user defined function throws",
+ );
+ assertThrows(
+ () => stringify(a),
+ Error,
+ "BOOM",
+ "Inspect should fail and maintain a clear CTX_STACK",
+ );
+});
+
+Deno.test(function consoleTestWithCustomInspectFunction() {
+ function a() {}
+ Object.assign(a, {
+ [customInspect]() {
+ return "b";
+ },
+ });
+
+ assertEquals(stringify(a), "b");
+});
+
+Deno.test(function consoleTestWithIntegerFormatSpecifier() {
+ assertEquals(stringify("%i"), "%i");
+ assertEquals(stringify("%i", 42.0), "42");
+ assertEquals(stringify("%i", 42), "42");
+ assertEquals(stringify("%i", "42"), "NaN");
+ assertEquals(stringify("%i", 1.5), "1");
+ assertEquals(stringify("%i", -0.5), "0");
+ assertEquals(stringify("%i", ""), "NaN");
+ assertEquals(stringify("%i", Symbol()), "NaN");
+ assertEquals(stringify("%i %d", 42, 43), "42 43");
+ assertEquals(stringify("%d %i", 42), "42 %i");
+ assertEquals(stringify("%d", 12345678901234567890123), "1");
+ assertEquals(
+ stringify("%i", 12345678901234567890123n),
+ "12345678901234567890123n",
+ );
+});
+
+Deno.test(function consoleTestWithFloatFormatSpecifier() {
+ assertEquals(stringify("%f"), "%f");
+ assertEquals(stringify("%f", 42.0), "42");
+ assertEquals(stringify("%f", 42), "42");
+ assertEquals(stringify("%f", "42"), "NaN");
+ assertEquals(stringify("%f", 1.5), "1.5");
+ assertEquals(stringify("%f", -0.5), "-0.5");
+ assertEquals(stringify("%f", Math.PI), "3.141592653589793");
+ assertEquals(stringify("%f", ""), "NaN");
+ assertEquals(stringify("%f", Symbol("foo")), "NaN");
+ assertEquals(stringify("%f", 5n), "NaN");
+ assertEquals(stringify("%f %f", 42, 43), "42 43");
+ assertEquals(stringify("%f %f", 42), "42 %f");
+});
+
+Deno.test(function consoleTestWithStringFormatSpecifier() {
+ assertEquals(stringify("%s"), "%s");
+ assertEquals(stringify("%s", undefined), "undefined");
+ assertEquals(stringify("%s", "foo"), "foo");
+ assertEquals(stringify("%s", 42), "42");
+ assertEquals(stringify("%s", "42"), "42");
+ assertEquals(stringify("%s %s", 42, 43), "42 43");
+ assertEquals(stringify("%s %s", 42), "42 %s");
+ assertEquals(stringify("%s", Symbol("foo")), "Symbol(foo)");
+});
+
+Deno.test(function consoleTestWithObjectFormatSpecifier() {
+ assertEquals(stringify("%o"), "%o");
+ assertEquals(stringify("%o", 42), "42");
+ assertEquals(stringify("%o", "foo"), `"foo"`);
+ assertEquals(stringify("o: %o, a: %O", {}, []), "o: {}, a: []");
+ assertEquals(stringify("%o", { a: 42 }), "{ a: 42 }");
+ assertEquals(
+ stringify("%o", { a: { b: { c: { d: new Set([1]) } } } }),
+ "{\n a: {\n b: { c: { d: Set(1) { 1 } } }\n }\n}",
+ );
+});
+
+Deno.test(function consoleTestWithStyleSpecifier() {
+ assertEquals(stringify("%cfoo%cbar"), "%cfoo%cbar");
+ assertEquals(stringify("%cfoo%cbar", ""), "foo%cbar");
+ assertEquals(stripColor(stringify("%cfoo%cbar", "", "color: red")), "foobar");
+});
+
+Deno.test(function consoleParseCssColor() {
+ assertEquals(parseCssColor("inherit"), null);
+ assertEquals(parseCssColor("black"), [0, 0, 0]);
+ assertEquals(parseCssColor("darkmagenta"), [139, 0, 139]);
+ assertEquals(parseCssColor("slateblue"), [106, 90, 205]);
+ assertEquals(parseCssColor("#ffaa00"), [255, 170, 0]);
+ assertEquals(parseCssColor("#ffAA00"), [255, 170, 0]);
+ assertEquals(parseCssColor("#fa0"), [255, 170, 0]);
+ assertEquals(parseCssColor("#FA0"), [255, 170, 0]);
+ assertEquals(parseCssColor("#18d"), [17, 136, 221]);
+ assertEquals(parseCssColor("#18D"), [17, 136, 221]);
+ assertEquals(parseCssColor("#1188DD"), [17, 136, 221]);
+ assertEquals(parseCssColor("rgb(100, 200, 50)"), [100, 200, 50]);
+ assertEquals(parseCssColor("rgb(+100.3, -200, .5)"), [100, 0, 1]);
+ assertEquals(parseCssColor("hsl(75, 60%, 40%)"), [133, 163, 41]);
+
+ assertEquals(parseCssColor("rgb(100,200,50)"), [100, 200, 50]);
+ assertEquals(
+ parseCssColor("rgb( \t\n100 \t\n, \t\n200 \t\n, \t\n50 \t\n)"),
+ [100, 200, 50],
+ );
+});
+
+Deno.test(function consoleParseCss() {
+ assertEquals(
+ parseCss("background-color: inherit"),
+ { ...DEFAULT_CSS, backgroundColor: "inherit" },
+ );
+ assertEquals(
+ parseCss("color: inherit"),
+ { ...DEFAULT_CSS, color: "inherit" },
+ );
+ assertEquals(
+ parseCss("background-color: red"),
+ { ...DEFAULT_CSS, backgroundColor: "red" },
+ );
+ assertEquals(parseCss("color: blue"), { ...DEFAULT_CSS, color: "blue" });
+ assertEquals(
+ parseCss("font-weight: bold"),
+ { ...DEFAULT_CSS, fontWeight: "bold" },
+ );
+ assertEquals(
+ parseCss("font-style: italic"),
+ { ...DEFAULT_CSS, fontStyle: "italic" },
+ );
+ assertEquals(
+ parseCss("font-style: oblique"),
+ { ...DEFAULT_CSS, fontStyle: "italic" },
+ );
+ assertEquals(
+ parseCss("text-decoration-color: green"),
+ { ...DEFAULT_CSS, textDecorationColor: [0, 128, 0] },
+ );
+ assertEquals(
+ parseCss("text-decoration-line: underline overline line-through"),
+ {
+ ...DEFAULT_CSS,
+ textDecorationLine: ["underline", "overline", "line-through"],
+ },
+ );
+ assertEquals(
+ parseCss("text-decoration: yellow underline"),
+ {
+ ...DEFAULT_CSS,
+ textDecorationColor: [255, 255, 0],
+ textDecorationLine: ["underline"],
+ },
+ );
+
+ assertEquals(
+ parseCss("color:red;font-weight:bold;"),
+ { ...DEFAULT_CSS, color: "red", fontWeight: "bold" },
+ );
+ assertEquals(
+ parseCss(
+ " \t\ncolor \t\n: \t\nred \t\n; \t\nfont-weight \t\n: \t\nbold \t\n; \t\n",
+ ),
+ { ...DEFAULT_CSS, color: "red", fontWeight: "bold" },
+ );
+ assertEquals(
+ parseCss("color: red; font-weight: bold, font-style: italic"),
+ { ...DEFAULT_CSS, color: "red" },
+ );
+});
+
+Deno.test(function consoleCssToAnsi() {
+ assertEquals(
+ cssToAnsiEsc({ ...DEFAULT_CSS, backgroundColor: "inherit" }),
+ "_[49m",
+ );
+ assertEquals(
+ cssToAnsiEsc({ ...DEFAULT_CSS, backgroundColor: "foo" }),
+ "_[49m",
+ );
+ assertEquals(
+ cssToAnsiEsc({ ...DEFAULT_CSS, backgroundColor: "black" }),
+ "_[40m",
+ );
+ assertEquals(
+ cssToAnsiEsc({ ...DEFAULT_CSS, color: "inherit" }),
+ "_[39m",
+ );
+ assertEquals(
+ cssToAnsiEsc({ ...DEFAULT_CSS, color: "blue" }),
+ "_[34m",
+ );
+ assertEquals(
+ cssToAnsiEsc({ ...DEFAULT_CSS, backgroundColor: [200, 201, 202] }),
+ "_[48;2;200;201;202m",
+ );
+ assertEquals(
+ cssToAnsiEsc({ ...DEFAULT_CSS, color: [203, 204, 205] }),
+ "_[38;2;203;204;205m",
+ );
+ assertEquals(cssToAnsiEsc({ ...DEFAULT_CSS, fontWeight: "bold" }), "_[1m");
+ assertEquals(cssToAnsiEsc({ ...DEFAULT_CSS, fontStyle: "italic" }), "_[3m");
+ assertEquals(
+ cssToAnsiEsc({ ...DEFAULT_CSS, textDecorationColor: [206, 207, 208] }),
+ "_[58;2;206;207;208m",
+ );
+ assertEquals(
+ cssToAnsiEsc({ ...DEFAULT_CSS, textDecorationLine: ["underline"] }),
+ "_[4m",
+ );
+ assertEquals(
+ cssToAnsiEsc(
+ { ...DEFAULT_CSS, textDecorationLine: ["overline", "line-through"] },
+ ),
+ "_[9m_[53m",
+ );
+ assertEquals(
+ cssToAnsiEsc(
+ { ...DEFAULT_CSS, color: [203, 204, 205], fontWeight: "bold" },
+ ),
+ "_[38;2;203;204;205m_[1m",
+ );
+ assertEquals(
+ cssToAnsiEsc(
+ { ...DEFAULT_CSS, color: [0, 0, 0], fontWeight: "bold" },
+ { ...DEFAULT_CSS, color: [203, 204, 205], fontStyle: "italic" },
+ ),
+ "_[38;2;0;0;0m_[1m_[23m",
+ );
+});
+
+Deno.test(function consoleTestWithVariousOrInvalidFormatSpecifier() {
+ assertEquals(stringify("%s:%s"), "%s:%s");
+ assertEquals(stringify("%i:%i"), "%i:%i");
+ assertEquals(stringify("%d:%d"), "%d:%d");
+ assertEquals(stringify("%%s%s", "foo"), "%sfoo");
+ assertEquals(stringify("%s:%s", undefined), "undefined:%s");
+ assertEquals(stringify("%s:%s", "foo", "bar"), "foo:bar");
+ assertEquals(stringify("%s:%s", "foo", "bar", "baz"), "foo:bar baz");
+ assertEquals(stringify("%%%s%%", "hi"), "%hi%");
+ assertEquals(stringify("%d:%d", 12), "12:%d");
+ assertEquals(stringify("%i:%i", 12), "12:%i");
+ assertEquals(stringify("%f:%f", 12), "12:%f");
+ assertEquals(stringify("o: %o, a: %o", {}), "o: {}, a: %o");
+ assertEquals(stringify("abc%", 1), "abc% 1");
+});
+
+Deno.test(function consoleTestCallToStringOnLabel() {
+ const methods = ["count", "countReset", "time", "timeLog", "timeEnd"];
+ mockConsole((console) => {
+ for (const method of methods) {
+ let hasCalled = false;
+ console[method]({
+ toString() {
+ hasCalled = true;
+ },
+ });
+ assertEquals(hasCalled, true);
+ }
+ });
+});
+
+Deno.test(function consoleTestError() {
+ class MyError extends Error {
+ constructor(errStr: string) {
+ super(errStr);
+ this.name = "MyError";
+ }
+ }
+ try {
+ throw new MyError("This is an error");
+ } catch (e) {
+ assert(
+ stringify(e)
+ .split("\n")[0] // error has been caught
+ .includes("MyError: This is an error"),
+ );
+ }
+});
+
+Deno.test(function consoleTestClear() {
+ mockConsole((console, out) => {
+ console.clear();
+ assertEquals(out.toString(), "\x1b[1;1H" + "\x1b[0J");
+ });
+});
+
+// Test bound this issue
+Deno.test(function consoleDetachedLog() {
+ mockConsole((console) => {
+ const log = console.log;
+ const dir = console.dir;
+ const dirxml = console.dirxml;
+ const debug = console.debug;
+ const info = console.info;
+ const warn = console.warn;
+ const error = console.error;
+ const consoleAssert = console.assert;
+ const consoleCount = console.count;
+ const consoleCountReset = console.countReset;
+ const consoleTable = console.table;
+ const consoleTime = console.time;
+ const consoleTimeLog = console.timeLog;
+ const consoleTimeEnd = console.timeEnd;
+ const consoleGroup = console.group;
+ const consoleGroupEnd = console.groupEnd;
+ const consoleClear = console.clear;
+ log("Hello world");
+ dir("Hello world");
+ dirxml("Hello world");
+ debug("Hello world");
+ info("Hello world");
+ warn("Hello world");
+ error("Hello world");
+ consoleAssert(true);
+ consoleCount("Hello world");
+ consoleCountReset("Hello world");
+ consoleTable({ test: "Hello world" });
+ consoleTime("Hello world");
+ consoleTimeLog("Hello world");
+ consoleTimeEnd("Hello world");
+ consoleGroup("Hello world");
+ consoleGroupEnd();
+ consoleClear();
+ });
+});
+
+class StringBuffer {
+ chunks: string[] = [];
+ add(x: string) {
+ this.chunks.push(x);
+ }
+ toString(): string {
+ return this.chunks.join("");
+ }
+}
+
+type ConsoleExamineFunc = (
+ // deno-lint-ignore no-explicit-any
+ csl: any,
+ out: StringBuffer,
+ err?: StringBuffer,
+ both?: StringBuffer,
+) => void;
+
+function mockConsole(f: ConsoleExamineFunc) {
+ const out = new StringBuffer();
+ const err = new StringBuffer();
+ const both = new StringBuffer();
+ const csl = new Console(
+ (x: string, level: number, printsNewLine: boolean) => {
+ const content = x + (printsNewLine ? "\n" : "");
+ const buf = level > 1 ? err : out;
+ buf.add(content);
+ both.add(content);
+ },
+ );
+ f(csl, out, err, both);
+}
+
+// console.group test
+Deno.test(function consoleGroup() {
+ mockConsole((console, out) => {
+ console.group("1");
+ console.log("2");
+ console.group("3");
+ console.log("4");
+ console.groupEnd();
+ console.groupEnd();
+ console.log("5");
+ console.log("6");
+
+ assertEquals(
+ out.toString(),
+ `1
+ 2
+ 3
+ 4
+5
+6
+`,
+ );
+ });
+});
+
+// console.group with console.warn test
+Deno.test(function consoleGroupWarn() {
+ mockConsole((console, _out, _err, both) => {
+ assert(both);
+ console.warn("1");
+ console.group();
+ console.warn("2");
+ console.group();
+ console.warn("3");
+ console.groupEnd();
+ console.warn("4");
+ console.groupEnd();
+ console.warn("5");
+
+ console.warn("6");
+ console.warn("7");
+ assertEquals(
+ both.toString(),
+ `1
+ 2
+ 3
+ 4
+5
+6
+7
+`,
+ );
+ });
+});
+
+// console.table test
+Deno.test(function consoleTable() {
+ mockConsole((console, out) => {
+ console.table({ a: "test", b: 1 });
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┬────────┐
+│ (idx) │ Values │
+├───────┼────────┤
+│ a │ "test" │
+│ b │ 1 │
+└───────┴────────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table({ a: { b: 10 }, b: { b: 20, c: 30 } }, ["c"]);
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┬────┐
+│ (idx) │ c │
+├───────┼────┤
+│ a │ │
+│ b │ 30 │
+└───────┴────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table([[1, 1], [234, 2.34], [56789, 56.789]]);
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┬───────┬────────┐
+│ (idx) │ 0 │ 1 │
+├───────┼───────┼────────┤
+│ 0 │ 1 │ 1 │
+│ 1 │ 234 │ 2.34 │
+│ 2 │ 56789 │ 56.789 │
+└───────┴───────┴────────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table([1, 2, [3, [4]], [5, 6], [[7], [8]]]);
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┬───────┬───────┬────────┐
+│ (idx) │ 0 │ 1 │ Values │
+├───────┼───────┼───────┼────────┤
+│ 0 │ │ │ 1 │
+│ 1 │ │ │ 2 │
+│ 2 │ 3 │ [ 4 ] │ │
+│ 3 │ 5 │ 6 │ │
+│ 4 │ [ 7 ] │ [ 8 ] │ │
+└───────┴───────┴───────┴────────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table(new Set([1, 2, 3, "test"]));
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌────────────┬────────┐
+│ (iter idx) │ Values │
+├────────────┼────────┤
+│ 0 │ 1 │
+│ 1 │ 2 │
+│ 2 │ 3 │
+│ 3 │ "test" │
+└────────────┴────────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table(
+ new Map([
+ [1, "one"],
+ [2, "two"],
+ ]),
+ );
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌────────────┬─────┬────────┐
+│ (iter idx) │ Key │ Values │
+├────────────┼─────┼────────┤
+│ 0 │ 1 │ "one" │
+│ 1 │ 2 │ "two" │
+└────────────┴─────┴────────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table({
+ a: true,
+ b: { c: { d: 10 }, e: [1, 2, [5, 6]] },
+ f: "test",
+ g: new Set([1, 2, 3, "test"]),
+ h: new Map([[1, "one"]]),
+ });
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┬───────────┬────────────────────┬────────┐
+│ (idx) │ c │ e │ Values │
+├───────┼───────────┼────────────────────┼────────┤
+│ a │ │ │ true │
+│ b │ { d: 10 } │ [ 1, 2, [ 5, 6 ] ] │ │
+│ f │ │ │ "test" │
+│ g │ │ │ │
+│ h │ │ │ │
+└───────┴───────────┴────────────────────┴────────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table([
+ 1,
+ "test",
+ false,
+ { a: 10 },
+ ["test", { b: 20, c: "test" }],
+ ]);
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┬────────┬──────────────────────┬────┬────────┐
+│ (idx) │ 0 │ 1 │ a │ Values │
+├───────┼────────┼──────────────────────┼────┼────────┤
+│ 0 │ │ │ │ 1 │
+│ 1 │ │ │ │ "test" │
+│ 2 │ │ │ │ false │
+│ 3 │ │ │ 10 │ │
+│ 4 │ "test" │ { b: 20, c: "test" } │ │ │
+└───────┴────────┴──────────────────────┴────┴────────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table([]);
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┐
+│ (idx) │
+├───────┤
+└───────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table({});
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┐
+│ (idx) │
+├───────┤
+└───────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table(new Set());
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌────────────┐
+│ (iter idx) │
+├────────────┤
+└────────────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table(new Map());
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌────────────┐
+│ (iter idx) │
+├────────────┤
+└────────────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table("test");
+ assertEquals(out.toString(), "test\n");
+ });
+ mockConsole((console, out) => {
+ console.table(["Hello", "你好", "Amapá"]);
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┬─────────┐
+│ (idx) │ Values │
+├───────┼─────────┤
+│ 0 │ "Hello" │
+│ 1 │ "你好" │
+│ 2 │ "Amapá" │
+└───────┴─────────┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table([
+ [1, 2],
+ [3, 4],
+ ]);
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┬───┬───┐
+│ (idx) │ 0 │ 1 │
+├───────┼───┼───┤
+│ 0 │ 1 │ 2 │
+│ 1 │ 3 │ 4 │
+└───────┴───┴───┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table({ 1: { a: 4, b: 5 }, 2: null, 3: { b: 6, c: 7 } }, ["b"]);
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┬───┐
+│ (idx) │ b │
+├───────┼───┤
+│ 1 │ 5 │
+│ 2 │ │
+│ 3 │ 6 │
+└───────┴───┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table([{ a: 0 }, { a: 1, b: 1 }, { a: 2 }, { a: 3, b: 3 }]);
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┬───┬───┐
+│ (idx) │ a │ b │
+├───────┼───┼───┤
+│ 0 │ 0 │ │
+│ 1 │ 1 │ 1 │
+│ 2 │ 2 │ │
+│ 3 │ 3 │ 3 │
+└───────┴───┴───┘
+`,
+ );
+ });
+ mockConsole((console, out) => {
+ console.table(
+ [{ a: 0 }, { a: 1, c: 1 }, { a: 2 }, { a: 3, c: 3 }],
+ ["a", "b", "c"],
+ );
+ assertEquals(
+ stripColor(out.toString()),
+ `\
+┌───────┬───┬───┬───┐
+│ (idx) │ a │ b │ c │
+├───────┼───┼───┼───┤
+│ 0 │ 0 │ │ │
+│ 1 │ 1 │ │ 1 │
+│ 2 │ 2 │ │ │
+│ 3 │ 3 │ │ 3 │
+└───────┴───┴───┴───┘
+`,
+ );
+ });
+});
+
+// console.log(Error) test
+Deno.test(function consoleLogShouldNotThrowError() {
+ mockConsole((console) => {
+ let result = 0;
+ try {
+ console.log(new Error("foo"));
+ result = 1;
+ } catch (_e) {
+ result = 2;
+ }
+ assertEquals(result, 1);
+ });
+
+ // output errors to the console should not include "Uncaught"
+ mockConsole((console, out) => {
+ console.log(new Error("foo"));
+ assertEquals(out.toString().includes("Uncaught"), false);
+ });
+});
+
+Deno.test(function consoleLogShouldNotThrowErrorWhenInvalidCssColorsAreGiven() {
+ mockConsole((console, out) => {
+ console.log("%cfoo", "color: foo; background-color: bar;");
+ assertEquals(stripColor(out.toString()), "foo\n");
+ });
+});
+
+// console.log(Invalid Date) test
+Deno.test(function consoleLogShouldNotThrowErrorWhenInvalidDateIsPassed() {
+ mockConsole((console, out) => {
+ const invalidDate = new Date("test");
+ console.log(invalidDate);
+ assertEquals(stripColor(out.toString()), "Invalid Date\n");
+ });
+});
+
+// console.log(new Proxy(new Set(), {}))
+Deno.test(function consoleLogShouldNotThrowErrorWhenInputIsProxiedSet() {
+ mockConsole((console, out) => {
+ const proxiedSet = new Proxy(new Set([1, 2]), {});
+ console.log(proxiedSet);
+ assertEquals(stripColor(out.toString()), "Set(2) { 1, 2 }\n");
+ });
+});
+
+// console.log(new Proxy(new Map(), {}))
+Deno.test(function consoleLogShouldNotThrowErrorWhenInputIsProxiedMap() {
+ mockConsole((console, out) => {
+ const proxiedMap = new Proxy(new Map([[1, 1], [2, 2]]), {});
+ console.log(proxiedMap);
+ assertEquals(stripColor(out.toString()), "Map(2) { 1 => 1, 2 => 2 }\n");
+ });
+});
+
+// console.log(new Proxy(new Uint8Array(), {}))
+Deno.test(function consoleLogShouldNotThrowErrorWhenInputIsProxiedTypedArray() {
+ mockConsole((console, out) => {
+ const proxiedUint8Array = new Proxy(new Uint8Array([1, 2]), {});
+ console.log(proxiedUint8Array);
+ assertEquals(stripColor(out.toString()), "Uint8Array(2) [ 1, 2 ]\n");
+ });
+});
+
+// console.log(new Proxy(new RegExp(), {}))
+Deno.test(function consoleLogShouldNotThrowErrorWhenInputIsProxiedRegExp() {
+ mockConsole((console, out) => {
+ const proxiedRegExp = new Proxy(/aaaa/, {});
+ console.log(proxiedRegExp);
+ assertEquals(stripColor(out.toString()), "/aaaa/\n");
+ });
+});
+
+// console.log(new Proxy(new Date(), {}))
+Deno.test(function consoleLogShouldNotThrowErrorWhenInputIsProxiedDate() {
+ mockConsole((console, out) => {
+ const proxiedDate = new Proxy(new Date("2022-09-24T15:59:39.529Z"), {});
+ console.log(proxiedDate);
+ assertEquals(stripColor(out.toString()), "2022-09-24T15:59:39.529Z\n");
+ });
+});
+
+// console.log(new Proxy(new Error(), {}))
+Deno.test(function consoleLogShouldNotThrowErrorWhenInputIsProxiedError() {
+ mockConsole((console, out) => {
+ const proxiedError = new Proxy(new Error("message"), {});
+ console.log(proxiedError);
+ assertStringIncludes(stripColor(out.toString()), "Error: message\n");
+ });
+});
+
+// console.dir test
+Deno.test(function consoleDir() {
+ mockConsole((console, out) => {
+ console.dir("DIR");
+ assertEquals(out.toString(), "DIR\n");
+ });
+ mockConsole((console, out) => {
+ console.dir("DIR", { indentLevel: 2 });
+ assertEquals(out.toString(), " DIR\n");
+ });
+});
+
+// console.dir test
+Deno.test(function consoleDirXml() {
+ mockConsole((console, out) => {
+ console.dirxml("DIRXML");
+ assertEquals(out.toString(), "DIRXML\n");
+ });
+ mockConsole((console, out) => {
+ console.dirxml("DIRXML", { indentLevel: 2 });
+ assertEquals(out.toString(), " DIRXML\n");
+ });
+});
+
+// console.trace test
+Deno.test(function consoleTrace() {
+ mockConsole((console, _out, err) => {
+ console.trace("%s", "custom message");
+ assert(err);
+ assert(err.toString().includes("Trace: custom message"));
+ });
+});
+
+Deno.test(function inspectString() {
+ assertEquals(
+ stripColor(Deno.inspect("\0")),
+ `"\\x00"`,
+ );
+ assertEquals(
+ stripColor(Deno.inspect("\x1b[2J")),
+ `"\\x1b[2J"`,
+ );
+});
+
+Deno.test(function inspectGetters() {
+ assertEquals(
+ stripColor(Deno.inspect({
+ get foo() {
+ return 0;
+ },
+ })),
+ "{ foo: [Getter] }",
+ );
+
+ assertEquals(
+ stripColor(Deno.inspect({
+ get foo() {
+ return 0;
+ },
+ }, { getters: true })),
+ "{ foo: [Getter: 0] }",
+ );
+
+ assertEquals(
+ Deno.inspect({
+ get foo() {
+ throw new Error("bar");
+ },
+ }, { getters: true }),
+ "{ foo: [Getter: <Inspection threw (bar)>] }",
+ );
+});
+
+Deno.test(function inspectPrototype() {
+ class A {}
+ assertEquals(Deno.inspect(A.prototype), "{}");
+});
+
+Deno.test(function inspectSorted() {
+ assertEquals(
+ stripColor(Deno.inspect({ b: 2, a: 1 }, { sorted: true })),
+ "{ a: 1, b: 2 }",
+ );
+ assertEquals(
+ stripColor(Deno.inspect(new Set(["b", "a"]), { sorted: true })),
+ `Set(2) { "a", "b" }`,
+ );
+ assertEquals(
+ stripColor(Deno.inspect(
+ new Map([
+ ["b", 2],
+ ["a", 1],
+ ]),
+ { sorted: true },
+ )),
+ `Map(2) { "a" => 1, "b" => 2 }`,
+ );
+});
+
+Deno.test(function inspectTrailingComma() {
+ assertEquals(
+ stripColor(Deno.inspect(
+ [
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ ],
+ { trailingComma: true },
+ )),
+ `[
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+]`,
+ );
+ assertEquals(
+ stripColor(Deno.inspect(
+ {
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: 1,
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: 2,
+ },
+ { trailingComma: true },
+ )),
+ `{
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: 1,
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: 2,
+}`,
+ );
+ assertEquals(
+ stripColor(Deno.inspect(
+ new Set([
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ ]),
+ { trailingComma: true },
+ )),
+ `Set(2) {
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+}`,
+ );
+ assertEquals(
+ stripColor(Deno.inspect(
+ new Map([
+ ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1],
+ ["bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 2],
+ ]),
+ { trailingComma: true },
+ )),
+ `Map(2) {
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" => 1,
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" => 2,
+}`,
+ );
+});
+
+Deno.test(function inspectCompact() {
+ assertEquals(
+ stripColor(Deno.inspect({ a: 1, b: 2 }, { compact: false })),
+ `{
+ a: 1,
+ b: 2
+}`,
+ );
+});
+
+Deno.test(function inspectIterableLimit() {
+ assertEquals(
+ stripColor(Deno.inspect(["a", "b", "c"], { iterableLimit: 2 })),
+ `[ "a", "b", ... 1 more item ]`,
+ );
+ assertEquals(
+ stripColor(Deno.inspect(new Set(["a", "b", "c"]), { iterableLimit: 2 })),
+ `Set(3) { "a", "b", ... 1 more item }`,
+ );
+ assertEquals(
+ stripColor(Deno.inspect(
+ new Map([
+ ["a", 1],
+ ["b", 2],
+ ["c", 3],
+ ]),
+ { iterableLimit: 2 },
+ )),
+ `Map(3) { "a" => 1, "b" => 2, ... 1 more item }`,
+ );
+});
+
+Deno.test(function inspectProxy() {
+ assertEquals(
+ stripColor(Deno.inspect(
+ new Proxy([1, 2, 3], {}),
+ )),
+ "[ 1, 2, 3 ]",
+ );
+ assertEquals(
+ stripColor(Deno.inspect(
+ new Proxy({ key: "value" }, {}),
+ )),
+ `{ key: "value" }`,
+ );
+ assertEquals(
+ stripColor(Deno.inspect(
+ new Proxy({}, {
+ get(_target, key) {
+ if (key === Symbol.toStringTag) {
+ return "MyProxy";
+ } else {
+ return 5;
+ }
+ },
+ getOwnPropertyDescriptor() {
+ return {
+ enumerable: true,
+ configurable: true,
+ value: 5,
+ };
+ },
+ ownKeys() {
+ return ["prop1", "prop2"];
+ },
+ }),
+ )),
+ `Object [MyProxy] { prop1: 5, prop2: 5 }`,
+ );
+ assertEquals(
+ stripColor(Deno.inspect(
+ new Proxy([1, 2, 3], { get() {} }),
+ { showProxy: true },
+ )),
+ "Proxy [ [ 1, 2, 3 ], { get: [Function: get] } ]",
+ );
+ assertEquals(
+ stripColor(Deno.inspect(
+ new Proxy({ a: 1 }, {
+ set(): boolean {
+ return false;
+ },
+ }),
+ { showProxy: true },
+ )),
+ "Proxy [ { a: 1 }, { set: [Function: set] } ]",
+ );
+ assertEquals(
+ stripColor(Deno.inspect(
+ new Proxy([1, 2, 3, 4, 5, 6, 7], { get() {} }),
+ { showProxy: true },
+ )),
+ `Proxy [
+ [
+ 1, 2, 3, 4,
+ 5, 6, 7
+ ],
+ { get: [Function: get] }
+]`,
+ );
+ assertEquals(
+ stripColor(Deno.inspect(
+ new Proxy(function fn() {}, { get() {} }),
+ { showProxy: true },
+ )),
+ "Proxy [ [Function: fn], { get: [Function: get] } ]",
+ );
+});
+
+Deno.test(function inspectError() {
+ const error1 = new Error("This is an error");
+ const error2 = new Error("This is an error", {
+ cause: new Error("This is a cause error"),
+ });
+
+ assertStringIncludes(
+ stripColor(Deno.inspect(error1)),
+ "Error: This is an error",
+ );
+ assertStringIncludes(
+ stripColor(Deno.inspect(error2)),
+ "Error: This is an error",
+ );
+ assertStringIncludes(
+ stripColor(Deno.inspect(error2)),
+ "Caused by Error: This is a cause error",
+ );
+});
+
+Deno.test(function inspectErrorCircular() {
+ const error1 = new Error("This is an error");
+ const error2 = new Error("This is an error", {
+ cause: new Error("This is a cause error"),
+ });
+ error1.cause = error1;
+ assert(error2.cause instanceof Error);
+ error2.cause.cause = error2;
+
+ assertStringIncludes(
+ stripColor(Deno.inspect(error1)),
+ "Error: This is an error",
+ );
+ assertStringIncludes(
+ stripColor(Deno.inspect(error2)),
+ "<ref *1> Error: This is an error",
+ );
+ assertStringIncludes(
+ stripColor(Deno.inspect(error2)),
+ "Caused by Error: This is a cause error",
+ );
+ assertStringIncludes(
+ stripColor(Deno.inspect(error2)),
+ "Caused by [Circular *1]",
+ );
+});
+
+Deno.test(function inspectColors() {
+ assertEquals(Deno.inspect(1), "1");
+ assertStringIncludes(Deno.inspect(1, { colors: true }), "\x1b[");
+});
+
+Deno.test(function inspectEmptyArray() {
+ const arr: string[] = [];
+
+ assertEquals(
+ Deno.inspect(arr, {
+ compact: false,
+ trailingComma: true,
+ }),
+ "[]",
+ );
+});
+
+Deno.test(function inspectDeepEmptyArray() {
+ const obj = {
+ arr: [],
+ };
+
+ assertEquals(
+ Deno.inspect(obj, {
+ compact: false,
+ trailingComma: true,
+ }),
+ `{
+ arr: [],
+}`,
+ );
+});
+
+Deno.test(function inspectEmptyMap() {
+ const map = new Map();
+
+ assertEquals(
+ Deno.inspect(map, {
+ compact: false,
+ trailingComma: true,
+ }),
+ "Map(0) {}",
+ );
+});
+
+Deno.test(function inspectEmptySet() {
+ const set = new Set();
+
+ assertEquals(
+ Deno.inspect(set, {
+ compact: false,
+ trailingComma: true,
+ }),
+ "Set(0) {}",
+ );
+});
+
+Deno.test(function inspectEmptyUint8Array() {
+ const typedArray = new Uint8Array(0);
+
+ assertEquals(
+ Deno.inspect(typedArray, {
+ compact: false,
+ trailingComma: true,
+ }),
+ "Uint8Array(0) []",
+ );
+});
+
+Deno.test(function inspectLargeArrayBuffer() {
+ const arrayBuffer = new ArrayBuffer(2 ** 32 + 1);
+ assertEquals(
+ Deno.inspect(arrayBuffer),
+ `ArrayBuffer {
+ [Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... 4294967197 more bytes>,
+ byteLength: 4294967297
+}`,
+ );
+ structuredClone(arrayBuffer, { transfer: [arrayBuffer] });
+ assertEquals(
+ Deno.inspect(arrayBuffer),
+ "ArrayBuffer { (detached), byteLength: 0 }",
+ );
+
+ const sharedArrayBuffer = new SharedArrayBuffer(2 ** 32 + 1);
+ assertEquals(
+ Deno.inspect(sharedArrayBuffer),
+ `SharedArrayBuffer {
+ [Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... 4294967197 more bytes>,
+ byteLength: 4294967297
+}`,
+ );
+});
+
+Deno.test(function inspectStringAbbreviation() {
+ const LONG_STRING =
+ "This is a really long string which will be abbreviated with ellipsis.";
+ const obj = {
+ str: LONG_STRING,
+ };
+ const arr = [LONG_STRING];
+
+ assertEquals(
+ Deno.inspect(obj, { strAbbreviateSize: 10 }),
+ '{ str: "This is a "... 59 more characters }',
+ );
+
+ assertEquals(
+ Deno.inspect(arr, { strAbbreviateSize: 10 }),
+ '[ "This is a "... 59 more characters ]',
+ );
+});
+
+Deno.test(async function inspectAggregateError() {
+ try {
+ await Promise.any([]);
+ } catch (err) {
+ assertEquals(
+ Deno.inspect(err).trimEnd(),
+ "AggregateError: All promises were rejected",
+ );
+ }
+});
+
+Deno.test(function inspectWithPrototypePollution() {
+ const originalExec = RegExp.prototype.exec;
+ try {
+ RegExp.prototype.exec = () => {
+ throw Error();
+ };
+ Deno.inspect("foo");
+ } finally {
+ RegExp.prototype.exec = originalExec;
+ }
+});
+
+Deno.test(function inspectPromiseLike() {
+ assertEquals(
+ Deno.inspect(Object.create(Promise.prototype)),
+ "Promise {}",
+ );
+});
+
+Deno.test(function inspectorMethods() {
+ console.timeStamp("test");
+ console.profile("test");
+ console.profileEnd("test");
+});
+
+Deno.test(function inspectQuotesOverride() {
+ assertEquals(
+ // @ts-ignore - 'quotes' is an internal option
+ Deno.inspect("foo", { quotes: ["'", '"', "`"] }),
+ "'foo'",
+ );
+ assertEquals(
+ // @ts-ignore - 'quotes' is an internal option
+ Deno.inspect("'foo'", { quotes: ["'", '"', "`"] }),
+ `"'foo'"`,
+ );
+});
+
+Deno.test(function inspectAnonymousFunctions() {
+ assertEquals(Deno.inspect(() => {}), "[Function (anonymous)]");
+ assertEquals(Deno.inspect(function () {}), "[Function (anonymous)]");
+ assertEquals(Deno.inspect(async () => {}), "[AsyncFunction (anonymous)]");
+ assertEquals(
+ Deno.inspect(async function () {}),
+ "[AsyncFunction (anonymous)]",
+ );
+ assertEquals(
+ Deno.inspect(function* () {}),
+ "[GeneratorFunction (anonymous)]",
+ );
+ assertEquals(
+ Deno.inspect(async function* () {}),
+ "[AsyncGeneratorFunction (anonymous)]",
+ );
+});
+
+Deno.test(function inspectBreakLengthOption() {
+ assertEquals(
+ Deno.inspect("123456789\n".repeat(3), { breakLength: 34 }),
+ `"123456789\\n123456789\\n123456789\\n"`,
+ );
+ assertEquals(
+ Deno.inspect("123456789\n".repeat(3), { breakLength: 33 }),
+ `"123456789\\n" +
+ "123456789\\n" +
+ "123456789\\n"`,
+ );
+});
+
+Deno.test(function inspectEscapeSequencesFalse() {
+ assertEquals(
+ Deno.inspect("foo\nbar", { escapeSequences: true }),
+ '"foo\\nbar"',
+ ); // default behavior
+ assertEquals(
+ Deno.inspect("foo\nbar", { escapeSequences: false }),
+ '"foo\nbar"',
+ );
+});
diff --git a/tests/unit/copy_file_test.ts b/tests/unit/copy_file_test.ts
new file mode 100644
index 000000000..ad467f510
--- /dev/null
+++ b/tests/unit/copy_file_test.ts
@@ -0,0 +1,249 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals, assertRejects, assertThrows } from "./test_util.ts";
+
+function readFileString(filename: string | URL): string {
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ return dec.decode(dataRead);
+}
+
+function writeFileString(filename: string | URL, s: string) {
+ const enc = new TextEncoder();
+ const data = enc.encode(s);
+ Deno.writeFileSync(filename, data, { mode: 0o666 });
+}
+
+function assertSameContent(
+ filename1: string | URL,
+ filename2: string | URL,
+) {
+ const data1 = Deno.readFileSync(filename1);
+ const data2 = Deno.readFileSync(filename2);
+ assertEquals(data1, data2);
+}
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function copyFileSyncSuccess() {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ writeFileString(fromFilename, "Hello world!");
+ Deno.copyFileSync(fromFilename, toFilename);
+ // No change to original file
+ assertEquals(readFileString(fromFilename), "Hello world!");
+ // Original == Dest
+ assertSameContent(fromFilename, toFilename);
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function copyFileSyncByUrl() {
+ const tempDir = Deno.makeTempDirSync();
+ const fromUrl = new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/from.txt`,
+ );
+ const toUrl = new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/to.txt`,
+ );
+ writeFileString(fromUrl, "Hello world!");
+ Deno.copyFileSync(fromUrl, toUrl);
+ // No change to original file
+ assertEquals(readFileString(fromUrl), "Hello world!");
+ // Original == Dest
+ assertSameContent(fromUrl, toUrl);
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ function copyFileSyncFailure() {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ // We skip initial writing here, from.txt does not exist
+ assertThrows(
+ () => {
+ Deno.copyFileSync(fromFilename, toFilename);
+ },
+ Deno.errors.NotFound,
+ `copy '${fromFilename}' -> '${toFilename}'`,
+ );
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: false } },
+ function copyFileSyncPerm1() {
+ assertThrows(() => {
+ Deno.copyFileSync("/from.txt", "/to.txt");
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { write: false, read: true } },
+ function copyFileSyncPerm2() {
+ assertThrows(() => {
+ Deno.copyFileSync("/from.txt", "/to.txt");
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function copyFileSyncOverwrite() {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ writeFileString(fromFilename, "Hello world!");
+ // Make Dest exist and have different content
+ writeFileString(toFilename, "Goodbye!");
+ Deno.copyFileSync(fromFilename, toFilename);
+ // No change to original file
+ assertEquals(readFileString(fromFilename), "Hello world!");
+ // Original == Dest
+ assertSameContent(fromFilename, toFilename);
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function copyFileSuccess() {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ writeFileString(fromFilename, "Hello world!");
+ await Deno.copyFile(fromFilename, toFilename);
+ // No change to original file
+ assertEquals(readFileString(fromFilename), "Hello world!");
+ // Original == Dest
+ assertSameContent(fromFilename, toFilename);
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function copyFileByUrl() {
+ const tempDir = Deno.makeTempDirSync();
+ const fromUrl = new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/from.txt`,
+ );
+ const toUrl = new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/to.txt`,
+ );
+ writeFileString(fromUrl, "Hello world!");
+ await Deno.copyFile(fromUrl, toUrl);
+ // No change to original file
+ assertEquals(readFileString(fromUrl), "Hello world!");
+ // Original == Dest
+ assertSameContent(fromUrl, toUrl);
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function copyFileFailure() {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ // We skip initial writing here, from.txt does not exist
+ await assertRejects(
+ async () => {
+ await Deno.copyFile(fromFilename, toFilename);
+ },
+ Deno.errors.NotFound,
+ `copy '${fromFilename}' -> '${toFilename}'`,
+ );
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function copyFileOverwrite() {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ writeFileString(fromFilename, "Hello world!");
+ // Make Dest exist and have different content
+ writeFileString(toFilename, "Goodbye!");
+ await Deno.copyFile(fromFilename, toFilename);
+ // No change to original file
+ assertEquals(readFileString(fromFilename), "Hello world!");
+ // Original == Dest
+ assertSameContent(fromFilename, toFilename);
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: false, write: true } },
+ async function copyFilePerm1() {
+ await assertRejects(async () => {
+ await Deno.copyFile("/from.txt", "/to.txt");
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: false } },
+ async function copyFilePerm2() {
+ await assertRejects(async () => {
+ await Deno.copyFile("/from.txt", "/to.txt");
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+function copyFileSyncMode(content: string): void {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ Deno.writeTextFileSync(fromFilename, content);
+ Deno.chmodSync(fromFilename, 0o100755);
+
+ Deno.copyFileSync(fromFilename, toFilename);
+ const toStat = Deno.statSync(toFilename);
+ assertEquals(toStat.mode!, 0o100755);
+}
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ function copyFileSyncChmod() {
+ // this Tests different optimization paths on MacOS:
+ //
+ // < 128 KB clonefile() w/ fallback to copyfile()
+ // > 128 KB
+ copyFileSyncMode("Hello world!");
+ copyFileSyncMode("Hello world!".repeat(128 * 1024));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function copyFileNulPath() {
+ const fromFilename = "from.txt\0";
+ const toFilename = "to.txt\0";
+ await assertRejects(async () => {
+ await Deno.copyFile(fromFilename, toFilename);
+ }, TypeError);
+ },
+);
diff --git a/tests/unit/cron_test.ts b/tests/unit/cron_test.ts
new file mode 100644
index 000000000..02573a898
--- /dev/null
+++ b/tests/unit/cron_test.ts
@@ -0,0 +1,460 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals, assertThrows } from "./test_util.ts";
+
+// @ts-ignore This is not publicly typed namespace, but it's there for sure.
+const {
+ formatToCronSchedule,
+ parseScheduleToString,
+ // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
+} = Deno[Deno.internal];
+
+const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
+
+Deno.test(function noNameTest() {
+ assertThrows(
+ // @ts-ignore test
+ () => Deno.cron(),
+ TypeError,
+ "Deno.cron requires a unique name",
+ );
+});
+
+Deno.test(function noSchedule() {
+ assertThrows(
+ // @ts-ignore test
+ () => Deno.cron("foo"),
+ TypeError,
+ "Deno.cron requires a valid schedule",
+ );
+});
+
+Deno.test(function noHandler() {
+ assertThrows(
+ // @ts-ignore test
+ () => Deno.cron("foo", "*/1 * * * *"),
+ TypeError,
+ "Deno.cron requires a handler",
+ );
+});
+
+Deno.test(function invalidNameTest() {
+ assertThrows(
+ () => Deno.cron("abc[]", "*/1 * * * *", () => {}),
+ TypeError,
+ "Invalid cron name",
+ );
+ assertThrows(
+ () => Deno.cron("a**bc", "*/1 * * * *", () => {}),
+ TypeError,
+ "Invalid cron name",
+ );
+ assertThrows(
+ () => Deno.cron("abc<>", "*/1 * * * *", () => {}),
+ TypeError,
+ "Invalid cron name",
+ );
+ assertThrows(
+ () => Deno.cron(";']", "*/1 * * * *", () => {}),
+ TypeError,
+ "Invalid cron name",
+ );
+ assertThrows(
+ () =>
+ Deno.cron(
+ "0000000000000000000000000000000000000000000000000000000000000000000000",
+ "*/1 * * * *",
+ () => {},
+ ),
+ TypeError,
+ "Cron name is too long",
+ );
+});
+
+Deno.test(function invalidScheduleTest() {
+ assertThrows(
+ () => Deno.cron("abc", "bogus", () => {}),
+ TypeError,
+ "Invalid cron schedule",
+ );
+ assertThrows(
+ () => Deno.cron("abc", "* * * * * *", () => {}),
+ TypeError,
+ "Invalid cron schedule",
+ );
+ assertThrows(
+ () => Deno.cron("abc", "* * * *", () => {}),
+ TypeError,
+ "Invalid cron schedule",
+ );
+ assertThrows(
+ () => Deno.cron("abc", "m * * * *", () => {}),
+ TypeError,
+ "Invalid cron schedule",
+ );
+});
+
+Deno.test(function invalidBackoffScheduleTest() {
+ assertThrows(
+ () =>
+ Deno.cron(
+ "abc",
+ "*/1 * * * *",
+ { backoffSchedule: [1, 1, 1, 1, 1, 1] },
+ () => {},
+ ),
+ TypeError,
+ "Invalid backoff schedule",
+ );
+ assertThrows(
+ () =>
+ Deno.cron("abc", "*/1 * * * *", { backoffSchedule: [3600001] }, () => {}),
+ TypeError,
+ "Invalid backoff schedule",
+ );
+});
+
+Deno.test(async function tooManyCrons() {
+ const crons: Promise<void>[] = [];
+ const ac = new AbortController();
+ for (let i = 0; i <= 100; i++) {
+ const c = Deno.cron(
+ `abc_${i}`,
+ "*/1 * * * *",
+ { signal: ac.signal },
+ () => {},
+ );
+ crons.push(c);
+ }
+
+ try {
+ assertThrows(
+ () => {
+ Deno.cron("next-cron", "*/1 * * * *", { signal: ac.signal }, () => {});
+ },
+ TypeError,
+ "Too many crons",
+ );
+ } finally {
+ ac.abort();
+ for (const c of crons) {
+ await c;
+ }
+ }
+});
+
+Deno.test(async function duplicateCrons() {
+ const ac = new AbortController();
+ const c = Deno.cron("abc", "*/20 * * * *", { signal: ac.signal }, () => {});
+ try {
+ assertThrows(
+ () => Deno.cron("abc", "*/20 * * * *", () => {}),
+ TypeError,
+ "Cron with this name already exists",
+ );
+ } finally {
+ ac.abort();
+ await c;
+ }
+});
+
+Deno.test(async function basicTest() {
+ Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "100");
+
+ let count = 0;
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const ac = new AbortController();
+ const c = Deno.cron("abc", "*/20 * * * *", { signal: ac.signal }, () => {
+ count++;
+ if (count > 5) {
+ resolve();
+ }
+ });
+ try {
+ await promise;
+ } finally {
+ ac.abort();
+ await c;
+ }
+});
+
+Deno.test(async function basicTestWithJsonFormatScheduleExpression() {
+ Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "100");
+
+ let count = 0;
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const ac = new AbortController();
+ const c = Deno.cron(
+ "abc",
+ { minute: { every: 20 } },
+ { signal: ac.signal },
+ () => {
+ count++;
+ if (count > 5) {
+ resolve();
+ }
+ },
+ );
+ try {
+ await promise;
+ } finally {
+ ac.abort();
+ await c;
+ }
+});
+
+Deno.test(async function multipleCrons() {
+ Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "100");
+
+ let count0 = 0;
+ let count1 = 0;
+ const { promise: promise0, resolve: resolve0 } = Promise.withResolvers<
+ void
+ >();
+ const { promise: promise1, resolve: resolve1 } = Promise.withResolvers<
+ void
+ >();
+ const ac = new AbortController();
+ const c0 = Deno.cron("abc", "*/20 * * * *", { signal: ac.signal }, () => {
+ count0++;
+ if (count0 > 5) {
+ resolve0();
+ }
+ });
+ const c1 = Deno.cron("xyz", "*/20 * * * *", { signal: ac.signal }, () => {
+ count1++;
+ if (count1 > 5) {
+ resolve1();
+ }
+ });
+ try {
+ await promise0;
+ await promise1;
+ } finally {
+ ac.abort();
+ await c0;
+ await c1;
+ }
+});
+
+Deno.test(async function overlappingExecutions() {
+ Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "100");
+
+ let count = 0;
+ const { promise: promise0, resolve: resolve0 } = Promise.withResolvers<
+ void
+ >();
+ const { promise: promise1, resolve: resolve1 } = Promise.withResolvers<
+ void
+ >();
+ const ac = new AbortController();
+ const c = Deno.cron(
+ "abc",
+ "*/20 * * * *",
+ { signal: ac.signal },
+ async () => {
+ resolve0();
+ count++;
+ await promise1;
+ },
+ );
+ try {
+ await promise0;
+ } finally {
+ await sleep(2000);
+ resolve1();
+ ac.abort();
+ await c;
+ }
+ assertEquals(count, 1);
+});
+
+Deno.test(async function retriesWithBackoffSchedule() {
+ Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "5000");
+
+ let count = 0;
+ const ac = new AbortController();
+ const c = Deno.cron("abc", "*/20 * * * *", {
+ signal: ac.signal,
+ backoffSchedule: [10, 20],
+ }, async () => {
+ count += 1;
+ await sleep(10);
+ throw new TypeError("cron error");
+ });
+ try {
+ await sleep(6000);
+ } finally {
+ ac.abort();
+ await c;
+ }
+
+ // The cron should have executed 3 times (1st attempt and 2 retries).
+ assertEquals(count, 3);
+});
+
+Deno.test(async function retriesWithBackoffScheduleOldApi() {
+ Deno.env.set("DENO_CRON_TEST_SCHEDULE_OFFSET", "5000");
+
+ let count = 0;
+ const ac = new AbortController();
+ const c = Deno.cron("abc2", "*/20 * * * *", {
+ signal: ac.signal,
+ backoffSchedule: [10, 20],
+ }, async () => {
+ count += 1;
+ await sleep(10);
+ throw new TypeError("cron error");
+ });
+
+ try {
+ await sleep(6000);
+ } finally {
+ ac.abort();
+ await c;
+ }
+
+ // The cron should have executed 3 times (1st attempt and 2 retries).
+ assertEquals(count, 3);
+});
+
+Deno.test("formatToCronSchedule - undefined value", () => {
+ const result = formatToCronSchedule();
+ assertEquals(result, "*");
+});
+
+Deno.test("formatToCronSchedule - number value", () => {
+ const result = formatToCronSchedule(5);
+ assertEquals(result, "5");
+});
+
+Deno.test("formatToCronSchedule - exact array value", () => {
+ const result = formatToCronSchedule({ exact: [1, 2, 3] });
+ assertEquals(result, "1,2,3");
+});
+
+Deno.test("formatToCronSchedule - exact number value", () => {
+ const result = formatToCronSchedule({ exact: 5 });
+ assertEquals(result, "5");
+});
+
+Deno.test("formatToCronSchedule - start, end, every values", () => {
+ const result = formatToCronSchedule({ start: 1, end: 10, every: 2 });
+ assertEquals(result, "1-10/2");
+});
+
+Deno.test("formatToCronSchedule - start, end values", () => {
+ const result = formatToCronSchedule({ start: 1, end: 10 });
+ assertEquals(result, "1-10");
+});
+
+Deno.test("formatToCronSchedule - start, every values", () => {
+ const result = formatToCronSchedule({ start: 1, every: 2 });
+ assertEquals(result, "1/2");
+});
+
+Deno.test("formatToCronSchedule - start value", () => {
+ const result = formatToCronSchedule({ start: 1 });
+ assertEquals(result, "1/1");
+});
+
+Deno.test("formatToCronSchedule - end, every values", () => {
+ assertThrows(
+ () => formatToCronSchedule({ end: 10, every: 2 }),
+ TypeError,
+ "Invalid cron schedule",
+ );
+});
+
+Deno.test("Parse CronSchedule to string", () => {
+ const result = parseScheduleToString({
+ minute: { exact: [1, 2, 3] },
+ hour: { start: 1, end: 10, every: 2 },
+ dayOfMonth: { exact: 5 },
+ month: { start: 1, end: 10 },
+ dayOfWeek: { start: 1, every: 2 },
+ });
+ assertEquals(result, "1,2,3 1-10/2 5 1-10 1/2");
+});
+
+Deno.test("Parse schedule to string - string", () => {
+ const result = parseScheduleToString("* * * * *");
+ assertEquals(result, "* * * * *");
+});
+
+Deno.test("error on two handlers", () => {
+ assertThrows(
+ () => {
+ // @ts-ignore test
+ Deno.cron("abc", "* * * * *", () => {}, () => {});
+ },
+ TypeError,
+ "Deno.cron requires a single handler",
+ );
+});
+
+Deno.test("Parse test", () => {
+ assertEquals(
+ parseScheduleToString({
+ minute: 3,
+ }),
+ "3 * * * *",
+ );
+ assertEquals(
+ parseScheduleToString({
+ hour: { every: 2 },
+ }),
+ "0 */2 * * *",
+ );
+ assertEquals(
+ parseScheduleToString({
+ dayOfMonth: { every: 10 },
+ }),
+ "0 0 */10 * *",
+ );
+ assertEquals(
+ parseScheduleToString({
+ month: { every: 3 },
+ }),
+ "0 0 1 */3 *",
+ );
+ assertEquals(
+ parseScheduleToString({
+ dayOfWeek: { every: 2 },
+ }),
+ "0 0 * * */2",
+ );
+ assertEquals(
+ parseScheduleToString({
+ minute: 3,
+ hour: { every: 2 },
+ }),
+ "3 */2 * * *",
+ );
+ assertEquals(
+ parseScheduleToString({
+ dayOfMonth: { start: 1, end: 10 },
+ }),
+ "0 0 1-10 * *",
+ );
+ assertEquals(
+ parseScheduleToString({
+ minute: { every: 10 },
+ dayOfMonth: { every: 5 },
+ }),
+ "*/10 * */5 * *",
+ );
+ assertEquals(
+ parseScheduleToString({
+ hour: { every: 3 },
+ month: { every: 2 },
+ }),
+ "0 */3 * */2 *",
+ );
+ assertEquals(
+ parseScheduleToString({
+ minute: { every: 5 },
+ month: { every: 2 },
+ }),
+ "*/5 * * */2 *",
+ );
+});
diff --git a/tests/unit/custom_event_test.ts b/tests/unit/custom_event_test.ts
new file mode 100644
index 000000000..b72084eb2
--- /dev/null
+++ b/tests/unit/custom_event_test.ts
@@ -0,0 +1,27 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+Deno.test(function customEventInitializedWithDetail() {
+ const type = "touchstart";
+ const detail = { message: "hello" };
+ const customEventInit = {
+ bubbles: true,
+ cancelable: true,
+ detail,
+ } as CustomEventInit;
+ const event = new CustomEvent(type, customEventInit);
+
+ assertEquals(event.bubbles, true);
+ assertEquals(event.cancelable, true);
+ assertEquals(event.currentTarget, null);
+ assertEquals(event.detail, detail);
+ assertEquals(event.isTrusted, false);
+ assertEquals(event.target, null);
+ assertEquals(event.type, type);
+});
+
+Deno.test(function toStringShouldBeWebCompatibility() {
+ const type = "touchstart";
+ const event = new CustomEvent(type, {});
+ assertEquals(event.toString(), "[object CustomEvent]");
+});
diff --git a/tests/unit/dir_test.ts b/tests/unit/dir_test.ts
new file mode 100644
index 000000000..4aaadfb12
--- /dev/null
+++ b/tests/unit/dir_test.ts
@@ -0,0 +1,63 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals, assertThrows } from "./test_util.ts";
+
+Deno.test({ permissions: { read: true } }, function dirCwdNotNull() {
+ assert(Deno.cwd() != null);
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function dirCwdChdirSuccess() {
+ const initialdir = Deno.cwd();
+ const path = Deno.makeTempDirSync();
+ Deno.chdir(path);
+ const current = Deno.cwd();
+ if (Deno.build.os === "darwin") {
+ assertEquals(current, "/private" + path);
+ } else {
+ assertEquals(current, path);
+ }
+ Deno.chdir(initialdir);
+ },
+);
+
+Deno.test({ permissions: { read: true, write: true } }, function dirCwdError() {
+ // excluding windows since it throws resource busy, while removeSync
+ if (["linux", "darwin"].includes(Deno.build.os)) {
+ const initialdir = Deno.cwd();
+ const path = Deno.makeTempDirSync();
+ Deno.chdir(path);
+ Deno.removeSync(path);
+ try {
+ assertThrows(() => {
+ Deno.cwd();
+ }, Deno.errors.NotFound);
+ } finally {
+ Deno.chdir(initialdir);
+ }
+ }
+});
+
+Deno.test({ permissions: { read: false } }, function dirCwdPermError() {
+ assertThrows(
+ () => {
+ Deno.cwd();
+ },
+ Deno.errors.PermissionDenied,
+ "Requires read access to <CWD>, run again with the --allow-read flag",
+ );
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function dirChdirError() {
+ const path = Deno.makeTempDirSync() + "test";
+ assertThrows(
+ () => {
+ Deno.chdir(path);
+ },
+ Deno.errors.NotFound,
+ `chdir '${path}'`,
+ );
+ },
+);
diff --git a/tests/unit/dom_exception_test.ts b/tests/unit/dom_exception_test.ts
new file mode 100644
index 000000000..de335e105
--- /dev/null
+++ b/tests/unit/dom_exception_test.ts
@@ -0,0 +1,23 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import {
+ assertEquals,
+ assertNotEquals,
+ assertStringIncludes,
+} from "./test_util.ts";
+
+Deno.test(function customInspectFunction() {
+ const exception = new DOMException("test");
+ assertEquals(Deno.inspect(exception), exception.stack);
+ assertStringIncludes(Deno.inspect(DOMException.prototype), "DOMException");
+});
+
+Deno.test(function nameToCodeMappingPrototypeAccess() {
+ const newCode = 100;
+ const objectPrototype = Object.prototype as unknown as {
+ pollution: number;
+ };
+ objectPrototype.pollution = newCode;
+ assertNotEquals(newCode, new DOMException("test", "pollution").code);
+ Reflect.deleteProperty(objectPrototype, "pollution");
+});
diff --git a/tests/unit/error_stack_test.ts b/tests/unit/error_stack_test.ts
new file mode 100644
index 000000000..7188b9f53
--- /dev/null
+++ b/tests/unit/error_stack_test.ts
@@ -0,0 +1,54 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals, assertMatch } from "./test_util.ts";
+
+Deno.test(function errorStackMessageLine() {
+ const e1 = new Error();
+ e1.name = "Foo";
+ e1.message = "bar";
+ assertMatch(e1.stack!, /^Foo: bar\n/);
+
+ const e2 = new Error();
+ e2.name = "";
+ e2.message = "bar";
+ assertMatch(e2.stack!, /^bar\n/);
+
+ const e3 = new Error();
+ e3.name = "Foo";
+ e3.message = "";
+ assertMatch(e3.stack!, /^Foo\n/);
+
+ const e4 = new Error();
+ e4.name = "";
+ e4.message = "";
+ assertMatch(e4.stack!, /^\n/);
+
+ const e5 = new Error();
+ // deno-lint-ignore ban-ts-comment
+ // @ts-expect-error
+ e5.name = undefined;
+ // deno-lint-ignore ban-ts-comment
+ // @ts-expect-error
+ e5.message = undefined;
+ assertMatch(e5.stack!, /^Error\n/);
+
+ const e6 = new Error();
+ // deno-lint-ignore ban-ts-comment
+ // @ts-expect-error
+ e6.name = null;
+ // deno-lint-ignore ban-ts-comment
+ // @ts-expect-error
+ e6.message = null;
+ assertMatch(e6.stack!, /^null: null\n/);
+});
+
+Deno.test(function captureStackTrace() {
+ function foo() {
+ const error = new Error();
+ const stack1 = error.stack!;
+ Error.captureStackTrace(error, foo);
+ const stack2 = error.stack!;
+ // stack2 should be stack1 without the first frame.
+ assertEquals(stack2, stack1.replace(/(?<=^[^\n]*\n)[^\n]*\n/, ""));
+ }
+ foo();
+});
diff --git a/tests/unit/error_test.ts b/tests/unit/error_test.ts
new file mode 100644
index 000000000..9ba09ce0d
--- /dev/null
+++ b/tests/unit/error_test.ts
@@ -0,0 +1,33 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertThrows, fail } from "./test_util.ts";
+
+Deno.test("Errors work", () => {
+ assert(new Deno.errors.NotFound("msg") instanceof Error);
+ assert(new Deno.errors.PermissionDenied("msg") instanceof Error);
+ assert(new Deno.errors.ConnectionRefused("msg") instanceof Error);
+ assert(new Deno.errors.ConnectionReset("msg") instanceof Error);
+ assert(new Deno.errors.ConnectionAborted("msg") instanceof Error);
+ assert(new Deno.errors.NotConnected("msg") instanceof Error);
+ assert(new Deno.errors.AddrInUse("msg") instanceof Error);
+ assert(new Deno.errors.AddrNotAvailable("msg") instanceof Error);
+ assert(new Deno.errors.BrokenPipe("msg") instanceof Error);
+ assert(new Deno.errors.AlreadyExists("msg") instanceof Error);
+ assert(new Deno.errors.InvalidData("msg") instanceof Error);
+ assert(new Deno.errors.TimedOut("msg") instanceof Error);
+ assert(new Deno.errors.Interrupted("msg") instanceof Error);
+ assert(new Deno.errors.WouldBlock("msg") instanceof Error);
+ assert(new Deno.errors.WriteZero("msg") instanceof Error);
+ assert(new Deno.errors.UnexpectedEof("msg") instanceof Error);
+ assert(new Deno.errors.BadResource("msg") instanceof Error);
+ assert(new Deno.errors.Http("msg") instanceof Error);
+ assert(new Deno.errors.Busy("msg") instanceof Error);
+ assert(new Deno.errors.NotSupported("msg") instanceof Error);
+});
+
+Deno.test("Errors have some tamper resistance", () => {
+ // deno-lint-ignore no-explicit-any
+ (Object.prototype as any).get = () => {};
+ assertThrows(() => fail("test error"), Error, "test error");
+ // deno-lint-ignore no-explicit-any
+ delete (Object.prototype as any).get;
+});
diff --git a/tests/unit/esnext_test.ts b/tests/unit/esnext_test.ts
new file mode 100644
index 000000000..6b2334f42
--- /dev/null
+++ b/tests/unit/esnext_test.ts
@@ -0,0 +1,43 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+// TODO(@kitsonk) remove when we are no longer patching TypeScript to have
+// these types available.
+
+Deno.test(function typeCheckingEsNextArrayString() {
+ const b = ["a", "b", "c", "d", "e", "f"];
+ assertEquals(b.findLast((val) => typeof val === "string"), "f");
+ assertEquals(b.findLastIndex((val) => typeof val === "string"), 5);
+});
+
+Deno.test(function intlListFormat() {
+ const formatter = new Intl.ListFormat("en", {
+ style: "long",
+ type: "conjunction",
+ });
+ assertEquals(
+ formatter.format(["red", "green", "blue"]),
+ "red, green, and blue",
+ );
+
+ const formatter2 = new Intl.ListFormat("en", {
+ style: "short",
+ type: "disjunction",
+ });
+ assertEquals(formatter2.formatToParts(["Rust", "golang"]), [
+ { type: "element", value: "Rust" },
+ { type: "literal", value: " or " },
+ { type: "element", value: "golang" },
+ ]);
+
+ // Works with iterables as well
+ assertEquals(
+ formatter.format(new Set(["red", "green", "blue"])),
+ "red, green, and blue",
+ );
+ assertEquals(formatter2.formatToParts(new Set(["Rust", "golang"])), [
+ { type: "element", value: "Rust" },
+ { type: "literal", value: " or " },
+ { type: "element", value: "golang" },
+ ]);
+});
diff --git a/tests/unit/event_target_test.ts b/tests/unit/event_target_test.ts
new file mode 100644
index 000000000..b084eaf90
--- /dev/null
+++ b/tests/unit/event_target_test.ts
@@ -0,0 +1,295 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+// deno-lint-ignore-file no-window-prefix
+import { assertEquals, assertThrows } from "./test_util.ts";
+
+Deno.test(function addEventListenerTest() {
+ const document = new EventTarget();
+
+ assertEquals(document.addEventListener("x", null, false), undefined);
+ assertEquals(document.addEventListener("x", null, true), undefined);
+ assertEquals(document.addEventListener("x", null), undefined);
+});
+
+Deno.test(function constructedEventTargetCanBeUsedAsExpected() {
+ const target = new EventTarget();
+ const event = new Event("foo", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ const listener = (e: Event) => {
+ assertEquals(e, event);
+ ++callCount;
+ };
+
+ target.addEventListener("foo", listener);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 1);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+
+ target.removeEventListener("foo", listener);
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+});
+
+Deno.test(function anEventTargetCanBeSubclassed() {
+ class NicerEventTarget extends EventTarget {
+ on(
+ type: string,
+ callback: ((e: Event) => void) | null,
+ options?: AddEventListenerOptions,
+ ) {
+ this.addEventListener(type, callback, options);
+ }
+
+ off(
+ type: string,
+ callback: ((e: Event) => void) | null,
+ options?: EventListenerOptions,
+ ) {
+ this.removeEventListener(type, callback, options);
+ }
+ }
+
+ const target = new NicerEventTarget();
+ new Event("foo", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ const listener = () => {
+ ++callCount;
+ };
+
+ target.on("foo", listener);
+ assertEquals(callCount, 0);
+
+ target.off("foo", listener);
+ assertEquals(callCount, 0);
+});
+
+Deno.test(function removingNullEventListenerShouldSucceed() {
+ const document = new EventTarget();
+ assertEquals(document.removeEventListener("x", null, false), undefined);
+ assertEquals(document.removeEventListener("x", null, true), undefined);
+ assertEquals(document.removeEventListener("x", null), undefined);
+});
+
+Deno.test(function constructedEventTargetUseObjectPrototype() {
+ const target = new EventTarget();
+ const event = new Event("toString", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ const listener = (e: Event) => {
+ assertEquals(e, event);
+ ++callCount;
+ };
+
+ target.addEventListener("toString", listener);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 1);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+
+ target.removeEventListener("toString", listener);
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+});
+
+Deno.test(function toStringShouldBeWebCompatible() {
+ const target = new EventTarget();
+ assertEquals(target.toString(), "[object EventTarget]");
+});
+
+Deno.test(function dispatchEventShouldNotThrowError() {
+ let hasThrown = false;
+
+ try {
+ const target = new EventTarget();
+ const event = new Event("hasOwnProperty", {
+ bubbles: true,
+ cancelable: false,
+ });
+ const listener = () => {};
+ target.addEventListener("hasOwnProperty", listener);
+ target.dispatchEvent(event);
+ } catch {
+ hasThrown = true;
+ }
+
+ assertEquals(hasThrown, false);
+});
+
+Deno.test(function eventTargetThisShouldDefaultToWindow() {
+ const {
+ addEventListener,
+ dispatchEvent,
+ removeEventListener,
+ } = EventTarget.prototype;
+ let n = 1;
+ const event = new Event("hello");
+ const listener = () => {
+ n = 2;
+ };
+
+ addEventListener("hello", listener);
+ window.dispatchEvent(event);
+ assertEquals(n, 2);
+ n = 1;
+ removeEventListener("hello", listener);
+ window.dispatchEvent(event);
+ assertEquals(n, 1);
+
+ window.addEventListener("hello", listener);
+ dispatchEvent(event);
+ assertEquals(n, 2);
+ n = 1;
+ window.removeEventListener("hello", listener);
+ dispatchEvent(event);
+ assertEquals(n, 1);
+});
+
+Deno.test(function eventTargetShouldAcceptEventListenerObject() {
+ const target = new EventTarget();
+ const event = new Event("foo", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ const listener = {
+ handleEvent(e: Event) {
+ assertEquals(e, event);
+ ++callCount;
+ },
+ };
+
+ target.addEventListener("foo", listener);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 1);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+
+ target.removeEventListener("foo", listener);
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+});
+
+Deno.test(function eventTargetShouldAcceptAsyncFunction() {
+ const target = new EventTarget();
+ const event = new Event("foo", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ const listener = (e: Event) => {
+ assertEquals(e, event);
+ ++callCount;
+ };
+
+ target.addEventListener("foo", listener);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 1);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+
+ target.removeEventListener("foo", listener);
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+});
+
+Deno.test(
+ function eventTargetShouldAcceptAsyncFunctionForEventListenerObject() {
+ const target = new EventTarget();
+ const event = new Event("foo", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ const listener = {
+ handleEvent(e: Event) {
+ assertEquals(e, event);
+ ++callCount;
+ },
+ };
+
+ target.addEventListener("foo", listener);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 1);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+
+ target.removeEventListener("foo", listener);
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+ },
+);
+Deno.test(function eventTargetDispatchShouldSetTargetNoListener() {
+ const target = new EventTarget();
+ const event = new Event("foo");
+ assertEquals(event.target, null);
+ target.dispatchEvent(event);
+ assertEquals(event.target, target);
+});
+
+Deno.test(function eventTargetDispatchShouldSetTargetInListener() {
+ const target = new EventTarget();
+ const event = new Event("foo");
+ assertEquals(event.target, null);
+ let called = false;
+ target.addEventListener("foo", (e) => {
+ assertEquals(e.target, target);
+ called = true;
+ });
+ target.dispatchEvent(event);
+ assertEquals(called, true);
+});
+
+Deno.test(function eventTargetDispatchShouldFireCurrentListenersOnly() {
+ const target = new EventTarget();
+ const event = new Event("foo");
+ let callCount = 0;
+ target.addEventListener("foo", () => {
+ ++callCount;
+ target.addEventListener("foo", () => {
+ ++callCount;
+ });
+ });
+ target.dispatchEvent(event);
+ assertEquals(callCount, 1);
+});
+
+Deno.test(function eventTargetAddEventListenerGlobalAbort() {
+ return new Promise((resolve) => {
+ const c = new AbortController();
+
+ c.signal.addEventListener("abort", () => resolve());
+ addEventListener("test", () => {}, { signal: c.signal });
+ c.abort();
+ });
+});
+
+Deno.test(function eventTargetBrandChecking() {
+ const self = {};
+
+ assertThrows(
+ () => {
+ EventTarget.prototype.addEventListener.call(self, "test", null);
+ },
+ TypeError,
+ );
+
+ assertThrows(
+ () => {
+ EventTarget.prototype.removeEventListener.call(self, "test", null);
+ },
+ TypeError,
+ );
+
+ assertThrows(
+ () => {
+ EventTarget.prototype.dispatchEvent.call(self, new Event("test"));
+ },
+ TypeError,
+ );
+});
diff --git a/tests/unit/event_test.ts b/tests/unit/event_test.ts
new file mode 100644
index 000000000..c82873cf6
--- /dev/null
+++ b/tests/unit/event_test.ts
@@ -0,0 +1,143 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals, assertStringIncludes } from "./test_util.ts";
+
+Deno.test(function eventInitializedWithType() {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEquals(event.isTrusted, false);
+ assertEquals(event.target, null);
+ assertEquals(event.currentTarget, null);
+ assertEquals(event.type, "click");
+ assertEquals(event.bubbles, false);
+ assertEquals(event.cancelable, false);
+});
+
+Deno.test(function eventInitializedWithTypeAndDict() {
+ const init = "submit";
+ const eventInit = { bubbles: true, cancelable: true } as EventInit;
+ const event = new Event(init, eventInit);
+
+ assertEquals(event.isTrusted, false);
+ assertEquals(event.target, null);
+ assertEquals(event.currentTarget, null);
+ assertEquals(event.type, "submit");
+ assertEquals(event.bubbles, true);
+ assertEquals(event.cancelable, true);
+});
+
+Deno.test(function eventComposedPathSuccess() {
+ const type = "click";
+ const event = new Event(type);
+ const composedPath = event.composedPath();
+
+ assertEquals(composedPath, []);
+});
+
+Deno.test(function eventStopPropagationSuccess() {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEquals(event.cancelBubble, false);
+ event.stopPropagation();
+ assertEquals(event.cancelBubble, true);
+});
+
+Deno.test(function eventStopImmediatePropagationSuccess() {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEquals(event.cancelBubble, false);
+ event.stopImmediatePropagation();
+ assertEquals(event.cancelBubble, true);
+});
+
+Deno.test(function eventPreventDefaultSuccess() {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEquals(event.defaultPrevented, false);
+ event.preventDefault();
+ assertEquals(event.defaultPrevented, false);
+
+ const eventInit = { bubbles: true, cancelable: true } as EventInit;
+ const cancelableEvent = new Event(type, eventInit);
+ assertEquals(cancelableEvent.defaultPrevented, false);
+ cancelableEvent.preventDefault();
+ assertEquals(cancelableEvent.defaultPrevented, true);
+});
+
+Deno.test(function eventInitializedWithNonStringType() {
+ // deno-lint-ignore no-explicit-any
+ const type: any = undefined;
+ const event = new Event(type);
+
+ assertEquals(event.isTrusted, false);
+ assertEquals(event.target, null);
+ assertEquals(event.currentTarget, null);
+ assertEquals(event.type, "undefined");
+ assertEquals(event.bubbles, false);
+ assertEquals(event.cancelable, false);
+});
+
+Deno.test(function eventInspectOutput() {
+ // deno-lint-ignore no-explicit-any
+ const cases: Array<[any, (event: any) => string]> = [
+ [
+ new Event("test"),
+ (event: Event) =>
+ `Event {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n srcElement: null,\n target: null,\n returnValue: true,\n timeStamp: ${event.timeStamp},\n type: "test"\n}`,
+ ],
+ [
+ new ErrorEvent("error"),
+ (event: Event) =>
+ `ErrorEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n srcElement: null,\n target: null,\n returnValue: true,\n timeStamp: ${event.timeStamp},\n type: "error",\n message: "",\n filename: "",\n lineno: 0,\n colno: 0,\n error: undefined\n}`,
+ ],
+ [
+ new CloseEvent("close"),
+ (event: Event) =>
+ `CloseEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n srcElement: null,\n target: null,\n returnValue: true,\n timeStamp: ${event.timeStamp},\n type: "close",\n wasClean: false,\n code: 0,\n reason: ""\n}`,
+ ],
+ [
+ new CustomEvent("custom"),
+ (event: Event) =>
+ `CustomEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n srcElement: null,\n target: null,\n returnValue: true,\n timeStamp: ${event.timeStamp},\n type: "custom",\n detail: undefined\n}`,
+ ],
+ [
+ new ProgressEvent("progress"),
+ (event: Event) =>
+ `ProgressEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n srcElement: null,\n target: null,\n returnValue: true,\n timeStamp: ${event.timeStamp},\n type: "progress",\n lengthComputable: false,\n loaded: 0,\n total: 0\n}`,
+ ],
+ ];
+
+ for (const [event, outputProvider] of cases) {
+ assertEquals(Deno.inspect(event), outputProvider(event));
+ }
+});
+
+Deno.test(function inspectEvent() {
+ // has a customInspect implementation that previously would throw on a getter
+ assertEquals(
+ Deno.inspect(Event.prototype),
+ `Event {
+ bubbles: [Getter],
+ cancelable: [Getter],
+ composed: [Getter],
+ currentTarget: [Getter],
+ defaultPrevented: [Getter],
+ eventPhase: [Getter],
+ srcElement: [Getter/Setter],
+ target: [Getter],
+ returnValue: [Getter/Setter],
+ timeStamp: [Getter],
+ type: [Getter]
+}`,
+ );
+
+ // ensure this still works
+ assertStringIncludes(
+ Deno.inspect(new Event("test")),
+ // check a substring because one property is a timestamp
+ `Event {\n bubbles: false,\n cancelable: false,`,
+ );
+});
diff --git a/tests/unit/fetch_test.ts b/tests/unit/fetch_test.ts
new file mode 100644
index 000000000..80837a456
--- /dev/null
+++ b/tests/unit/fetch_test.ts
@@ -0,0 +1,2071 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+ delay,
+ fail,
+ unimplemented,
+} from "./test_util.ts";
+import { Buffer } from "@test_util/std/io/buffer.ts";
+
+const listenPort = 4506;
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchRequiresOneArgument() {
+ await assertRejects(
+ fetch as unknown as () => Promise<void>,
+ TypeError,
+ );
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function fetchProtocolError() {
+ await assertRejects(
+ async () => {
+ await fetch("ftp://localhost:21/a/file");
+ },
+ TypeError,
+ "not supported",
+ );
+});
+
+function findClosedPortInRange(
+ minPort: number,
+ maxPort: number,
+): number | never {
+ let port = minPort;
+
+ // If we hit the return statement of this loop
+ // that means that we did not throw an
+ // AddrInUse error when we executed Deno.listen.
+ while (port < maxPort) {
+ try {
+ const listener = Deno.listen({ port });
+ listener.close();
+ return port;
+ } catch (_e) {
+ port++;
+ }
+ }
+
+ unimplemented(
+ `No available ports between ${minPort} and ${maxPort} to test fetch`,
+ );
+}
+
+Deno.test(
+ // TODO(bartlomieju): reenable this test
+ // https://github.com/denoland/deno/issues/18350
+ { ignore: Deno.build.os === "windows", permissions: { net: true } },
+ async function fetchConnectionError() {
+ const port = findClosedPortInRange(4000, 9999);
+ await assertRejects(
+ async () => {
+ await fetch(`http://localhost:${port}`);
+ },
+ TypeError,
+ "error trying to connect",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchDnsError() {
+ await assertRejects(
+ async () => {
+ await fetch("http://nil/");
+ },
+ TypeError,
+ "error trying to connect",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInvalidUriError() {
+ await assertRejects(
+ async () => {
+ await fetch("http://<invalid>/");
+ },
+ TypeError,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchMalformedUriError() {
+ await assertRejects(
+ async () => {
+ const url = new URL("http://{{google/");
+ await fetch(url);
+ },
+ TypeError,
+ );
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function fetchJsonSuccess() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const json = await response.json();
+ assertEquals(json.name, "deno");
+});
+
+Deno.test({ permissions: { net: false } }, async function fetchPerm() {
+ await assertRejects(async () => {
+ await fetch("http://localhost:4545/assets/fixture.json");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchUrl() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ assertEquals(response.url, "http://localhost:4545/assets/fixture.json");
+ const _json = await response.json();
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchURL() {
+ const response = await fetch(
+ new URL("http://localhost:4545/assets/fixture.json"),
+ );
+ assertEquals(response.url, "http://localhost:4545/assets/fixture.json");
+ const _json = await response.json();
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchHeaders() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const headers = response.headers;
+ assertEquals(headers.get("Content-Type"), "application/json");
+ const _json = await response.json();
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchBlob() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const headers = response.headers;
+ const blob = await response.blob();
+ assertEquals(blob.type, headers.get("Content-Type"));
+ assertEquals(blob.size, Number(headers.get("Content-Length")));
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyUsedReader() {
+ const response = await fetch(
+ "http://localhost:4545/assets/fixture.json",
+ );
+ assert(response.body !== null);
+
+ const reader = response.body.getReader();
+ // Getting a reader should lock the stream but does not consume the body
+ // so bodyUsed should not be true
+ assertEquals(response.bodyUsed, false);
+ reader.releaseLock();
+ await response.json();
+ assertEquals(response.bodyUsed, true);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyUsedCancelStream() {
+ const response = await fetch(
+ "http://localhost:4545/assets/fixture.json",
+ );
+ assert(response.body !== null);
+
+ assertEquals(response.bodyUsed, false);
+ const promise = response.body.cancel();
+ assertEquals(response.bodyUsed, true);
+ await promise;
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function fetchAsyncIterator() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const headers = response.headers;
+
+ assert(response.body !== null);
+ let total = 0;
+ for await (const chunk of response.body) {
+ assert(chunk instanceof Uint8Array);
+ total += chunk.length;
+ }
+
+ assertEquals(total, Number(headers.get("Content-Length")));
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchBodyReader() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const headers = response.headers;
+ assert(response.body !== null);
+ const reader = response.body.getReader();
+ let total = 0;
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ assert(value instanceof Uint8Array);
+ total += value.length;
+ }
+
+ assertEquals(total, Number(headers.get("Content-Length")));
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyReaderBigBody() {
+ const data = "a".repeat(10 << 10); // 10mb
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data,
+ });
+ assert(response.body !== null);
+ const reader = await response.body.getReader();
+ let total = 0;
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ total += value.length;
+ }
+
+ assertEquals(total, data.length);
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function responseClone() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const response1 = response.clone();
+ assert(response !== response1);
+ assertEquals(response.status, response1.status);
+ assertEquals(response.statusText, response1.statusText);
+ const u8a = new Uint8Array(await response.arrayBuffer());
+ const u8a1 = new Uint8Array(await response1.arrayBuffer());
+ for (let i = 0; i < u8a.byteLength; i++) {
+ assertEquals(u8a[i], u8a1[i]);
+ }
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchMultipartFormDataSuccess() {
+ const response = await fetch(
+ "http://localhost:4545/multipart_form_data.txt",
+ );
+ const formData = await response.formData();
+ assert(formData.has("field_1"));
+ assertEquals(formData.get("field_1")!.toString(), "value_1 \r\n");
+ assert(formData.has("field_2"));
+ const file = formData.get("field_2") as File;
+ assertEquals(file.name, "file.js");
+
+ assertEquals(await file.text(), `console.log("Hi")`);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchMultipartFormBadContentType() {
+ const response = await fetch(
+ "http://localhost:4545/multipart_form_bad_content_type",
+ );
+ assert(response.body !== null);
+
+ await assertRejects(
+ async () => {
+ await response.formData();
+ },
+ TypeError,
+ "Body can not be decoded as form data",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchURLEncodedFormDataSuccess() {
+ const response = await fetch(
+ "http://localhost:4545/subdir/form_urlencoded.txt",
+ );
+ const formData = await response.formData();
+ assert(formData.has("field_1"));
+ assertEquals(formData.get("field_1")!.toString(), "Hi");
+ assert(formData.has("field_2"));
+ assertEquals(formData.get("field_2")!.toString(), "<Deno>");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataBinaryFileBody() {
+ // Some random bytes
+ // deno-fmt-ignore
+ const binaryFile = new Uint8Array([108,2,0,0,145,22,162,61,157,227,166,77,138,75,180,56,119,188,177,183]);
+ const response = await fetch("http://localhost:4545/echo_multipart_file", {
+ method: "POST",
+ body: binaryFile,
+ });
+ const resultForm = await response.formData();
+ const resultFile = resultForm.get("file") as File;
+
+ assertEquals(resultFile.type, "application/octet-stream");
+ assertEquals(resultFile.name, "file.bin");
+ assertEquals(new Uint8Array(await resultFile.arrayBuffer()), binaryFile);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataMultipleFilesBody() {
+ const files = [
+ {
+ // deno-fmt-ignore
+ content: new Uint8Array([137,80,78,71,13,10,26,10, 137, 1, 25]),
+ type: "image/png",
+ name: "image",
+ fileName: "some-image.png",
+ },
+ {
+ // deno-fmt-ignore
+ content: new Uint8Array([108,2,0,0,145,22,162,61,157,227,166,77,138,75,180,56,119,188,177,183]),
+ name: "file",
+ fileName: "file.bin",
+ expectedType: "application/octet-stream",
+ },
+ {
+ content: new TextEncoder().encode("deno land"),
+ type: "text/plain",
+ name: "text",
+ fileName: "deno.txt",
+ },
+ ];
+ const form = new FormData();
+ form.append("field", "value");
+ for (const file of files) {
+ form.append(
+ file.name,
+ new Blob([file.content], { type: file.type }),
+ file.fileName,
+ );
+ }
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: form,
+ });
+ const resultForm = await response.formData();
+ assertEquals(form.get("field"), resultForm.get("field"));
+ for (const file of files) {
+ const inputFile = form.get(file.name) as File;
+ const resultFile = resultForm.get(file.name) as File;
+ assertEquals(inputFile.size, resultFile.size);
+ assertEquals(inputFile.name, resultFile.name);
+ assertEquals(file.expectedType || file.type, resultFile.type);
+ assertEquals(
+ new Uint8Array(await resultFile.arrayBuffer()),
+ file.content,
+ );
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithRedirection() {
+ const response = await fetch("http://localhost:4546/assets/hello.txt");
+ assertEquals(response.status, 200);
+ assertEquals(response.statusText, "OK");
+ assertEquals(response.url, "http://localhost:4545/assets/hello.txt");
+ const body = await response.text();
+ assert(body.includes("Hello world!"));
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithRelativeRedirection() {
+ const response = await fetch(
+ "http://localhost:4545/run/001_hello.js",
+ );
+ assertEquals(response.status, 200);
+ assertEquals(response.statusText, "OK");
+ const body = await response.text();
+ assert(body.includes("Hello"));
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithRelativeRedirectionUrl() {
+ const cases = [
+ ["end", "http://localhost:4550/a/b/end"],
+ ["/end", "http://localhost:4550/end"],
+ ];
+ for (const [loc, redUrl] of cases) {
+ const response = await fetch("http://localhost:4550/a/b/c", {
+ headers: new Headers([["x-location", loc]]),
+ });
+ assertEquals(response.url, redUrl);
+ assertEquals(response.redirected, true);
+ assertEquals(response.status, 404);
+ assertEquals(await response.text(), "");
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithInfRedirection() {
+ await assertRejects(
+ () => fetch("http://localhost:4549"),
+ TypeError,
+ "redirect",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitStringBody() {
+ const data = "Hello World";
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data,
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ assert(response.headers.get("content-type")!.startsWith("text/plain"));
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchRequestInitStringBody() {
+ const data = "Hello World";
+ const req = new Request("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data,
+ });
+ const response = await fetch(req);
+ const text = await response.text();
+ assertEquals(text, data);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchSeparateInit() {
+ // related to: https://github.com/denoland/deno/issues/10396
+ const req = new Request("http://localhost:4545/run/001_hello.js");
+ const init = {
+ method: "GET",
+ };
+ req.headers.set("foo", "bar");
+ const res = await fetch(req, init);
+ assertEquals(res.status, 200);
+ await res.text();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitTypedArrayBody() {
+ const data = "Hello World";
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: new TextEncoder().encode(data),
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitArrayBufferBody() {
+ const data = "Hello World";
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: new TextEncoder().encode(data).buffer,
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitURLSearchParamsBody() {
+ const data = "param1=value1&param2=value2";
+ const params = new URLSearchParams(data);
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: params,
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ assert(
+ response.headers
+ .get("content-type")!
+ .startsWith("application/x-www-form-urlencoded"),
+ );
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function fetchInitBlobBody() {
+ const data = "const a = 1 🦕";
+ const blob = new Blob([data], {
+ type: "text/javascript",
+ });
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: blob,
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ assert(response.headers.get("content-type")!.startsWith("text/javascript"));
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataBody() {
+ const form = new FormData();
+ form.append("field", "value");
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: form,
+ });
+ const resultForm = await response.formData();
+ assertEquals(form.get("field"), resultForm.get("field"));
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataBlobFilenameBody() {
+ const form = new FormData();
+ form.append("field", "value");
+ form.append(
+ "file",
+ new Blob([new TextEncoder().encode("deno")]),
+ "file name",
+ );
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: form,
+ });
+ const resultForm = await response.formData();
+ assertEquals(form.get("field"), resultForm.get("field"));
+ const file = resultForm.get("file");
+ assert(file instanceof File);
+ assertEquals(file.name, "file name");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataFileFilenameBody() {
+ const form = new FormData();
+ form.append("field", "value");
+ form.append(
+ "file",
+ new File([new Blob([new TextEncoder().encode("deno")])], "file name"),
+ );
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: form,
+ });
+ const resultForm = await response.formData();
+ assertEquals(form.get("field"), resultForm.get("field"));
+ const file = resultForm.get("file");
+ assert(file instanceof File);
+ assertEquals(file.name, "file name");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataTextFileBody() {
+ const fileContent = "deno land";
+ const form = new FormData();
+ form.append("field", "value");
+ form.append(
+ "file",
+ new Blob([new TextEncoder().encode(fileContent)], {
+ type: "text/plain",
+ }),
+ "deno.txt",
+ );
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: form,
+ });
+ const resultForm = await response.formData();
+ assertEquals(form.get("field"), resultForm.get("field"));
+
+ const file = form.get("file") as File;
+ const resultFile = resultForm.get("file") as File;
+
+ assertEquals(file.size, resultFile.size);
+ assertEquals(file.name, resultFile.name);
+ assertEquals(file.type, resultFile.type);
+ assertEquals(await file.text(), await resultFile.text());
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function fetchUserAgent() {
+ const data = "Hello World";
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: new TextEncoder().encode(data),
+ });
+ assertEquals(response.headers.get("user-agent"), `Deno/${Deno.version.deno}`);
+ await response.text();
+});
+
+function bufferServer(addr: string): Promise<Buffer> {
+ const [hostname, port] = addr.split(":");
+ const listener = Deno.listen({
+ hostname,
+ port: Number(port),
+ }) as Deno.Listener;
+ return listener.accept().then(async (conn: Deno.Conn) => {
+ const buf = new Buffer();
+ const p1 = buf.readFrom(conn);
+ const p2 = conn.write(
+ new TextEncoder().encode(
+ "HTTP/1.0 404 Not Found\r\nContent-Length: 2\r\n\r\nNF",
+ ),
+ );
+ // Wait for both an EOF on the read side of the socket and for the write to
+ // complete before closing it. Due to keep-alive, the EOF won't be sent
+ // until the Connection close (HTTP/1.0) response, so readFrom() can't
+ // proceed write. Conversely, if readFrom() is async, waiting for the
+ // write() to complete is not a guarantee that we've read the incoming
+ // request.
+ await Promise.all([p1, p2]);
+ conn.close();
+ listener.close();
+ return buf;
+ });
+}
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchRequest() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Hello", "World"],
+ ["Foo", "Bar"],
+ ],
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "content-length: 0\r\n",
+ "hello: World\r\n",
+ "foo: Bar\r\n",
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n\r\n`,
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchRequestAcceptHeaders() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Accept", "text/html"],
+ ["Accept-Language", "en-US"],
+ ],
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "content-length: 0\r\n",
+ "accept: text/html\r\n",
+ "accept-language: en-US\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n\r\n`,
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchPostBodyString() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const body = "hello world";
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Hello", "World"],
+ ["Foo", "Bar"],
+ ],
+ body,
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "hello: World\r\n",
+ "foo: Bar\r\n",
+ "content-type: text/plain;charset=UTF-8\r\n",
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n`,
+ `content-length: ${body.length}\r\n\r\n`,
+ body,
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchPostBodyTypedArray() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const bodyStr = "hello world";
+ const body = new TextEncoder().encode(bodyStr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Hello", "World"],
+ ["Foo", "Bar"],
+ ],
+ body,
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "hello: World\r\n",
+ "foo: Bar\r\n",
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n`,
+ `content-length: ${body.byteLength}\r\n\r\n`,
+ bodyStr,
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchUserSetContentLength() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Content-Length", "10"],
+ ],
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "content-length: 0\r\n",
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n\r\n`,
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchUserSetTransferEncoding() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Transfer-Encoding", "chunked"],
+ ],
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "content-length: 0\r\n",
+ `host: ${addr}\r\n`,
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n\r\n",
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithNonAsciiRedirection() {
+ const response = await fetch("http://localhost:4545/non_ascii_redirect", {
+ redirect: "manual",
+ });
+ assertEquals(response.status, 301);
+ assertEquals(response.headers.get("location"), "/redirect®");
+ await response.text();
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithManualRedirection() {
+ const response = await fetch("http://localhost:4546/", {
+ redirect: "manual",
+ }); // will redirect to http://localhost:4545/
+ assertEquals(response.status, 301);
+ assertEquals(response.url, "http://localhost:4546/");
+ assertEquals(response.type, "basic");
+ assertEquals(response.headers.get("Location"), "http://localhost:4545/");
+ await response.body!.cancel();
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithErrorRedirection() {
+ await assertRejects(
+ () =>
+ fetch("http://localhost:4546/", {
+ redirect: "error",
+ }),
+ TypeError,
+ "redirect",
+ );
+ },
+);
+
+Deno.test(function responseRedirect() {
+ const redir = Response.redirect("http://example.com/newLocation", 301);
+ assertEquals(redir.status, 301);
+ assertEquals(redir.statusText, "");
+ assertEquals(redir.url, "");
+ assertEquals(
+ redir.headers.get("Location"),
+ "http://example.com/newLocation",
+ );
+ assertEquals(redir.type, "default");
+});
+
+Deno.test(function responseRedirectTakeURLObjectAsParameter() {
+ const redir = Response.redirect(new URL("https://example.com/"));
+ assertEquals(
+ redir.headers.get("Location"),
+ "https://example.com/",
+ );
+});
+
+Deno.test(async function responseWithoutBody() {
+ const response = new Response();
+ assertEquals(await response.arrayBuffer(), new ArrayBuffer(0));
+ const blob = await response.blob();
+ assertEquals(blob.size, 0);
+ assertEquals(await blob.arrayBuffer(), new ArrayBuffer(0));
+ assertEquals(await response.text(), "");
+ await assertRejects(async () => {
+ await response.json();
+ });
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchBodyReadTwice() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+
+ // Read body
+ const _json = await response.json();
+ assert(_json);
+
+ // All calls after the body was consumed, should fail
+ const methods = ["json", "text", "formData", "arrayBuffer"] as const;
+ for (const method of methods) {
+ try {
+ await response[method]();
+ fail(
+ "Reading body multiple times should failed, the stream should've been locked.",
+ );
+ } catch {
+ // pass
+ }
+ }
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyReaderAfterRead() {
+ const response = await fetch(
+ "http://localhost:4545/assets/fixture.json",
+ );
+ assert(response.body !== null);
+ const reader = await response.body.getReader();
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ }
+
+ try {
+ response.body.getReader();
+ fail("The stream should've been locked.");
+ } catch {
+ // pass
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyReaderWithCancelAndNewReader() {
+ const data = "a".repeat(1 << 10);
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data,
+ });
+ assert(response.body !== null);
+ const firstReader = await response.body.getReader();
+
+ // Acquire reader without reading & release
+ await firstReader.releaseLock();
+
+ const reader = await response.body.getReader();
+
+ let total = 0;
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ total += value.length;
+ }
+
+ assertEquals(total, data.length);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyReaderWithReadCancelAndNewReader() {
+ const data = "a".repeat(1 << 10);
+
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data,
+ });
+ assert(response.body !== null);
+ const firstReader = await response.body.getReader();
+
+ // Do one single read with first reader
+ const { value: firstValue } = await firstReader.read();
+ assert(firstValue);
+ await firstReader.releaseLock();
+
+ // Continue read with second reader
+ const reader = await response.body.getReader();
+ let total = firstValue.length || 0;
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ total += value.length;
+ }
+ assertEquals(total, data.length);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchResourceCloseAfterStreamCancel() {
+ const res = await fetch("http://localhost:4545/assets/fixture.json");
+ assert(res.body !== null);
+
+ // After ReadableStream.cancel is called, resource handle must be closed
+ // The test should not fail with: Test case is leaking resources
+ await res.body.cancel();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchNullBodyStatus() {
+ const nullBodyStatus = [101, 204, 205, 304];
+
+ for (const status of nullBodyStatus) {
+ const headers = new Headers([["x-status", String(status)]]);
+ const res = await fetch("http://localhost:4545/echo_server", {
+ body: "deno",
+ method: "POST",
+ headers,
+ });
+ assertEquals(res.body, null);
+ assertEquals(res.status, status);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchResponseContentLength() {
+ const body = new Uint8Array(2 ** 16);
+ const headers = new Headers([["content-type", "application/octet-stream"]]);
+ const res = await fetch("http://localhost:4545/echo_server", {
+ body: body,
+ method: "POST",
+ headers,
+ });
+ assertEquals(Number(res.headers.get("content-length")), body.byteLength);
+
+ const blob = await res.blob();
+ // Make sure Body content-type is correctly set
+ assertEquals(blob.type, "application/octet-stream");
+ assertEquals(blob.size, body.byteLength);
+ },
+);
+
+Deno.test(function fetchResponseConstructorNullBody() {
+ const nullBodyStatus = [204, 205, 304];
+
+ for (const status of nullBodyStatus) {
+ try {
+ new Response("deno", { status });
+ fail("Response with null body status cannot have body");
+ } catch (e) {
+ assert(e instanceof TypeError);
+ assertEquals(
+ e.message,
+ "Response with null body status cannot have body",
+ );
+ }
+ }
+});
+
+Deno.test(function fetchResponseConstructorInvalidStatus() {
+ const invalidStatus = [100, 600, 199, null, "", NaN];
+
+ for (const status of invalidStatus) {
+ try {
+ // deno-lint-ignore ban-ts-comment
+ // @ts-ignore
+ new Response("deno", { status });
+ fail(`Invalid status: ${status}`);
+ } catch (e) {
+ assert(e instanceof RangeError);
+ assert(
+ e.message.endsWith(
+ "is not equal to 101 and outside the range [200, 599].",
+ ),
+ );
+ }
+ }
+});
+
+Deno.test(function fetchResponseEmptyConstructor() {
+ const response = new Response();
+ assertEquals(response.status, 200);
+ assertEquals(response.body, null);
+ assertEquals(response.type, "default");
+ assertEquals(response.url, "");
+ assertEquals(response.redirected, false);
+ assertEquals(response.ok, true);
+ assertEquals(response.bodyUsed, false);
+ assertEquals([...response.headers], []);
+});
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchCustomHttpClientParamCertificateSuccess(): Promise<
+ void
+ > {
+ const caCert = Deno.readTextFileSync("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const response = await fetch("https://localhost:5545/assets/fixture.json", {
+ client,
+ });
+ const json = await response.json();
+ assertEquals(json.name, "deno");
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ function createHttpClientAcceptPoolIdleTimeout() {
+ const client = Deno.createHttpClient({
+ poolIdleTimeout: 1000,
+ });
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchCustomClientUserAgent(): Promise<
+ void
+ > {
+ const data = "Hello World";
+ const client = Deno.createHttpClient({});
+ const response = await fetch("http://localhost:4545/echo_server", {
+ client,
+ method: "POST",
+ body: new TextEncoder().encode(data),
+ });
+ assertEquals(
+ response.headers.get("user-agent"),
+ `Deno/${Deno.version.deno}`,
+ );
+ await response.text();
+ client.close();
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchPostBodyReadableStream() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const stream = new TransformStream();
+ const writer = stream.writable.getWriter();
+ // transformer writes don't resolve until they are read, so awaiting these
+ // will cause the transformer to hang, as the suspend the transformer, it
+ // is also illogical to await for the reads, as that is the whole point of
+ // streams is to have a "queue" which gets drained...
+ writer.write(new TextEncoder().encode("hello "));
+ writer.write(new TextEncoder().encode("world"));
+ writer.close();
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Hello", "World"],
+ ["Foo", "Bar"],
+ ],
+ body: stream.readable,
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "hello: World\r\n",
+ "foo: Bar\r\n",
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n`,
+ `transfer-encoding: chunked\r\n\r\n`,
+ "B\r\n",
+ "hello world\r\n",
+ "0\r\n\r\n",
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test({}, function fetchWritableRespProps() {
+ const original = new Response("https://deno.land", {
+ status: 404,
+ headers: { "x-deno": "foo" },
+ });
+ const new_ = new Response("https://deno.land", original);
+ assertEquals(original.status, new_.status);
+ assertEquals(new_.headers.get("x-deno"), "foo");
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchFilterOutCustomHostHeader(): Promise<
+ void
+ > {
+ const addr = `127.0.0.1:${listenPort}`;
+ const [hostname, port] = addr.split(":");
+ const listener = Deno.listen({
+ hostname,
+ port: Number(port),
+ }) as Deno.Listener;
+
+ let httpConn: Deno.HttpConn;
+ listener.accept().then(async (conn: Deno.Conn) => {
+ httpConn = Deno.serveHttp(conn);
+
+ await httpConn.nextRequest()
+ .then(async (requestEvent: Deno.RequestEvent | null) => {
+ const hostHeader = requestEvent?.request.headers.get("Host");
+ const headersToReturn = hostHeader
+ ? { "Host": hostHeader }
+ : undefined;
+
+ await requestEvent?.respondWith(
+ new Response("", {
+ status: 200,
+ headers: headersToReturn,
+ }),
+ );
+ });
+ });
+
+ const response = await fetch(`http://${addr}/`, {
+ headers: { "Host": "example.com" },
+ });
+ await response.text();
+ listener.close();
+ httpConn!.close();
+
+ assertEquals(response.headers.get("Host"), addr);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchNoServerReadableStreamBody() {
+ const completed = Promise.withResolvers<void>();
+ const failed = Promise.withResolvers<void>();
+ const body = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new Uint8Array([1]));
+ setTimeout(async () => {
+ // This is technically a race. If the fetch has failed by this point, the enqueue will
+ // throw. If not, it will succeed. Windows appears to take a while to time out the fetch,
+ // so we will just wait for that here before we attempt to enqueue so it's consistent
+ // across platforms.
+ await failed.promise;
+ assertThrows(() => controller.enqueue(new Uint8Array([2])));
+ completed.resolve();
+ }, 1000);
+ },
+ });
+ const nonExistentHostname = "http://localhost:47582";
+ await assertRejects(async () => {
+ await fetch(nonExistentHostname, { body, method: "POST" });
+ }, TypeError);
+ failed.resolve();
+ await completed.promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchHeadRespBody() {
+ const res = await fetch("http://localhost:4545/echo_server", {
+ method: "HEAD",
+ });
+ assertEquals(res.body, null);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function fetchClientCertWrongPrivateKey(): Promise<void> {
+ await assertRejects(async () => {
+ const client = Deno.createHttpClient({
+ certChain: "bad data",
+ privateKey: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.key",
+ ),
+ });
+ await fetch("https://localhost:5552/assets/fixture.json", {
+ client,
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function fetchClientCertBadPrivateKey(): Promise<void> {
+ await assertRejects(async () => {
+ const client = Deno.createHttpClient({
+ certChain: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.crt",
+ ),
+ privateKey: "bad data",
+ });
+ await fetch("https://localhost:5552/assets/fixture.json", {
+ client,
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function fetchClientCertNotPrivateKey(): Promise<void> {
+ await assertRejects(async () => {
+ const client = Deno.createHttpClient({
+ certChain: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.crt",
+ ),
+ privateKey: "",
+ });
+ await fetch("https://localhost:5552/assets/fixture.json", {
+ client,
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function fetchCustomClientPrivateKey(): Promise<
+ void
+ > {
+ const data = "Hello World";
+ const caCert = await Deno.readTextFile("tests/testdata/tls/RootCA.crt");
+ const client = Deno.createHttpClient({
+ certChain: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.crt",
+ ),
+ privateKey: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.key",
+ ),
+ caCerts: [caCert],
+ });
+ const response = await fetch("https://localhost:5552/echo_server", {
+ client,
+ method: "POST",
+ body: new TextEncoder().encode(data),
+ });
+ assertEquals(
+ response.headers.get("user-agent"),
+ `Deno/${Deno.version.deno}`,
+ );
+ await response.text();
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchAbortWhileUploadStreaming(): Promise<void> {
+ const abortController = new AbortController();
+ try {
+ await fetch(
+ "http://localhost:5552/echo_server",
+ {
+ method: "POST",
+ body: new ReadableStream({
+ pull(controller) {
+ abortController.abort();
+ controller.enqueue(new Uint8Array([1, 2, 3, 4]));
+ },
+ }),
+ signal: abortController.signal,
+ },
+ );
+ fail("Fetch didn't reject.");
+ } catch (error) {
+ assert(error instanceof DOMException);
+ assertEquals(error.name, "AbortError");
+ assertEquals(error.message, "The signal has been aborted");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchAbortWhileUploadStreamingWithReason(): Promise<void> {
+ const abortController = new AbortController();
+ const abortReason = new Error();
+ try {
+ await fetch(
+ "http://localhost:5552/echo_server",
+ {
+ method: "POST",
+ body: new ReadableStream({
+ pull(controller) {
+ abortController.abort(abortReason);
+ controller.enqueue(new Uint8Array([1, 2, 3, 4]));
+ },
+ }),
+ signal: abortController.signal,
+ },
+ );
+ fail("Fetch didn't reject.");
+ } catch (error) {
+ assertEquals(error, abortReason);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchAbortWhileUploadStreamingWithPrimitiveReason(): Promise<
+ void
+ > {
+ const abortController = new AbortController();
+ try {
+ await fetch(
+ "http://localhost:5552/echo_server",
+ {
+ method: "POST",
+ body: new ReadableStream({
+ pull(controller) {
+ abortController.abort("Abort reason");
+ controller.enqueue(new Uint8Array([1, 2, 3, 4]));
+ },
+ }),
+ signal: abortController.signal,
+ },
+ );
+ fail("Fetch didn't reject.");
+ } catch (error) {
+ assertEquals(error, "Abort reason");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchHeaderValueShouldNotPanic() {
+ for (let i = 0; i < 0x21; i++) {
+ if (i === 0x09 || i === 0x0A || i === 0x0D || i === 0x20) {
+ continue; // these header value will be normalized, will not cause an error.
+ }
+ // ensure there will be an error instead of panic.
+ await assertRejects(() =>
+ fetch("http://localhost:4545/echo_server", {
+ method: "HEAD",
+ headers: { "val": String.fromCharCode(i) },
+ }), TypeError);
+ }
+ await assertRejects(() =>
+ fetch("http://localhost:4545/echo_server", {
+ method: "HEAD",
+ headers: { "val": String.fromCharCode(127) },
+ }), TypeError);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchHeaderNameShouldNotPanic() {
+ const validTokens =
+ "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUWVXYZ^_`abcdefghijklmnopqrstuvwxyz|~"
+ .split("");
+ for (let i = 0; i <= 255; i++) {
+ const token = String.fromCharCode(i);
+ if (validTokens.includes(token)) {
+ continue;
+ }
+ // ensure there will be an error instead of panic.
+ await assertRejects(() =>
+ fetch("http://localhost:4545/echo_server", {
+ method: "HEAD",
+ headers: { [token]: "value" },
+ }), TypeError);
+ }
+ await assertRejects(() =>
+ fetch("http://localhost:4545/echo_server", {
+ method: "HEAD",
+ headers: { "": "value" },
+ }), TypeError);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchSupportsHttpsOverIpAddress() {
+ const caCert = await Deno.readTextFile("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const res = await fetch("https://localhost:5546/http_version", { client });
+ assert(res.ok);
+ assertEquals(await res.text(), "HTTP/1.1");
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchSupportsHttp1Only() {
+ const caCert = await Deno.readTextFile("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const res = await fetch("https://localhost:5546/http_version", { client });
+ assert(res.ok);
+ assertEquals(await res.text(), "HTTP/1.1");
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchSupportsHttp2() {
+ const caCert = await Deno.readTextFile("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const res = await fetch("https://localhost:5547/http_version", { client });
+ assert(res.ok);
+ assertEquals(await res.text(), "HTTP/2.0");
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchForceHttp1OnHttp2Server() {
+ const client = Deno.createHttpClient({ http2: false, http1: true });
+ await assertRejects(
+ () => fetch("http://localhost:5549/http_version", { client }),
+ TypeError,
+ );
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchForceHttp2OnHttp1Server() {
+ const client = Deno.createHttpClient({ http2: true, http1: false });
+ await assertRejects(
+ () => fetch("http://localhost:5548/http_version", { client }),
+ TypeError,
+ );
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchPrefersHttp2() {
+ const caCert = await Deno.readTextFile("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const res = await fetch("https://localhost:5545/http_version", { client });
+ assert(res.ok);
+ assertEquals(await res.text(), "HTTP/2.0");
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function createHttpClientAllowHost() {
+ const client = Deno.createHttpClient({
+ allowHost: true,
+ });
+ const res = await fetch("http://localhost:4545/echo_server", {
+ headers: {
+ "host": "example.com",
+ },
+ client,
+ });
+ assert(res.ok);
+ assertEquals(res.headers.get("host"), "example.com");
+ await res.body?.cancel();
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function createHttpClientExplicitResourceManagement() {
+ using client = Deno.createHttpClient({});
+ const response = await fetch("http://localhost:4545/assets/fixture.json", {
+ client,
+ });
+ const json = await response.json();
+ assertEquals(json.name, "deno");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function createHttpClientExplicitResourceManagementDoubleClose() {
+ using client = Deno.createHttpClient({});
+ const response = await fetch("http://localhost:4545/assets/fixture.json", {
+ client,
+ });
+ const json = await response.json();
+ assertEquals(json.name, "deno");
+ // Close the client even though we declared it with `using` to confirm that
+ // the cleanup done as per `Symbol.dispose` will not throw any errors.
+ client.close();
+ },
+);
+
+Deno.test({ permissions: { read: false } }, async function fetchFilePerm() {
+ await assertRejects(async () => {
+ await fetch(import.meta.resolve("../testdata/subdir/json_1.json"));
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { read: false } },
+ async function fetchFilePermDoesNotExist() {
+ await assertRejects(async () => {
+ await fetch(import.meta.resolve("./bad.json"));
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function fetchFileBadMethod() {
+ await assertRejects(
+ async () => {
+ await fetch(
+ import.meta.resolve("../testdata/subdir/json_1.json"),
+ {
+ method: "POST",
+ },
+ );
+ },
+ TypeError,
+ "Fetching files only supports the GET method. Received POST.",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function fetchFileDoesNotExist() {
+ await assertRejects(
+ async () => {
+ await fetch(import.meta.resolve("./bad.json"));
+ },
+ TypeError,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function fetchFile() {
+ const res = await fetch(
+ import.meta.resolve("../testdata/subdir/json_1.json"),
+ );
+ assert(res.ok);
+ const fixture = await Deno.readTextFile(
+ "tests/testdata/subdir/json_1.json",
+ );
+ assertEquals(await res.text(), fixture);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchContentLengthPost() {
+ const response = await fetch("http://localhost:4545/content_length", {
+ method: "POST",
+ });
+ const length = await response.text();
+ assertEquals(length, 'Some("0")');
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchContentLengthPut() {
+ const response = await fetch("http://localhost:4545/content_length", {
+ method: "PUT",
+ });
+ const length = await response.text();
+ assertEquals(length, 'Some("0")');
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchContentLengthPatch() {
+ const response = await fetch("http://localhost:4545/content_length", {
+ method: "PATCH",
+ });
+ const length = await response.text();
+ assertEquals(length, "None");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchContentLengthPostWithStringBody() {
+ const response = await fetch("http://localhost:4545/content_length", {
+ method: "POST",
+ body: "Hey!",
+ });
+ const length = await response.text();
+ assertEquals(length, 'Some("4")');
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchContentLengthPostWithBufferBody() {
+ const response = await fetch("http://localhost:4545/content_length", {
+ method: "POST",
+ body: new TextEncoder().encode("Hey!"),
+ });
+ const length = await response.text();
+ assertEquals(length, 'Some("4")');
+ },
+);
+
+Deno.test(async function staticResponseJson() {
+ const data = { hello: "world" };
+ const resp = Response.json(data);
+ assertEquals(resp.status, 200);
+ assertEquals(resp.headers.get("content-type"), "application/json");
+ const res = await resp.json();
+ assertEquals(res, data);
+});
+
+function invalidServer(addr: string, body: Uint8Array): Deno.Listener {
+ const [hostname, port] = addr.split(":");
+ const listener = Deno.listen({
+ hostname,
+ port: Number(port),
+ }) as Deno.Listener;
+
+ (async () => {
+ for await (const conn of listener) {
+ const p1 = conn.read(new Uint8Array(2 ** 14));
+ const p2 = conn.write(body);
+
+ await Promise.all([p1, p2]);
+ conn.close();
+ }
+ })();
+
+ return listener;
+}
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchWithInvalidContentLengthAndTransferEncoding(): Promise<
+ void
+ > {
+ const addr = `127.0.0.1:${listenPort}`;
+ const data = "a".repeat(10 << 10);
+
+ const body = new TextEncoder().encode(
+ `HTTP/1.1 200 OK\r\nContent-Length: ${
+ Math.round(data.length * 2)
+ }\r\nTransfer-Encoding: chunked\r\n\r\n${
+ data.length.toString(16)
+ }\r\n${data}\r\n0\r\n\r\n`,
+ );
+
+ // if transfer-encoding is sent, content-length is ignored
+ // even if it has an invalid value (content-length > totalLength)
+ const listener = invalidServer(addr, body);
+ const response = await fetch(`http://${addr}/`);
+
+ const res = await response.arrayBuffer();
+ const buf = new TextEncoder().encode(data);
+ assertEquals(res.byteLength, buf.byteLength);
+ assertEquals(new Uint8Array(res), buf);
+
+ listener.close();
+ },
+);
+
+Deno.test(
+ // TODO(bartlomieju): reenable this test
+ // https://github.com/denoland/deno/issues/18350
+ { ignore: Deno.build.os === "windows", permissions: { net: true } },
+ async function fetchWithInvalidContentLength(): Promise<
+ void
+ > {
+ const addr = `127.0.0.1:${listenPort}`;
+ const data = "a".repeat(10 << 10);
+
+ const body = new TextEncoder().encode(
+ `HTTP/1.1 200 OK\r\nContent-Length: ${
+ Math.round(data.length / 2)
+ }\r\nContent-Length: ${data.length}\r\n\r\n${data}`,
+ );
+
+ // It should fail if multiple content-length headers with different values are sent
+ const listener = invalidServer(addr, body);
+ await assertRejects(
+ async () => {
+ await fetch(`http://${addr}/`);
+ },
+ TypeError,
+ "invalid content-length parsed",
+ );
+
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchWithInvalidContentLength2(): Promise<
+ void
+ > {
+ const addr = `127.0.0.1:${listenPort}`;
+ const data = "a".repeat(10 << 10);
+
+ const contentLength = data.length / 2;
+ const body = new TextEncoder().encode(
+ `HTTP/1.1 200 OK\r\nContent-Length: ${contentLength}\r\n\r\n${data}`,
+ );
+
+ const listener = invalidServer(addr, body);
+ const response = await fetch(`http://${addr}/`);
+
+ // If content-length < totalLength, a maximum of content-length bytes
+ // should be returned.
+ const res = await response.arrayBuffer();
+ const buf = new TextEncoder().encode(data);
+ assertEquals(res.byteLength, contentLength);
+ assertEquals(new Uint8Array(res), buf.subarray(contentLength));
+
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchWithInvalidContentLength3(): Promise<
+ void
+ > {
+ const addr = `127.0.0.1:${listenPort}`;
+ const data = "a".repeat(10 << 10);
+
+ const contentLength = data.length * 2;
+ const body = new TextEncoder().encode(
+ `HTTP/1.1 200 OK\r\nContent-Length: ${contentLength}\r\n\r\n${data}`,
+ );
+
+ const listener = invalidServer(addr, body);
+ const response = await fetch(`http://${addr}/`);
+ // If content-length > totalLength, a maximum of content-length bytes
+ // should be returned.
+ await assertRejects(
+ async () => {
+ await response.arrayBuffer();
+ },
+ Error,
+ "end of file before message length reached",
+ );
+
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBlobUrl(): Promise<void> {
+ const blob = new Blob(["ok"], { type: "text/plain" });
+ const url = URL.createObjectURL(blob);
+ assert(url.startsWith("blob:"), `URL was ${url}`);
+ const res = await fetch(url);
+ assertEquals(res.url, url);
+ assertEquals(res.status, 200);
+ assertEquals(res.headers.get("content-length"), "2");
+ assertEquals(res.headers.get("content-type"), "text/plain");
+ assertEquals(await res.text(), "ok");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchResponseStreamIsLockedWhileReading() {
+ const response = await fetch("http://localhost:4545/echo_server", {
+ body: new Uint8Array(5000),
+ method: "POST",
+ });
+
+ assertEquals(response.body!.locked, false);
+ const promise = response.arrayBuffer();
+ assertEquals(response.body!.locked, true);
+
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchConstructorClones() {
+ const req = new Request("https://example.com", {
+ method: "POST",
+ body: "foo",
+ });
+ assertEquals(await req.text(), "foo");
+ await assertRejects(() => req.text());
+
+ const req2 = new Request(req, { method: "PUT", body: "bar" }); // should not have any impact on req
+ await assertRejects(() => req.text());
+ assertEquals(await req2.text(), "bar");
+
+ assertEquals(req.method, "POST");
+ assertEquals(req2.method, "PUT");
+
+ assertEquals(req.headers.get("x-foo"), null);
+ assertEquals(req2.headers.get("x-foo"), null);
+ req2.headers.set("x-foo", "bar"); // should not have any impact on req
+ assertEquals(req.headers.get("x-foo"), null);
+ assertEquals(req2.headers.get("x-foo"), "bar");
+ },
+);
+
+Deno.test(
+ // TODO(bartlomieju): reenable this test
+ // https://github.com/denoland/deno/issues/18350
+ { ignore: Deno.build.os === "windows", permissions: { net: true } },
+ async function fetchRequestBodyErrorCatchable() {
+ const listener = Deno.listen({ hostname: "127.0.0.1", port: listenPort });
+ const server = (async () => {
+ const conn = await listener.accept();
+ listener.close();
+ const buf = new Uint8Array(256);
+ const n = await conn.read(buf);
+ const data = new TextDecoder().decode(buf.subarray(0, n!)); // this is the request headers + first body chunk
+ assert(data.startsWith("POST / HTTP/1.1\r\n"));
+ assert(data.endsWith("1\r\na\r\n"));
+ const n2 = await conn.read(buf);
+ assertEquals(n2, 6); // this is the second body chunk
+ const n3 = await conn.read(buf);
+ assertEquals(n3, null); // the connection now abruptly closes because the client has errored
+ conn.close();
+ })();
+
+ const stream = new ReadableStream({
+ async start(controller) {
+ controller.enqueue(new TextEncoder().encode("a"));
+ await delay(1000);
+ controller.enqueue(new TextEncoder().encode("b"));
+ await delay(1000);
+ controller.error(new Error("foo"));
+ },
+ });
+
+ const err = await assertRejects(() =>
+ fetch(`http://localhost:${listenPort}/`, {
+ body: stream,
+ method: "POST",
+ })
+ );
+
+ assert(err instanceof TypeError, `err was not a TypeError ${err}`);
+ assert(err.cause, `err.cause was null ${err}`);
+ assert(
+ err.cause instanceof Error,
+ `err.cause was not an Error ${err.cause}`,
+ );
+ assertEquals(err.cause.message, "foo");
+
+ await server;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchRequestBodyEmptyStream() {
+ const body = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new Uint8Array([]));
+ controller.close();
+ },
+ });
+
+ await assertRejects(
+ async () => {
+ const controller = new AbortController();
+ const promise = fetch("http://localhost:4545/echo_server", {
+ body,
+ method: "POST",
+ signal: controller.signal,
+ });
+ try {
+ controller.abort();
+ } catch (e) {
+ console.log(e);
+ fail("abort should not throw");
+ }
+ await promise;
+ },
+ DOMException,
+ "The signal has been aborted",
+ );
+ },
+);
+
+Deno.test("Request with subarray TypedArray body", async () => {
+ const body = new Uint8Array([1, 2, 3, 4, 5]).subarray(1);
+ const req = new Request("https://example.com", { method: "POST", body });
+ const actual = new Uint8Array(await req.arrayBuffer());
+ const expected = new Uint8Array([2, 3, 4, 5]);
+ assertEquals(actual, expected);
+});
+
+Deno.test("Response with subarray TypedArray body", async () => {
+ const body = new Uint8Array([1, 2, 3, 4, 5]).subarray(1);
+ const req = new Response(body);
+ const actual = new Uint8Array(await req.arrayBuffer());
+ const expected = new Uint8Array([2, 3, 4, 5]);
+ assertEquals(actual, expected);
+});
diff --git a/tests/unit/ffi_test.ts b/tests/unit/ffi_test.ts
new file mode 100644
index 000000000..2b56a8db1
--- /dev/null
+++ b/tests/unit/ffi_test.ts
@@ -0,0 +1,137 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assertEquals, assertRejects, assertThrows } from "./test_util.ts";
+
+Deno.test({ permissions: { ffi: true } }, function dlopenInvalidArguments() {
+ const filename = "/usr/lib/libc.so.6";
+ assertThrows(() => {
+ // @ts-expect-error: ForeignFunction cannot be null
+ Deno.dlopen(filename, { malloc: null });
+ }, TypeError);
+ assertThrows(() => {
+ Deno.dlopen(filename, {
+ // @ts-expect-error: invalid NativeType
+ malloc: { parameters: ["a"], result: "b" },
+ });
+ }, TypeError);
+ assertThrows(() => {
+ // @ts-expect-error: DynamicLibrary symbols cannot be null
+ Deno.dlopen(filename, null);
+ }, TypeError);
+ assertThrows(() => {
+ // @ts-expect-error: require 2 arguments
+ Deno.dlopen(filename);
+ }, TypeError);
+});
+
+Deno.test({ permissions: { ffi: false } }, function ffiPermissionDenied() {
+ assertThrows(() => {
+ Deno.dlopen("/usr/lib/libc.so.6", {});
+ }, Deno.errors.PermissionDenied);
+ const fnptr = new Deno.UnsafeFnPointer(
+ // @ts-expect-error: Not NonNullable but null check is after permissions check.
+ null,
+ {
+ parameters: ["u32", "pointer"],
+ result: "void",
+ } as const,
+ );
+ assertThrows(() => {
+ fnptr.call(123, null);
+ }, Deno.errors.PermissionDenied);
+ assertThrows(() => {
+ Deno.UnsafePointer.of(new Uint8Array(0));
+ }, Deno.errors.PermissionDenied);
+ const ptrView = new Deno.UnsafePointerView(
+ // @ts-expect-error: Not NonNullable but null check is after permissions check.
+ null,
+ );
+ assertThrows(() => {
+ ptrView.copyInto(new Uint8Array(0));
+ }, Deno.errors.PermissionDenied);
+ assertThrows(() => {
+ ptrView.getCString();
+ }, Deno.errors.PermissionDenied);
+ assertThrows(() => {
+ ptrView.getUint8();
+ }, Deno.errors.PermissionDenied);
+ assertThrows(() => {
+ ptrView.getInt8();
+ }, Deno.errors.PermissionDenied);
+ assertThrows(() => {
+ ptrView.getUint16();
+ }, Deno.errors.PermissionDenied);
+ assertThrows(() => {
+ ptrView.getInt16();
+ }, Deno.errors.PermissionDenied);
+ assertThrows(() => {
+ ptrView.getUint32();
+ }, Deno.errors.PermissionDenied);
+ assertThrows(() => {
+ ptrView.getInt32();
+ }, Deno.errors.PermissionDenied);
+ assertThrows(() => {
+ ptrView.getFloat32();
+ }, Deno.errors.PermissionDenied);
+ assertThrows(() => {
+ ptrView.getFloat64();
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { ffi: true } }, function pointerOf() {
+ const buffer = new ArrayBuffer(1024);
+ const baseAddress = Deno.UnsafePointer.value(Deno.UnsafePointer.of(buffer));
+ const uint8Address = Deno.UnsafePointer.value(
+ Deno.UnsafePointer.of(new Uint8Array(buffer)),
+ );
+ assertEquals(baseAddress, uint8Address);
+ const float64Address = Deno.UnsafePointer.value(
+ Deno.UnsafePointer.of(new Float64Array(buffer)),
+ );
+ assertEquals(baseAddress, float64Address);
+ const uint8AddressOffset = Deno.UnsafePointer.value(
+ Deno.UnsafePointer.of(new Uint8Array(buffer, 100)),
+ );
+ assertEquals(Number(baseAddress) + 100, uint8AddressOffset);
+ const float64AddressOffset = Deno.UnsafePointer.value(
+ Deno.UnsafePointer.of(new Float64Array(buffer, 80)),
+ );
+ assertEquals(Number(baseAddress) + 80, float64AddressOffset);
+});
+
+Deno.test({ permissions: { ffi: true } }, function callWithError() {
+ const throwCb = () => {
+ throw new Error("Error");
+ };
+ const cb = new Deno.UnsafeCallback({
+ parameters: [],
+ result: "void",
+ }, throwCb);
+ const fnPointer = new Deno.UnsafeFnPointer(cb.pointer, {
+ parameters: [],
+ result: "void",
+ });
+ assertThrows(() => fnPointer.call());
+ cb.close();
+});
+
+Deno.test(
+ { permissions: { ffi: true }, ignore: true },
+ async function callNonBlockingWithError() {
+ const throwCb = () => {
+ throw new Error("Error");
+ };
+ const cb = new Deno.UnsafeCallback({
+ parameters: [],
+ result: "void",
+ }, throwCb);
+ const fnPointer = new Deno.UnsafeFnPointer(cb.pointer, {
+ parameters: [],
+ result: "void",
+ nonblocking: true,
+ });
+ // TODO(mmastrac): currently ignored as we do not thread callback exceptions through nonblocking pointers
+ await assertRejects(async () => await fnPointer.call());
+ cb.close();
+ },
+);
diff --git a/tests/unit/file_test.ts b/tests/unit/file_test.ts
new file mode 100644
index 000000000..1af3a3f84
--- /dev/null
+++ b/tests/unit/file_test.ts
@@ -0,0 +1,112 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals } from "./test_util.ts";
+
+// deno-lint-ignore no-explicit-any
+function testFirstArgument(arg1: any[], expectedSize: number) {
+ const file = new File(arg1, "name");
+ assert(file instanceof File);
+ assertEquals(file.name, "name");
+ assertEquals(file.size, expectedSize);
+ assertEquals(file.type, "");
+}
+
+Deno.test(function fileEmptyFileBits() {
+ testFirstArgument([], 0);
+});
+
+Deno.test(function fileStringFileBits() {
+ testFirstArgument(["bits"], 4);
+});
+
+Deno.test(function fileUnicodeStringFileBits() {
+ testFirstArgument(["𝓽𝓮𝔁𝓽"], 16);
+});
+
+Deno.test(function fileStringObjectFileBits() {
+ testFirstArgument([new String("string object")], 13);
+});
+
+Deno.test(function fileEmptyBlobFileBits() {
+ testFirstArgument([new Blob()], 0);
+});
+
+Deno.test(function fileBlobFileBits() {
+ testFirstArgument([new Blob(["bits"])], 4);
+});
+
+Deno.test(function fileEmptyFileFileBits() {
+ testFirstArgument([new File([], "world.txt")], 0);
+});
+
+Deno.test(function fileFileFileBits() {
+ testFirstArgument([new File(["bits"], "world.txt")], 4);
+});
+
+Deno.test(function fileArrayBufferFileBits() {
+ testFirstArgument([new ArrayBuffer(8)], 8);
+});
+
+Deno.test(function fileTypedArrayFileBits() {
+ testFirstArgument([new Uint8Array([0x50, 0x41, 0x53, 0x53])], 4);
+});
+
+Deno.test(function fileVariousFileBits() {
+ testFirstArgument(
+ [
+ "bits",
+ new Blob(["bits"]),
+ new Blob(),
+ new Uint8Array([0x50, 0x41]),
+ new Uint16Array([0x5353]),
+ new Uint32Array([0x53534150]),
+ ],
+ 16,
+ );
+});
+
+Deno.test(function fileNumberInFileBits() {
+ testFirstArgument([12], 2);
+});
+
+Deno.test(function fileArrayInFileBits() {
+ testFirstArgument([[1, 2, 3]], 5);
+});
+
+Deno.test(function fileObjectInFileBits() {
+ // "[object Object]"
+ testFirstArgument([{}], 15);
+});
+
+// deno-lint-ignore no-explicit-any
+function testSecondArgument(arg2: any, expectedFileName: string) {
+ const file = new File(["bits"], arg2);
+ assert(file instanceof File);
+ assertEquals(file.name, expectedFileName);
+}
+
+Deno.test(function fileUsingFileName() {
+ testSecondArgument("dummy", "dummy");
+});
+
+Deno.test(function fileUsingNullFileName() {
+ testSecondArgument(null, "null");
+});
+
+Deno.test(function fileUsingNumberFileName() {
+ testSecondArgument(1, "1");
+});
+
+Deno.test(function fileUsingEmptyStringFileName() {
+ testSecondArgument("", "");
+});
+
+Deno.test(function inspectFile() {
+ assertEquals(
+ Deno.inspect(new File([], "file-name.txt")),
+ `File { name: "file-name.txt", size: 0, type: "" }`,
+ );
+ assertEquals(
+ Deno.inspect(new File([], "file-name.txt", { type: "text/plain" })),
+ `File { name: "file-name.txt", size: 0, type: "text/plain" }`,
+ );
+});
diff --git a/tests/unit/filereader_test.ts b/tests/unit/filereader_test.ts
new file mode 100644
index 000000000..158cf5383
--- /dev/null
+++ b/tests/unit/filereader_test.ts
@@ -0,0 +1,242 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+Deno.test(function fileReaderConstruct() {
+ const fr = new FileReader();
+ assertEquals(fr.readyState, FileReader.EMPTY);
+
+ assertEquals(FileReader.EMPTY, 0);
+ assertEquals(FileReader.LOADING, 1);
+ assertEquals(FileReader.DONE, 2);
+});
+
+Deno.test(async function fileReaderLoadBlob() {
+ await new Promise<void>((resolve) => {
+ const fr = new FileReader();
+ const b1 = new Blob(["Hello World"]);
+
+ assertEquals(fr.readyState, FileReader.EMPTY);
+
+ const hasOnEvents = {
+ load: false,
+ loadend: false,
+ loadstart: false,
+ progress: 0,
+ };
+ const hasDispatchedEvents = {
+ load: false,
+ loadend: false,
+ loadstart: false,
+ progress: 0,
+ };
+ let result: string | null = null;
+
+ fr.addEventListener("load", () => {
+ hasDispatchedEvents.load = true;
+ });
+ fr.addEventListener("loadend", () => {
+ hasDispatchedEvents.loadend = true;
+ });
+ fr.addEventListener("loadstart", () => {
+ hasDispatchedEvents.loadstart = true;
+ });
+ fr.addEventListener("progress", () => {
+ hasDispatchedEvents.progress += 1;
+ });
+
+ fr.onloadstart = () => {
+ hasOnEvents.loadstart = true;
+ };
+ fr.onprogress = () => {
+ assertEquals(fr.readyState, FileReader.LOADING);
+
+ hasOnEvents.progress += 1;
+ };
+ fr.onload = () => {
+ hasOnEvents.load = true;
+ };
+ fr.onloadend = (ev) => {
+ hasOnEvents.loadend = true;
+ result = fr.result as string;
+
+ assertEquals(hasOnEvents.loadstart, true);
+ assertEquals(hasDispatchedEvents.loadstart, true);
+ assertEquals(hasOnEvents.load, true);
+ assertEquals(hasDispatchedEvents.load, true);
+ assertEquals(hasOnEvents.loadend, true);
+ assertEquals(hasDispatchedEvents.loadend, true);
+
+ assertEquals(fr.readyState, FileReader.DONE);
+
+ assertEquals(result, "Hello World");
+ assertEquals(ev.lengthComputable, true);
+ resolve();
+ };
+
+ fr.readAsText(b1);
+ });
+});
+
+Deno.test(async function fileReaderLoadBlobDouble() {
+ // impl note from https://w3c.github.io/FileAPI/
+ // Event handler for the load or error events could have started another load,
+ // if that happens the loadend event for the first load is not fired
+
+ const fr = new FileReader();
+ const b1 = new Blob(["First load"]);
+ const b2 = new Blob(["Second load"]);
+
+ await new Promise<void>((resolve) => {
+ let result: string | null = null;
+
+ fr.onload = () => {
+ result = fr.result as string;
+ assertEquals(result === "First load" || result === "Second load", true);
+
+ if (result === "First load") {
+ fr.readAsText(b2);
+ }
+ };
+ fr.onloadend = () => {
+ assertEquals(result, "Second load");
+
+ resolve();
+ };
+
+ fr.readAsText(b1);
+ });
+});
+
+Deno.test(async function fileReaderLoadBlobArrayBuffer() {
+ await new Promise<void>((resolve) => {
+ const fr = new FileReader();
+ const b1 = new Blob(["Hello World"]);
+ let result: ArrayBuffer | null = null;
+
+ fr.onloadend = (ev) => {
+ assertEquals(fr.result instanceof ArrayBuffer, true);
+ result = fr.result as ArrayBuffer;
+
+ const decoder = new TextDecoder();
+ const text = decoder.decode(result);
+
+ assertEquals(text, "Hello World");
+ assertEquals(ev.lengthComputable, true);
+ resolve();
+ };
+
+ fr.readAsArrayBuffer(b1);
+ });
+});
+
+Deno.test(async function fileReaderLoadBlobDataUrl() {
+ await new Promise<void>((resolve) => {
+ const fr = new FileReader();
+ const b1 = new Blob(["Hello World"]);
+ let result: string | null = null;
+
+ fr.onloadend = (ev) => {
+ result = fr.result as string;
+ assertEquals(
+ result,
+ "data:application/octet-stream;base64,SGVsbG8gV29ybGQ=",
+ );
+ assertEquals(ev.lengthComputable, true);
+ resolve();
+ };
+
+ fr.readAsDataURL(b1);
+ });
+});
+
+Deno.test(async function fileReaderLoadBlobAbort() {
+ await new Promise<void>((resolve) => {
+ const fr = new FileReader();
+ const b1 = new Blob(["Hello World"]);
+
+ const hasOnEvents = {
+ load: false,
+ loadend: false,
+ abort: false,
+ };
+
+ fr.onload = () => {
+ hasOnEvents.load = true;
+ };
+ fr.onloadend = (ev) => {
+ hasOnEvents.loadend = true;
+
+ assertEquals(hasOnEvents.load, false);
+ assertEquals(hasOnEvents.loadend, true);
+ assertEquals(hasOnEvents.abort, true);
+
+ assertEquals(fr.readyState, FileReader.DONE);
+ assertEquals(fr.result, null);
+ assertEquals(ev.lengthComputable, false);
+ resolve();
+ };
+ fr.onabort = () => {
+ hasOnEvents.abort = true;
+ };
+
+ fr.readAsDataURL(b1);
+ fr.abort();
+ });
+});
+
+Deno.test(async function fileReaderLoadBlobAbort() {
+ await new Promise<void>((resolve) => {
+ const fr = new FileReader();
+ const b1 = new Blob(["Hello World"]);
+
+ const hasOnEvents = {
+ load: false,
+ loadend: false,
+ abort: false,
+ };
+
+ fr.onload = () => {
+ hasOnEvents.load = true;
+ };
+ fr.onloadend = (ev) => {
+ hasOnEvents.loadend = true;
+
+ assertEquals(hasOnEvents.load, false);
+ assertEquals(hasOnEvents.loadend, true);
+ assertEquals(hasOnEvents.abort, true);
+
+ assertEquals(fr.readyState, FileReader.DONE);
+ assertEquals(fr.result, null);
+ assertEquals(ev.lengthComputable, false);
+ resolve();
+ };
+ fr.onabort = () => {
+ hasOnEvents.abort = true;
+ };
+
+ fr.readAsDataURL(b1);
+ fr.abort();
+ });
+});
+
+Deno.test(
+ async function fileReaderDispatchesEventsInCorrectOrder() {
+ await new Promise<void>((resolve) => {
+ const fr = new FileReader();
+ const b1 = new Blob(["Hello World"]);
+ let out = "";
+ fr.addEventListener("loadend", () => {
+ out += "1";
+ });
+ fr.onloadend = (_ev) => {
+ out += "2";
+ };
+ fr.addEventListener("loadend", () => {
+ assertEquals(out, "12");
+ resolve();
+ });
+
+ fr.readAsDataURL(b1);
+ });
+ },
+);
diff --git a/tests/unit/files_test.ts b/tests/unit/files_test.ts
new file mode 100644
index 000000000..c29092963
--- /dev/null
+++ b/tests/unit/files_test.ts
@@ -0,0 +1,1095 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+// deno-lint-ignore-file no-deprecated-deno-api
+
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+} from "./test_util.ts";
+import { copy } from "@test_util/std/streams/copy.ts";
+
+Deno.test(function filesStdioFileDescriptors() {
+ assertEquals(Deno.stdin.rid, 0);
+ assertEquals(Deno.stdout.rid, 1);
+ assertEquals(Deno.stderr.rid, 2);
+});
+
+Deno.test({ permissions: { read: true } }, async function filesCopyToStdout() {
+ const filename = "tests/testdata/assets/fixture.json";
+ using file = await Deno.open(filename);
+ assert(file instanceof Deno.File);
+ assert(file instanceof Deno.FsFile);
+ assert(file.rid > 2);
+ const bytesWritten = await copy(file, Deno.stdout);
+ const fileSize = Deno.statSync(filename).size;
+ assertEquals(bytesWritten, fileSize);
+});
+
+Deno.test({ permissions: { read: true } }, async function filesIter() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = await Deno.open(filename);
+
+ let totalSize = 0;
+ for await (const buf of Deno.iter(file)) {
+ totalSize += buf.byteLength;
+ }
+
+ assertEquals(totalSize, 12);
+});
+
+Deno.test(
+ { permissions: { read: true } },
+ async function filesIterCustomBufSize() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = await Deno.open(filename);
+
+ let totalSize = 0;
+ let iterations = 0;
+ for await (const buf of Deno.iter(file, { bufSize: 6 })) {
+ totalSize += buf.byteLength;
+ iterations += 1;
+ }
+
+ assertEquals(totalSize, 12);
+ assertEquals(iterations, 2);
+ },
+);
+
+Deno.test({ permissions: { read: true } }, function filesIterSync() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = Deno.openSync(filename);
+
+ let totalSize = 0;
+ for (const buf of Deno.iterSync(file)) {
+ totalSize += buf.byteLength;
+ }
+
+ assertEquals(totalSize, 12);
+});
+
+Deno.test(
+ { permissions: { read: true } },
+ function filesIterSyncCustomBufSize() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = Deno.openSync(filename);
+
+ let totalSize = 0;
+ let iterations = 0;
+ for (const buf of Deno.iterSync(file, { bufSize: 6 })) {
+ totalSize += buf.byteLength;
+ iterations += 1;
+ }
+
+ assertEquals(totalSize, 12);
+ assertEquals(iterations, 2);
+ },
+);
+
+Deno.test(async function readerIter() {
+ // ref: https://github.com/denoland/deno/issues/2330
+ const encoder = new TextEncoder();
+
+ class TestReader implements Deno.Reader {
+ #offset = 0;
+ #buf: Uint8Array;
+
+ constructor(s: string) {
+ this.#buf = new Uint8Array(encoder.encode(s));
+ }
+
+ read(p: Uint8Array): Promise<number | null> {
+ const n = Math.min(p.byteLength, this.#buf.byteLength - this.#offset);
+ p.set(this.#buf.slice(this.#offset, this.#offset + n));
+ this.#offset += n;
+
+ if (n === 0) {
+ return Promise.resolve(null);
+ }
+
+ return Promise.resolve(n);
+ }
+ }
+
+ const reader = new TestReader("hello world!");
+
+ let totalSize = 0;
+ for await (const buf of Deno.iter(reader)) {
+ totalSize += buf.byteLength;
+ }
+
+ assertEquals(totalSize, 12);
+});
+
+Deno.test(async function readerIterSync() {
+ // ref: https://github.com/denoland/deno/issues/2330
+ const encoder = new TextEncoder();
+
+ class TestReader implements Deno.ReaderSync {
+ #offset = 0;
+ #buf: Uint8Array;
+
+ constructor(s: string) {
+ this.#buf = new Uint8Array(encoder.encode(s));
+ }
+
+ readSync(p: Uint8Array): number | null {
+ const n = Math.min(p.byteLength, this.#buf.byteLength - this.#offset);
+ p.set(this.#buf.slice(this.#offset, this.#offset + n));
+ this.#offset += n;
+
+ if (n === 0) {
+ return null;
+ }
+
+ return n;
+ }
+ }
+
+ const reader = new TestReader("hello world!");
+
+ let totalSize = 0;
+ for await (const buf of Deno.iterSync(reader)) {
+ totalSize += buf.byteLength;
+ }
+
+ assertEquals(totalSize, 12);
+});
+
+Deno.test(
+ {
+ permissions: { read: true, write: true },
+ },
+ function openSyncMode() {
+ const path = Deno.makeTempDirSync() + "/test_openSync.txt";
+ using _file = Deno.openSync(path, {
+ write: true,
+ createNew: true,
+ mode: 0o626,
+ });
+ const pathInfo = Deno.statSync(path);
+ if (Deno.build.os !== "windows") {
+ assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask());
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { read: true, write: true },
+ },
+ async function openMode() {
+ const path = (await Deno.makeTempDir()) + "/test_open.txt";
+ using _file = await Deno.open(path, {
+ write: true,
+ createNew: true,
+ mode: 0o626,
+ });
+ const pathInfo = Deno.statSync(path);
+ if (Deno.build.os !== "windows") {
+ assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask());
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { read: true, write: true },
+ },
+ function openSyncUrl() {
+ const tempDir = Deno.makeTempDirSync();
+ const fileUrl = new URL(
+ `file://${
+ Deno.build.os === "windows" ? "/" : ""
+ }${tempDir}/test_open.txt`,
+ );
+ using _file = Deno.openSync(fileUrl, {
+ write: true,
+ createNew: true,
+ mode: 0o626,
+ });
+ const pathInfo = Deno.statSync(fileUrl);
+ if (Deno.build.os !== "windows") {
+ assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask());
+ }
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ {
+ permissions: { read: true, write: true },
+ },
+ async function openUrl() {
+ const tempDir = await Deno.makeTempDir();
+ const fileUrl = new URL(
+ `file://${
+ Deno.build.os === "windows" ? "/" : ""
+ }${tempDir}/test_open.txt`,
+ );
+ using _file = await Deno.open(fileUrl, {
+ write: true,
+ createNew: true,
+ mode: 0o626,
+ });
+ const pathInfo = Deno.statSync(fileUrl);
+ if (Deno.build.os !== "windows") {
+ assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask());
+ }
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { write: false } },
+ async function writePermFailure() {
+ const filename = "tests/hello.txt";
+ const openOptions: Deno.OpenOptions[] = [{ write: true }, { append: true }];
+ for (const options of openOptions) {
+ await assertRejects(async () => {
+ await Deno.open(filename, options);
+ }, Deno.errors.PermissionDenied);
+ }
+ },
+);
+
+Deno.test(async function openOptions() {
+ const filename = "tests/testdata/assets/fixture.json";
+ await assertRejects(
+ async () => {
+ await Deno.open(filename, { write: false });
+ },
+ Error,
+ "OpenOptions requires at least one option to be true",
+ );
+
+ await assertRejects(
+ async () => {
+ await Deno.open(filename, { truncate: true, write: false });
+ },
+ Error,
+ "'truncate' option requires 'write' option",
+ );
+
+ await assertRejects(
+ async () => {
+ await Deno.open(filename, { create: true, write: false });
+ },
+ Error,
+ "'create' or 'createNew' options require 'write' or 'append' option",
+ );
+
+ await assertRejects(
+ async () => {
+ await Deno.open(filename, { createNew: true, append: false });
+ },
+ Error,
+ "'create' or 'createNew' options require 'write' or 'append' option",
+ );
+});
+
+Deno.test({ permissions: { read: false } }, async function readPermFailure() {
+ await assertRejects(async () => {
+ await Deno.open("package.json", { read: true });
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { write: true } },
+ async function writeNullBufferFailure() {
+ const tempDir = Deno.makeTempDirSync();
+ const filename = tempDir + "hello.txt";
+ const w = {
+ write: true,
+ truncate: true,
+ create: true,
+ };
+ using file = await Deno.open(filename, w);
+
+ // writing null should throw an error
+ await assertRejects(
+ async () => {
+ // deno-lint-ignore no-explicit-any
+ await file.write(null as any);
+ },
+ ); // TODO(bartlomieju): Check error kind when dispatch_minimal pipes errors properly
+ await Deno.remove(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function readNullBufferFailure() {
+ const tempDir = Deno.makeTempDirSync();
+ const filename = tempDir + "hello.txt";
+ using file = await Deno.open(filename, {
+ read: true,
+ write: true,
+ truncate: true,
+ create: true,
+ });
+
+ // reading into an empty buffer should return 0 immediately
+ const bytesRead = await file.read(new Uint8Array(0));
+ assert(bytesRead === 0);
+
+ // reading file into null buffer should throw an error
+ await assertRejects(async () => {
+ // deno-lint-ignore no-explicit-any
+ await file.read(null as any);
+ }, TypeError);
+ // TODO(bartlomieju): Check error kind when dispatch_minimal pipes errors properly
+
+ await Deno.remove(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { write: false, read: false } },
+ async function readWritePermFailure() {
+ const filename = "tests/hello.txt";
+ await assertRejects(async () => {
+ await Deno.open(filename, { read: true });
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function openNotFound() {
+ await assertRejects(
+ async () => {
+ await Deno.open("bad_file_name");
+ },
+ Deno.errors.NotFound,
+ `open 'bad_file_name'`,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ function openSyncNotFound() {
+ assertThrows(
+ () => {
+ Deno.openSync("bad_file_name");
+ },
+ Deno.errors.NotFound,
+ `open 'bad_file_name'`,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function createFile() {
+ const tempDir = await Deno.makeTempDir();
+ const filename = tempDir + "/test.txt";
+ const f = await Deno.create(filename);
+ let fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile);
+ assert(fileInfo.size === 0);
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ await f.write(data);
+ fileInfo = Deno.statSync(filename);
+ assert(fileInfo.size === 5);
+ f.close();
+
+ // TODO(bartlomieju): test different modes
+ await Deno.remove(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function createFileWithUrl() {
+ const tempDir = await Deno.makeTempDir();
+ const fileUrl = new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/test.txt`,
+ );
+ const f = await Deno.create(fileUrl);
+ let fileInfo = Deno.statSync(fileUrl);
+ assert(fileInfo.isFile);
+ assert(fileInfo.size === 0);
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ await f.write(data);
+ fileInfo = Deno.statSync(fileUrl);
+ assert(fileInfo.size === 5);
+ f.close();
+
+ await Deno.remove(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function createSyncFile() {
+ const tempDir = await Deno.makeTempDir();
+ const filename = tempDir + "/test.txt";
+ const f = Deno.createSync(filename);
+ let fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile);
+ assert(fileInfo.size === 0);
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ await f.write(data);
+ fileInfo = Deno.statSync(filename);
+ assert(fileInfo.size === 5);
+ f.close();
+
+ // TODO(bartlomieju): test different modes
+ await Deno.remove(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function createSyncFileWithUrl() {
+ const tempDir = await Deno.makeTempDir();
+ const fileUrl = new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/test.txt`,
+ );
+ const f = Deno.createSync(fileUrl);
+ let fileInfo = Deno.statSync(fileUrl);
+ assert(fileInfo.isFile);
+ assert(fileInfo.size === 0);
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ await f.write(data);
+ fileInfo = Deno.statSync(fileUrl);
+ assert(fileInfo.size === 5);
+ f.close();
+
+ await Deno.remove(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function openModeWrite() {
+ const tempDir = Deno.makeTempDirSync();
+ const encoder = new TextEncoder();
+ const filename = tempDir + "hello.txt";
+ const data = encoder.encode("Hello world!\n");
+ let file = await Deno.open(filename, {
+ create: true,
+ write: true,
+ truncate: true,
+ });
+ // assert file was created
+ let fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile);
+ assertEquals(fileInfo.size, 0);
+ // write some data
+ await file.write(data);
+ fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.size, 13);
+ // assert we can't read from file
+ let thrown = false;
+ try {
+ const buf = new Uint8Array(20);
+ await file.read(buf);
+ } catch (_e) {
+ thrown = true;
+ } finally {
+ assert(thrown, "'w' mode shouldn't allow to read file");
+ }
+ file.close();
+ // assert that existing file is truncated on open
+ file = await Deno.open(filename, {
+ write: true,
+ truncate: true,
+ });
+ file.close();
+ const fileSize = Deno.statSync(filename).size;
+ assertEquals(fileSize, 0);
+ await Deno.remove(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function openModeWriteRead() {
+ const tempDir = Deno.makeTempDirSync();
+ const encoder = new TextEncoder();
+ const filename = tempDir + "hello.txt";
+ const data = encoder.encode("Hello world!\n");
+
+ using file = await Deno.open(filename, {
+ write: true,
+ truncate: true,
+ create: true,
+ read: true,
+ });
+ const seekPosition = 0;
+ // assert file was created
+ let fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile);
+ assertEquals(fileInfo.size, 0);
+ // write some data
+ await file.write(data);
+ fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.size, 13);
+
+ const buf = new Uint8Array(20);
+ // seeking from beginning of a file
+ const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Start);
+ assertEquals(seekPosition, cursorPosition);
+ const result = await file.read(buf);
+ assertEquals(result, 13);
+
+ await Deno.remove(tempDir, { recursive: true });
+ },
+);
+
+Deno.test({ permissions: { read: true } }, async function seekStart() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = await Deno.open(filename);
+ const seekPosition = 6;
+ // Deliberately move 1 step forward
+ await file.read(new Uint8Array(1)); // "H"
+ // Skipping "Hello "
+ // seeking from beginning of a file plus seekPosition
+ const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Start);
+ assertEquals(seekPosition, cursorPosition);
+ const buf = new Uint8Array(6);
+ await file.read(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+Deno.test({ permissions: { read: true } }, async function seekStartBigInt() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = await Deno.open(filename);
+ const seekPosition = 6n;
+ // Deliberately move 1 step forward
+ await file.read(new Uint8Array(1)); // "H"
+ // Skipping "Hello "
+ // seeking from beginning of a file plus seekPosition
+ const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Start);
+ assertEquals(seekPosition, BigInt(cursorPosition));
+ const buf = new Uint8Array(6);
+ await file.read(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+Deno.test({ permissions: { read: true } }, function seekSyncStart() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = Deno.openSync(filename);
+ const seekPosition = 6;
+ // Deliberately move 1 step forward
+ file.readSync(new Uint8Array(1)); // "H"
+ // Skipping "Hello "
+ // seeking from beginning of a file plus seekPosition
+ const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.Start);
+ assertEquals(seekPosition, cursorPosition);
+ const buf = new Uint8Array(6);
+ file.readSync(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+Deno.test({ permissions: { read: true } }, async function seekCurrent() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = await Deno.open(filename);
+ // Deliberately move 1 step forward
+ await file.read(new Uint8Array(1)); // "H"
+ // Skipping "ello "
+ const seekPosition = 5;
+ // seekPosition is relative to current cursor position after read
+ const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Current);
+ assertEquals(seekPosition + 1, cursorPosition);
+ const buf = new Uint8Array(6);
+ await file.read(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+Deno.test({ permissions: { read: true } }, function seekSyncCurrent() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = Deno.openSync(filename);
+ // Deliberately move 1 step forward
+ file.readSync(new Uint8Array(1)); // "H"
+ // Skipping "ello "
+ const seekPosition = 5;
+ // seekPosition is relative to current cursor position after read
+ const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.Current);
+ assertEquals(seekPosition + 1, cursorPosition);
+ const buf = new Uint8Array(6);
+ file.readSync(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+Deno.test({ permissions: { read: true } }, async function seekEnd() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = await Deno.open(filename);
+ const seekPosition = -6;
+ // seek from end of file that has 12 chars, 12 - 6 = 6
+ const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.End);
+ assertEquals(6, cursorPosition);
+ const buf = new Uint8Array(6);
+ await file.read(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+Deno.test({ permissions: { read: true } }, function seekSyncEnd() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = Deno.openSync(filename);
+ const seekPosition = -6;
+ // seek from end of file that has 12 chars, 12 - 6 = 6
+ const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.End);
+ assertEquals(6, cursorPosition);
+ const buf = new Uint8Array(6);
+ file.readSync(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+Deno.test({ permissions: { read: true } }, async function seekMode() {
+ const filename = "tests/testdata/assets/hello.txt";
+ using file = await Deno.open(filename);
+ await assertRejects(
+ async () => {
+ await file.seek(1, -1 as unknown as Deno.SeekMode);
+ },
+ TypeError,
+ "Invalid seek mode",
+ );
+
+ // We should still be able to read the file
+ // since it is still open.
+ const buf = new Uint8Array(1);
+ await file.read(buf); // "H"
+ assertEquals(new TextDecoder().decode(buf), "H");
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function fileTruncateSyncSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test_fileTruncateSync.txt";
+ using file = Deno.openSync(filename, {
+ create: true,
+ read: true,
+ write: true,
+ });
+
+ file.truncateSync(20);
+ assertEquals(Deno.readFileSync(filename).byteLength, 20);
+ file.truncateSync(5);
+ assertEquals(Deno.readFileSync(filename).byteLength, 5);
+ file.truncateSync(-5);
+ assertEquals(Deno.readFileSync(filename).byteLength, 0);
+
+ Deno.removeSync(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function fileTruncateSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test_fileTruncate.txt";
+ using file = await Deno.open(filename, {
+ create: true,
+ read: true,
+ write: true,
+ });
+
+ await file.truncate(20);
+ assertEquals((await Deno.readFile(filename)).byteLength, 20);
+ await file.truncate(5);
+ assertEquals((await Deno.readFile(filename)).byteLength, 5);
+ await file.truncate(-5);
+ assertEquals((await Deno.readFile(filename)).byteLength, 0);
+
+ await Deno.remove(filename);
+ },
+);
+
+Deno.test({ permissions: { read: true } }, function fileStatSyncSuccess() {
+ using file = Deno.openSync("README.md");
+ const fileInfo = file.statSync();
+ assert(fileInfo.isFile);
+ assert(!fileInfo.isSymlink);
+ assert(!fileInfo.isDirectory);
+ assert(fileInfo.size);
+ assert(fileInfo.atime);
+ assert(fileInfo.mtime);
+ // The `birthtime` field is not available on Linux before kernel version 4.11.
+ assert(fileInfo.birthtime || Deno.build.os === "linux");
+});
+
+Deno.test(async function fileStatSuccess() {
+ using file = await Deno.open("README.md");
+ const fileInfo = await file.stat();
+ assert(fileInfo.isFile);
+ assert(!fileInfo.isSymlink);
+ assert(!fileInfo.isDirectory);
+ assert(fileInfo.size);
+ assert(fileInfo.atime);
+ assert(fileInfo.mtime);
+ // The `birthtime` field is not available on Linux before kernel version 4.11.
+ assert(fileInfo.birthtime || Deno.build.os === "linux");
+});
+
+Deno.test({ permissions: { read: true } }, async function readableStream() {
+ const filename = "tests/testdata/assets/hello.txt";
+ const file = await Deno.open(filename);
+ assert(file.readable instanceof ReadableStream);
+ const chunks = [];
+ for await (const chunk of file.readable) {
+ chunks.push(chunk);
+ }
+ assertEquals(chunks.length, 1);
+ assertEquals(chunks[0].byteLength, 12);
+});
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readableStreamTextEncoderPipe() {
+ const filename = "tests/testdata/assets/hello.txt";
+ const file = await Deno.open(filename);
+ const readable = file.readable.pipeThrough(new TextDecoderStream());
+ const chunks = [];
+ for await (const chunk of readable) {
+ chunks.push(chunk);
+ }
+ assertEquals(chunks.length, 1);
+ assertEquals(chunks[0].length, 12);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writableStream() {
+ const path = await Deno.makeTempFile();
+ const file = await Deno.open(path, { write: true });
+ assert(file.writable instanceof WritableStream);
+ const readable = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new TextEncoder().encode("hello "));
+ controller.enqueue(new TextEncoder().encode("world!"));
+ controller.close();
+ },
+ });
+ await readable.pipeTo(file.writable);
+ const res = await Deno.readTextFile(path);
+ assertEquals(res, "hello world!");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function readTextFileNonUtf8() {
+ const path = await Deno.makeTempFile();
+ using file = await Deno.open(path, { write: true });
+ await file.write(new TextEncoder().encode("hello "));
+ await file.write(new Uint8Array([0xC0]));
+
+ const res = await Deno.readTextFile(path);
+ const resSync = Deno.readTextFileSync(path);
+ assertEquals(res, resSync);
+ assertEquals(res, "hello \uFFFD");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function fsFileExplicitResourceManagement() {
+ let file2: Deno.FsFile;
+
+ {
+ using file = await Deno.open("tests/testdata/assets/hello.txt");
+ file2 = file;
+
+ const stat = file.statSync();
+ assert(stat.isFile);
+ }
+
+ assertThrows(() => file2.statSync(), Deno.errors.BadResource);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function fsFileExplicitResourceManagementManualClose() {
+ using file = await Deno.open("tests/testdata/assets/hello.txt");
+ file.close();
+ assertThrows(() => file.statSync(), Deno.errors.BadResource); // definitely closed
+ // calling [Symbol.dispose] after manual close is a no-op
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function fsFileDatasyncSyncSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test_fdatasyncSync.txt";
+ const file = Deno.openSync(filename, {
+ read: true,
+ write: true,
+ create: true,
+ });
+ const data = new Uint8Array(64);
+ file.writeSync(data);
+ file.syncDataSync();
+ assertEquals(Deno.readFileSync(filename), data);
+ file.close();
+ Deno.removeSync(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function fsFileDatasyncSuccess() {
+ const filename = (await Deno.makeTempDir()) + "/test_fdatasync.txt";
+ const file = await Deno.open(filename, {
+ read: true,
+ write: true,
+ create: true,
+ });
+ const data = new Uint8Array(64);
+ await file.write(data);
+ await file.syncData();
+ assertEquals(await Deno.readFile(filename), data);
+ file.close();
+ await Deno.remove(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function fsFileSyncSyncSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test_fsyncSync.txt";
+ const file = Deno.openSync(filename, {
+ read: true,
+ write: true,
+ create: true,
+ });
+ const size = 64;
+ file.truncateSync(size);
+ file.syncSync();
+ assertEquals(file.statSync().size, size);
+ file.close();
+ Deno.removeSync(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function fsFileSyncSuccess() {
+ const filename = (await Deno.makeTempDir()) + "/test_fsync.txt";
+ const file = await Deno.open(filename, {
+ read: true,
+ write: true,
+ create: true,
+ });
+ const size = 64;
+ await file.truncate(size);
+ await file.sync();
+ assertEquals((await file.stat()).size, size);
+ file.close();
+ await Deno.remove(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: true, hrtime: true } },
+ async function fsFileLockFileSync() {
+ await runFlockTests({ sync: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: true, hrtime: true } },
+ async function fsFileLockFileAsync() {
+ await runFlockTests({ sync: false });
+ },
+);
+
+async function runFlockTests(opts: { sync: boolean }) {
+ assertEquals(
+ await checkFirstBlocksSecond({
+ firstExclusive: true,
+ secondExclusive: false,
+ sync: opts.sync,
+ }),
+ true,
+ "exclusive blocks shared",
+ );
+ assertEquals(
+ await checkFirstBlocksSecond({
+ firstExclusive: false,
+ secondExclusive: true,
+ sync: opts.sync,
+ }),
+ true,
+ "shared blocks exclusive",
+ );
+ assertEquals(
+ await checkFirstBlocksSecond({
+ firstExclusive: true,
+ secondExclusive: true,
+ sync: opts.sync,
+ }),
+ true,
+ "exclusive blocks exclusive",
+ );
+ assertEquals(
+ await checkFirstBlocksSecond({
+ firstExclusive: false,
+ secondExclusive: false,
+ sync: opts.sync,
+ // need to wait for both to enter the lock to prevent the case where the
+ // first process enters and exits the lock before the second even enters
+ waitBothEnteredLock: true,
+ }),
+ false,
+ "shared does not block shared",
+ );
+}
+
+async function checkFirstBlocksSecond(opts: {
+ firstExclusive: boolean;
+ secondExclusive: boolean;
+ sync: boolean;
+ waitBothEnteredLock?: boolean;
+}) {
+ const firstProcess = runFlockTestProcess({
+ exclusive: opts.firstExclusive,
+ sync: opts.sync,
+ });
+ const secondProcess = runFlockTestProcess({
+ exclusive: opts.secondExclusive,
+ sync: opts.sync,
+ });
+ try {
+ const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
+
+ await Promise.all([
+ firstProcess.waitStartup(),
+ secondProcess.waitStartup(),
+ ]);
+
+ await firstProcess.enterLock();
+ await firstProcess.waitEnterLock();
+
+ await secondProcess.enterLock();
+ await sleep(100);
+
+ if (!opts.waitBothEnteredLock) {
+ await firstProcess.exitLock();
+ }
+
+ await secondProcess.waitEnterLock();
+
+ if (opts.waitBothEnteredLock) {
+ await firstProcess.exitLock();
+ }
+
+ await secondProcess.exitLock();
+
+ // collect the final output
+ const firstPsTimes = await firstProcess.getTimes();
+ const secondPsTimes = await secondProcess.getTimes();
+ return firstPsTimes.exitTime < secondPsTimes.enterTime;
+ } finally {
+ await firstProcess.close();
+ await secondProcess.close();
+ }
+}
+
+function runFlockTestProcess(opts: { exclusive: boolean; sync: boolean }) {
+ const path = "tests/testdata/assets/lock_target.txt";
+ const scriptText = `
+ const file = Deno.openSync("${path}");
+
+ // ready signal
+ Deno.stdout.writeSync(new Uint8Array(1));
+ // wait for enter lock signal
+ Deno.stdin.readSync(new Uint8Array(1));
+
+ // entering signal
+ Deno.stdout.writeSync(new Uint8Array(1));
+ // lock and record the entry time
+ ${
+ opts.sync
+ ? `file.lockSync(${opts.exclusive ? "true" : "false"});`
+ : `await file.lock(${opts.exclusive ? "true" : "false"});`
+ }
+ const enterTime = new Date().getTime();
+ // entered signal
+ Deno.stdout.writeSync(new Uint8Array(1));
+
+ // wait for exit lock signal
+ Deno.stdin.readSync(new Uint8Array(1));
+
+ // record the exit time and wait a little bit before releasing
+ // the lock so that the enter time of the next process doesn't
+ // occur at the same time as this exit time
+ const exitTime = new Date().getTime();
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ // release the lock
+ ${opts.sync ? "file.unlockSync();" : "await file.unlock();"}
+
+ // exited signal
+ Deno.stdout.writeSync(new Uint8Array(1));
+
+ // output the enter and exit time
+ console.log(JSON.stringify({ enterTime, exitTime }));
+`;
+
+ const process = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "--unstable", scriptText],
+ stdin: "piped",
+ stdout: "piped",
+ stderr: "null",
+ }).spawn();
+
+ const waitSignal = async () => {
+ const reader = process.stdout.getReader({ mode: "byob" });
+ await reader.read(new Uint8Array(1));
+ reader.releaseLock();
+ };
+ const signal = async () => {
+ const writer = process.stdin.getWriter();
+ await writer.write(new Uint8Array(1));
+ writer.releaseLock();
+ };
+
+ return {
+ async waitStartup() {
+ await waitSignal();
+ },
+ async enterLock() {
+ await signal();
+ await waitSignal(); // entering signal
+ },
+ async waitEnterLock() {
+ await waitSignal();
+ },
+ async exitLock() {
+ await signal();
+ await waitSignal();
+ },
+ getTimes: async () => {
+ const { stdout } = await process.output();
+ const text = new TextDecoder().decode(stdout);
+ return JSON.parse(text) as {
+ enterTime: number;
+ exitTime: number;
+ };
+ },
+ close: async () => {
+ await process.status;
+ await process.stdin.close();
+ },
+ };
+}
diff --git a/tests/unit/flock_test.ts b/tests/unit/flock_test.ts
new file mode 100644
index 000000000..4b194ce55
--- /dev/null
+++ b/tests/unit/flock_test.ts
@@ -0,0 +1,197 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+Deno.test(
+ { permissions: { read: true, run: true, hrtime: true } },
+ async function flockFileSync() {
+ await runFlockTests({ sync: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: true, hrtime: true } },
+ async function flockFileAsync() {
+ await runFlockTests({ sync: false });
+ },
+);
+
+async function runFlockTests(opts: { sync: boolean }) {
+ assertEquals(
+ await checkFirstBlocksSecond({
+ firstExclusive: true,
+ secondExclusive: false,
+ sync: opts.sync,
+ }),
+ true,
+ "exclusive blocks shared",
+ );
+ assertEquals(
+ await checkFirstBlocksSecond({
+ firstExclusive: false,
+ secondExclusive: true,
+ sync: opts.sync,
+ }),
+ true,
+ "shared blocks exclusive",
+ );
+ assertEquals(
+ await checkFirstBlocksSecond({
+ firstExclusive: true,
+ secondExclusive: true,
+ sync: opts.sync,
+ }),
+ true,
+ "exclusive blocks exclusive",
+ );
+ assertEquals(
+ await checkFirstBlocksSecond({
+ firstExclusive: false,
+ secondExclusive: false,
+ sync: opts.sync,
+ // need to wait for both to enter the lock to prevent the case where the
+ // first process enters and exits the lock before the second even enters
+ waitBothEnteredLock: true,
+ }),
+ false,
+ "shared does not block shared",
+ );
+}
+
+async function checkFirstBlocksSecond(opts: {
+ firstExclusive: boolean;
+ secondExclusive: boolean;
+ sync: boolean;
+ waitBothEnteredLock?: boolean;
+}) {
+ const firstProcess = runFlockTestProcess({
+ exclusive: opts.firstExclusive,
+ sync: opts.sync,
+ });
+ const secondProcess = runFlockTestProcess({
+ exclusive: opts.secondExclusive,
+ sync: opts.sync,
+ });
+ try {
+ const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
+
+ await Promise.all([
+ firstProcess.waitStartup(),
+ secondProcess.waitStartup(),
+ ]);
+
+ await firstProcess.enterLock();
+ await firstProcess.waitEnterLock();
+
+ await secondProcess.enterLock();
+ await sleep(100);
+
+ if (!opts.waitBothEnteredLock) {
+ await firstProcess.exitLock();
+ }
+
+ await secondProcess.waitEnterLock();
+
+ if (opts.waitBothEnteredLock) {
+ await firstProcess.exitLock();
+ }
+
+ await secondProcess.exitLock();
+
+ // collect the final output
+ const firstPsTimes = await firstProcess.getTimes();
+ const secondPsTimes = await secondProcess.getTimes();
+ return firstPsTimes.exitTime < secondPsTimes.enterTime;
+ } finally {
+ await firstProcess.close();
+ await secondProcess.close();
+ }
+}
+
+function runFlockTestProcess(opts: { exclusive: boolean; sync: boolean }) {
+ const path = "tests/testdata/assets/lock_target.txt";
+ const scriptText = `
+ const { rid } = Deno.openSync("${path}");
+
+ // ready signal
+ Deno.stdout.writeSync(new Uint8Array(1));
+ // wait for enter lock signal
+ Deno.stdin.readSync(new Uint8Array(1));
+
+ // entering signal
+ Deno.stdout.writeSync(new Uint8Array(1));
+ // lock and record the entry time
+ ${
+ opts.sync
+ ? `Deno.flockSync(rid, ${opts.exclusive ? "true" : "false"});`
+ : `await Deno.flock(rid, ${opts.exclusive ? "true" : "false"});`
+ }
+ const enterTime = new Date().getTime();
+ // entered signal
+ Deno.stdout.writeSync(new Uint8Array(1));
+
+ // wait for exit lock signal
+ Deno.stdin.readSync(new Uint8Array(1));
+
+ // record the exit time and wait a little bit before releasing
+ // the lock so that the enter time of the next process doesn't
+ // occur at the same time as this exit time
+ const exitTime = new Date().getTime();
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ // release the lock
+ ${opts.sync ? "Deno.funlockSync(rid);" : "await Deno.funlock(rid);"}
+
+ // exited signal
+ Deno.stdout.writeSync(new Uint8Array(1));
+
+ // output the enter and exit time
+ console.log(JSON.stringify({ enterTime, exitTime }));
+`;
+
+ const process = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "--unstable", scriptText],
+ stdin: "piped",
+ stdout: "piped",
+ stderr: "null",
+ }).spawn();
+
+ const waitSignal = async () => {
+ const reader = process.stdout.getReader({ mode: "byob" });
+ await reader.read(new Uint8Array(1));
+ reader.releaseLock();
+ };
+ const signal = async () => {
+ const writer = process.stdin.getWriter();
+ await writer.write(new Uint8Array(1));
+ writer.releaseLock();
+ };
+
+ return {
+ async waitStartup() {
+ await waitSignal();
+ },
+ async enterLock() {
+ await signal();
+ await waitSignal(); // entering signal
+ },
+ async waitEnterLock() {
+ await waitSignal();
+ },
+ async exitLock() {
+ await signal();
+ await waitSignal();
+ },
+ getTimes: async () => {
+ const { stdout } = await process.output();
+ const text = new TextDecoder().decode(stdout);
+ return JSON.parse(text) as {
+ enterTime: number;
+ exitTime: number;
+ };
+ },
+ close: async () => {
+ await process.status;
+ await process.stdin.close();
+ },
+ };
+}
diff --git a/tests/unit/fs_events_test.ts b/tests/unit/fs_events_test.ts
new file mode 100644
index 000000000..4f7cdc4d5
--- /dev/null
+++ b/tests/unit/fs_events_test.ts
@@ -0,0 +1,139 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assert, assertEquals, assertThrows, delay } from "./test_util.ts";
+
+// TODO(ry) Add more tests to specify format.
+
+Deno.test({ permissions: { read: false } }, function watchFsPermissions() {
+ assertThrows(() => {
+ Deno.watchFs(".");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, function watchFsInvalidPath() {
+ if (Deno.build.os === "windows") {
+ assertThrows(
+ () => {
+ Deno.watchFs("non-existent.file");
+ },
+ Error,
+ "Input watch path is neither a file nor a directory",
+ );
+ } else {
+ assertThrows(() => {
+ Deno.watchFs("non-existent.file");
+ }, Deno.errors.NotFound);
+ }
+});
+
+async function getTwoEvents(
+ iter: Deno.FsWatcher,
+): Promise<Deno.FsEvent[]> {
+ const events = [];
+ for await (const event of iter) {
+ events.push(event);
+ if (events.length > 2) break;
+ }
+ return events;
+}
+
+async function makeTempDir(): Promise<string> {
+ const testDir = await Deno.makeTempDir();
+ // The watcher sometimes witnesses the creation of it's own root
+ // directory. Delay a bit.
+ await delay(100);
+ return testDir;
+}
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function watchFsBasic() {
+ const testDir = await makeTempDir();
+ const iter = Deno.watchFs(testDir);
+
+ // Asynchronously capture two fs events.
+ const eventsPromise = getTwoEvents(iter);
+
+ // Make some random file system activity.
+ const file1 = testDir + "/file1.txt";
+ const file2 = testDir + "/file2.txt";
+ Deno.writeFileSync(file1, new Uint8Array([0, 1, 2]));
+ Deno.writeFileSync(file2, new Uint8Array([0, 1, 2]));
+
+ // We should have gotten two fs events.
+ const events = await eventsPromise;
+ assert(events.length >= 2);
+ assert(events[0].kind == "create");
+ assert(events[0].paths[0].includes(testDir));
+ assert(events[1].kind == "create" || events[1].kind == "modify");
+ assert(events[1].paths[0].includes(testDir));
+ },
+);
+
+// TODO(kt3k): This test is for the backward compatibility of `.return` method.
+// This should be removed at 2.0
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function watchFsReturn() {
+ const testDir = await makeTempDir();
+ const iter = Deno.watchFs(testDir);
+
+ // Asynchronously loop events.
+ const eventsPromise = getTwoEvents(iter);
+
+ // Close the watcher.
+ await iter.return!();
+
+ // Expect zero events.
+ const events = await eventsPromise;
+ assertEquals(events, []);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function watchFsClose() {
+ const testDir = await makeTempDir();
+ const iter = Deno.watchFs(testDir);
+
+ // Asynchronously loop events.
+ const eventsPromise = getTwoEvents(iter);
+
+ // Close the watcher.
+ iter.close();
+
+ // Expect zero events.
+ const events = await eventsPromise;
+ assertEquals(events, []);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function watchFsExplicitResourceManagement() {
+ let res;
+ {
+ const testDir = await makeTempDir();
+ using iter = Deno.watchFs(testDir);
+
+ res = iter[Symbol.asyncIterator]().next();
+ }
+
+ const { done } = await res;
+ assert(done);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function watchFsExplicitResourceManagementManualClose() {
+ const testDir = await makeTempDir();
+ using iter = Deno.watchFs(testDir);
+
+ const res = iter[Symbol.asyncIterator]().next();
+
+ iter.close();
+ const { done } = await res;
+ assert(done);
+ },
+);
diff --git a/tests/unit/get_random_values_test.ts b/tests/unit/get_random_values_test.ts
new file mode 100644
index 000000000..75aaf4c1b
--- /dev/null
+++ b/tests/unit/get_random_values_test.ts
@@ -0,0 +1,63 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertNotEquals, assertStrictEquals } from "./test_util.ts";
+
+Deno.test(function getRandomValuesInt8Array() {
+ const arr = new Int8Array(32);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Int8Array(32));
+});
+
+Deno.test(function getRandomValuesUint8Array() {
+ const arr = new Uint8Array(32);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Uint8Array(32));
+});
+
+Deno.test(function getRandomValuesUint8ClampedArray() {
+ const arr = new Uint8ClampedArray(32);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Uint8ClampedArray(32));
+});
+
+Deno.test(function getRandomValuesInt16Array() {
+ const arr = new Int16Array(4);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Int16Array(4));
+});
+
+Deno.test(function getRandomValuesUint16Array() {
+ const arr = new Uint16Array(4);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Uint16Array(4));
+});
+
+Deno.test(function getRandomValuesInt32Array() {
+ const arr = new Int32Array(8);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Int32Array(8));
+});
+
+Deno.test(function getRandomValuesBigInt64Array() {
+ const arr = new BigInt64Array(8);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new BigInt64Array(8));
+});
+
+Deno.test(function getRandomValuesUint32Array() {
+ const arr = new Uint32Array(8);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Uint32Array(8));
+});
+
+Deno.test(function getRandomValuesBigUint64Array() {
+ const arr = new BigUint64Array(8);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new BigUint64Array(8));
+});
+
+Deno.test(function getRandomValuesReturnValue() {
+ const arr = new Uint32Array(8);
+ const rtn = crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Uint32Array(8));
+ assertStrictEquals(rtn, arr);
+});
diff --git a/tests/unit/globals_test.ts b/tests/unit/globals_test.ts
new file mode 100644
index 000000000..00be3f451
--- /dev/null
+++ b/tests/unit/globals_test.ts
@@ -0,0 +1,225 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+// deno-lint-ignore-file no-window-prefix
+import { assert, assertEquals, assertRejects } from "./test_util.ts";
+
+Deno.test(function globalThisExists() {
+ assert(globalThis != null);
+});
+
+Deno.test(function noInternalGlobals() {
+ // globalThis.__bootstrap should not be there.
+ for (const key of Object.keys(globalThis)) {
+ assert(!key.startsWith("_"));
+ }
+});
+
+Deno.test(function windowExists() {
+ assert(window != null);
+});
+
+Deno.test(function selfExists() {
+ assert(self != null);
+});
+
+Deno.test(function windowWindowExists() {
+ assert(window.window === window);
+});
+
+Deno.test(function windowSelfExists() {
+ assert(window.self === window);
+});
+
+Deno.test(function globalThisEqualsWindow() {
+ assert(globalThis === window);
+});
+
+Deno.test(function globalThisEqualsSelf() {
+ assert(globalThis === self);
+});
+
+Deno.test(function globalThisInstanceofWindow() {
+ assert(globalThis instanceof Window);
+});
+
+Deno.test(function globalThisConstructorLength() {
+ assert(globalThis.constructor.length === 0);
+});
+
+Deno.test(function globalThisInstanceofEventTarget() {
+ assert(globalThis instanceof EventTarget);
+});
+
+Deno.test(function navigatorInstanceofNavigator() {
+ // TODO(nayeemrmn): Add `Navigator` to deno_lint globals.
+ // deno-lint-ignore no-undef
+ assert(navigator instanceof Navigator);
+});
+
+Deno.test(function DenoNamespaceExists() {
+ assert(Deno != null);
+});
+
+Deno.test(function DenoNamespaceEqualsWindowDeno() {
+ assert(Deno === window.Deno);
+});
+
+Deno.test(function DenoNamespaceIsNotFrozen() {
+ assert(!Object.isFrozen(Deno));
+});
+
+Deno.test(function webAssemblyExists() {
+ assert(typeof WebAssembly.compile === "function");
+});
+
+// @ts-ignore This is not publicly typed namespace, but it's there for sure.
+const core = Deno[Deno.internal].core;
+
+Deno.test(function DenoNamespaceConfigurable() {
+ const desc = Object.getOwnPropertyDescriptor(globalThis, "Deno");
+ assert(desc);
+ assert(desc.configurable);
+ assert(!desc.writable);
+});
+
+Deno.test(function DenoCoreNamespaceIsImmutable() {
+ const { print } = core;
+ try {
+ core.print = 1;
+ } catch {
+ // pass
+ }
+ assert(print === core.print);
+ try {
+ delete core.print;
+ } catch {
+ // pass
+ }
+ assert(print === core.print);
+});
+
+Deno.test(async function windowQueueMicrotask() {
+ let resolve1: () => void | undefined;
+ let resolve2: () => void | undefined;
+ let microtaskDone = false;
+ const p1 = new Promise<void>((res) => {
+ resolve1 = () => {
+ microtaskDone = true;
+ res();
+ };
+ });
+ const p2 = new Promise<void>((res) => {
+ resolve2 = () => {
+ assert(microtaskDone);
+ res();
+ };
+ });
+ window.queueMicrotask(resolve1!);
+ setTimeout(resolve2!, 0);
+ await p1;
+ await p2;
+});
+
+Deno.test(function webApiGlobalThis() {
+ assert(globalThis.FormData !== null);
+ assert(globalThis.TextEncoder !== null);
+ assert(globalThis.TextEncoderStream !== null);
+ assert(globalThis.TextDecoder !== null);
+ assert(globalThis.TextDecoderStream !== null);
+ assert(globalThis.CountQueuingStrategy !== null);
+ assert(globalThis.ByteLengthQueuingStrategy !== null);
+});
+
+Deno.test(function windowNameIsDefined() {
+ assertEquals(typeof globalThis.name, "string");
+ assertEquals(name, "");
+ assertEquals(window.name, name);
+ name = "foobar";
+ assertEquals(window.name, "foobar");
+ assertEquals(name, "foobar");
+ name = "";
+ assertEquals(window.name, "");
+ assertEquals(name, "");
+});
+
+Deno.test(async function promiseWithResolvers() {
+ {
+ const { promise, resolve } = Promise.withResolvers();
+ resolve(true);
+ assert(await promise);
+ }
+ {
+ const { promise, reject } = Promise.withResolvers();
+ reject(new Error("boom!"));
+ await assertRejects(() => promise, Error, "boom!");
+ }
+});
+
+Deno.test(async function arrayFromAsync() {
+ // Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fromAsync#examples
+ // Thank you.
+ const asyncIterable = (async function* () {
+ for (let i = 0; i < 5; i++) {
+ await new Promise((resolve) => setTimeout(resolve, 10 * i));
+ yield i;
+ }
+ })();
+
+ const a = await Array.fromAsync(asyncIterable);
+ assertEquals(a, [0, 1, 2, 3, 4]);
+
+ const b = await Array.fromAsync(new Map([[1, 2], [3, 4]]));
+ assertEquals(b, [[1, 2], [3, 4]]);
+});
+
+// Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy#examples
+Deno.test(function objectGroupBy() {
+ const inventory = [
+ { name: "asparagus", type: "vegetables", quantity: 5 },
+ { name: "bananas", type: "fruit", quantity: 0 },
+ { name: "goat", type: "meat", quantity: 23 },
+ { name: "cherries", type: "fruit", quantity: 5 },
+ { name: "fish", type: "meat", quantity: 22 },
+ ];
+ const result = Object.groupBy(inventory, ({ type }) => type);
+ assertEquals(result, {
+ vegetables: [
+ { name: "asparagus", type: "vegetables", quantity: 5 },
+ ],
+ fruit: [
+ { name: "bananas", type: "fruit", quantity: 0 },
+ { name: "cherries", type: "fruit", quantity: 5 },
+ ],
+ meat: [
+ { name: "goat", type: "meat", quantity: 23 },
+ { name: "fish", type: "meat", quantity: 22 },
+ ],
+ });
+});
+
+Deno.test(function objectGroupByEmpty() {
+ const empty: string[] = [];
+ const result = Object.groupBy(empty, () => "abc");
+ assertEquals(result.abc, undefined);
+});
+
+// Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/groupBy#examples
+Deno.test(function mapGroupBy() {
+ const inventory = [
+ { name: "asparagus", type: "vegetables", quantity: 9 },
+ { name: "bananas", type: "fruit", quantity: 5 },
+ { name: "goat", type: "meat", quantity: 23 },
+ { name: "cherries", type: "fruit", quantity: 12 },
+ { name: "fish", type: "meat", quantity: 22 },
+ ];
+ const restock = { restock: true };
+ const sufficient = { restock: false };
+ const result = Map.groupBy(
+ inventory,
+ ({ quantity }) => quantity < 6 ? restock : sufficient,
+ );
+ assertEquals(result.get(restock), [{
+ name: "bananas",
+ type: "fruit",
+ quantity: 5,
+ }]);
+});
diff --git a/tests/unit/headers_test.ts b/tests/unit/headers_test.ts
new file mode 100644
index 000000000..ad453b67f
--- /dev/null
+++ b/tests/unit/headers_test.ts
@@ -0,0 +1,416 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals, assertThrows } from "./test_util.ts";
+const {
+ inspectArgs,
+ // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
+} = Deno[Deno.internal];
+
+Deno.test(function headersHasCorrectNameProp() {
+ assertEquals(Headers.name, "Headers");
+});
+
+// Logic heavily copied from web-platform-tests, make
+// sure pass mostly header basic test
+// ref: https://github.com/web-platform-tests/wpt/blob/7c50c216081d6ea3c9afe553ee7b64534020a1b2/fetch/api/headers/headers-basic.html
+Deno.test(function newHeaderTest() {
+ new Headers();
+ new Headers(undefined);
+ new Headers({});
+ try {
+ // deno-lint-ignore no-explicit-any
+ new Headers(null as any);
+ } catch (e) {
+ assert(e instanceof TypeError);
+ }
+});
+
+const headerDict: Record<string, string> = {
+ name1: "value1",
+ name2: "value2",
+ name3: "value3",
+ // deno-lint-ignore no-explicit-any
+ name4: undefined as any,
+ "Content-Type": "value4",
+};
+// deno-lint-ignore no-explicit-any
+const headerSeq: any[] = [];
+for (const [name, value] of Object.entries(headerDict)) {
+ headerSeq.push([name, value]);
+}
+
+Deno.test(function newHeaderWithSequence() {
+ const headers = new Headers(headerSeq);
+ for (const [name, value] of Object.entries(headerDict)) {
+ assertEquals(headers.get(name), String(value));
+ }
+ assertEquals(headers.get("length"), null);
+});
+
+Deno.test(function newHeaderWithRecord() {
+ const headers = new Headers(headerDict);
+ for (const [name, value] of Object.entries(headerDict)) {
+ assertEquals(headers.get(name), String(value));
+ }
+});
+
+Deno.test(function newHeaderWithHeadersInstance() {
+ const headers = new Headers(headerDict);
+ const headers2 = new Headers(headers);
+ for (const [name, value] of Object.entries(headerDict)) {
+ assertEquals(headers2.get(name), String(value));
+ }
+});
+
+Deno.test(function headerAppendSuccess() {
+ const headers = new Headers();
+ for (const [name, value] of Object.entries(headerDict)) {
+ headers.append(name, value);
+ assertEquals(headers.get(name), String(value));
+ }
+});
+
+Deno.test(function headerSetSuccess() {
+ const headers = new Headers();
+ for (const [name, value] of Object.entries(headerDict)) {
+ headers.set(name, value);
+ assertEquals(headers.get(name), String(value));
+ }
+});
+
+Deno.test(function headerHasSuccess() {
+ const headers = new Headers(headerDict);
+ for (const name of Object.keys(headerDict)) {
+ assert(headers.has(name), "headers has name " + name);
+ assert(
+ !headers.has("nameNotInHeaders"),
+ "headers do not have header: nameNotInHeaders",
+ );
+ }
+});
+
+Deno.test(function headerDeleteSuccess() {
+ const headers = new Headers(headerDict);
+ for (const name of Object.keys(headerDict)) {
+ assert(headers.has(name), "headers have a header: " + name);
+ headers.delete(name);
+ assert(!headers.has(name), "headers do not have anymore a header: " + name);
+ }
+});
+
+Deno.test(function headerGetSuccess() {
+ const headers = new Headers(headerDict);
+ for (const [name, value] of Object.entries(headerDict)) {
+ assertEquals(headers.get(name), String(value));
+ assertEquals(headers.get("nameNotInHeaders"), null);
+ }
+});
+
+Deno.test(function headerEntriesSuccess() {
+ const headers = new Headers(headerDict);
+ const iterators = headers.entries();
+ for (const it of iterators) {
+ const key = it[0];
+ const value = it[1];
+ assert(headers.has(key));
+ assertEquals(value, headers.get(key));
+ }
+});
+
+Deno.test(function headerKeysSuccess() {
+ const headers = new Headers(headerDict);
+ const iterators = headers.keys();
+ for (const it of iterators) {
+ assert(headers.has(it));
+ }
+});
+
+Deno.test(function headerValuesSuccess() {
+ const headers = new Headers(headerDict);
+ const iterators = headers.values();
+ const entries = headers.entries();
+ const values = [];
+ for (const pair of entries) {
+ values.push(pair[1]);
+ }
+ for (const it of iterators) {
+ assert(values.includes(it));
+ }
+});
+
+const headerEntriesDict: Record<string, string> = {
+ name1: "value1",
+ Name2: "value2",
+ name: "value3",
+ "content-Type": "value4",
+ "Content-Typ": "value5",
+ "Content-Types": "value6",
+};
+
+Deno.test(function headerForEachSuccess() {
+ const headers = new Headers(headerEntriesDict);
+ const keys = Object.keys(headerEntriesDict);
+ keys.forEach((key) => {
+ const value = headerEntriesDict[key];
+ const newkey = key.toLowerCase();
+ headerEntriesDict[newkey] = value;
+ });
+ let callNum = 0;
+ headers.forEach((value, key, container) => {
+ assertEquals(headers, container);
+ assertEquals(value, headerEntriesDict[key]);
+ callNum++;
+ });
+ assertEquals(callNum, keys.length);
+});
+
+Deno.test(function headerSymbolIteratorSuccess() {
+ assert(Symbol.iterator in Headers.prototype);
+ const headers = new Headers(headerEntriesDict);
+ for (const header of headers) {
+ const key = header[0];
+ const value = header[1];
+ assert(headers.has(key));
+ assertEquals(value, headers.get(key));
+ }
+});
+
+Deno.test(function headerTypesAvailable() {
+ function newHeaders(): Headers {
+ return new Headers();
+ }
+ const headers = newHeaders();
+ assert(headers instanceof Headers);
+});
+
+// Modified from https://github.com/bitinn/node-fetch/blob/7d3293200a91ad52b5ca7962f9d6fd1c04983edb/test/test.js#L2001-L2014
+// Copyright (c) 2016 David Frank. MIT License.
+Deno.test(function headerIllegalReject() {
+ let errorCount = 0;
+ try {
+ new Headers({ "He y": "ok" });
+ } catch (_e) {
+ errorCount++;
+ }
+ try {
+ new Headers({ "Hé-y": "ok" });
+ } catch (_e) {
+ errorCount++;
+ }
+ try {
+ new Headers({ "He-y": "ăk" });
+ } catch (_e) {
+ errorCount++;
+ }
+ const headers = new Headers();
+ try {
+ headers.append("Hé-y", "ok");
+ } catch (_e) {
+ errorCount++;
+ }
+ try {
+ headers.delete("Hé-y");
+ } catch (_e) {
+ errorCount++;
+ }
+ try {
+ headers.get("Hé-y");
+ } catch (_e) {
+ errorCount++;
+ }
+ try {
+ headers.has("Hé-y");
+ } catch (_e) {
+ errorCount++;
+ }
+ try {
+ headers.set("Hé-y", "ok");
+ } catch (_e) {
+ errorCount++;
+ }
+ try {
+ headers.set("", "ok");
+ } catch (_e) {
+ errorCount++;
+ }
+ assertEquals(errorCount, 9);
+ // 'o k' is valid value but invalid name
+ new Headers({ "He-y": "o k" });
+});
+
+// If pair does not contain exactly two items,then throw a TypeError.
+Deno.test(function headerParamsShouldThrowTypeError() {
+ let hasThrown = 0;
+
+ try {
+ new Headers(([["1"]] as unknown) as Array<[string, string]>);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+
+ assertEquals(hasThrown, 2);
+});
+
+Deno.test(function headerParamsArgumentsCheck() {
+ const methodRequireOneParam = ["delete", "get", "has", "forEach"] as const;
+
+ const methodRequireTwoParams = ["append", "set"] as const;
+
+ methodRequireOneParam.forEach((method) => {
+ const headers = new Headers();
+ let hasThrown = 0;
+ try {
+ // deno-lint-ignore no-explicit-any
+ (headers as any)[method]();
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ });
+
+ methodRequireTwoParams.forEach((method) => {
+ const headers = new Headers();
+ let hasThrown = 0;
+
+ try {
+ // deno-lint-ignore no-explicit-any
+ (headers as any)[method]();
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+
+ hasThrown = 0;
+ try {
+ // deno-lint-ignore no-explicit-any
+ (headers as any)[method]("foo");
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ });
+});
+
+Deno.test(function headersInitMultiple() {
+ const headers = new Headers([
+ ["Set-Cookie", "foo=bar"],
+ ["Set-Cookie", "bar=baz"],
+ ["X-Deno", "foo"],
+ ["X-Deno", "bar"],
+ ]);
+ const actual = [...headers];
+ assertEquals(actual, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "bar=baz"],
+ ["x-deno", "foo, bar"],
+ ]);
+});
+
+Deno.test(function headerInitWithPrototypePollution() {
+ const originalExec = RegExp.prototype.exec;
+ try {
+ RegExp.prototype.exec = () => {
+ throw Error();
+ };
+ new Headers([
+ ["X-Deno", "foo"],
+ ["X-Deno", "bar"],
+ ]);
+ } finally {
+ RegExp.prototype.exec = originalExec;
+ }
+});
+
+Deno.test(function headersAppendMultiple() {
+ const headers = new Headers([
+ ["Set-Cookie", "foo=bar"],
+ ["X-Deno", "foo"],
+ ]);
+ headers.append("set-Cookie", "bar=baz");
+ headers.append("x-Deno", "bar");
+ const actual = [...headers];
+ assertEquals(actual, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "bar=baz"],
+ ["x-deno", "foo, bar"],
+ ]);
+});
+
+Deno.test(function headersAppendDuplicateSetCookieKey() {
+ const headers = new Headers([["Set-Cookie", "foo=bar"]]);
+ headers.append("set-Cookie", "foo=baz");
+ headers.append("Set-cookie", "baz=bar");
+ const actual = [...headers];
+ assertEquals(actual, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "foo=baz"],
+ ["set-cookie", "baz=bar"],
+ ]);
+});
+
+Deno.test(function headersGetSetCookie() {
+ const headers = new Headers([
+ ["Set-Cookie", "foo=bar"],
+ ["set-Cookie", "bar=qat"],
+ ]);
+ assertEquals(headers.get("SET-COOKIE"), "foo=bar, bar=qat");
+});
+
+Deno.test(function toStringShouldBeWebCompatibility() {
+ const headers = new Headers();
+ assertEquals(headers.toString(), "[object Headers]");
+});
+
+function stringify(...args: unknown[]): string {
+ return inspectArgs(args).replace(/\n$/, "");
+}
+
+Deno.test(function customInspectReturnsCorrectHeadersFormat() {
+ const blankHeaders = new Headers();
+ assertEquals(stringify(blankHeaders), "Headers {}");
+ const singleHeader = new Headers([["Content-Type", "application/json"]]);
+ assertEquals(
+ stringify(singleHeader),
+ `Headers { "content-type": "application/json" }`,
+ );
+ const multiParamHeader = new Headers([
+ ["Content-Type", "application/json"],
+ ["Content-Length", "1337"],
+ ]);
+ assertEquals(
+ stringify(multiParamHeader),
+ `Headers { "content-length": "1337", "content-type": "application/json" }`,
+ );
+});
+
+Deno.test(function invalidHeadersFlaky() {
+ assertThrows(
+ () => new Headers([["x", "\u0000x"]]),
+ TypeError,
+ "Header value is not valid.",
+ );
+ assertThrows(
+ () => new Headers([["x", "\u0000x"]]),
+ TypeError,
+ "Header value is not valid.",
+ );
+});
diff --git a/tests/unit/http_test.ts b/tests/unit/http_test.ts
new file mode 100644
index 000000000..17023004e
--- /dev/null
+++ b/tests/unit/http_test.ts
@@ -0,0 +1,2801 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { Buffer, BufReader, BufWriter } from "@test_util/std/io/mod.ts";
+import { TextProtoReader } from "../testdata/run/textproto.ts";
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertStrictEquals,
+ assertThrows,
+ delay,
+ fail,
+} from "./test_util.ts";
+import { join } from "@test_util/std/path/mod.ts";
+
+const listenPort = 4507;
+const listenPort2 = 4508;
+
+const {
+ buildCaseInsensitiveCommaValueFinder,
+ // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
+} = Deno[Deno.internal];
+
+async function writeRequestAndReadResponse(conn: Deno.Conn): Promise<string> {
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ const w = new BufWriter(conn);
+ const r = new BufReader(conn);
+ const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`;
+ const writeResult = await w.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await w.flush();
+ const tpr = new TextProtoReader(r);
+ const statusLine = await tpr.readLine();
+ assert(statusLine !== null);
+ const headers = await tpr.readMimeHeader();
+ assert(headers !== null);
+
+ const chunkedReader = chunkedBodyReader(headers, r);
+ const buf = new Uint8Array(5);
+ const dest = new Buffer();
+ let result: number | null;
+ while ((result = await chunkedReader.read(buf)) !== null) {
+ const len = Math.min(buf.byteLength, result);
+ await dest.write(buf.subarray(0, len));
+ }
+ return decoder.decode(dest.bytes());
+}
+
+Deno.test({ permissions: { net: true } }, async function httpServerBasic() {
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(new URL(request.url).href, `http://127.0.0.1:${listenPort}/`);
+ assertEquals(await request.text(), "");
+ await respondWith(
+ new Response("Hello World", { headers: { "foo": "bar" } }),
+ );
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ headers: { "connection": "close" },
+ });
+ const clone = resp.clone();
+ const text = await resp.text();
+ assertEquals(text, "Hello World");
+ assertEquals(resp.headers.get("foo"), "bar");
+ const cloneText = await clone.text();
+ assertEquals(cloneText, "Hello World");
+ await promise;
+
+ httpConn!.close();
+});
+
+// https://github.com/denoland/deno/issues/15107
+Deno.test(
+ { permissions: { net: true } },
+ async function httpLazyHeadersIssue15107() {
+ let headers: Headers;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: 2333 });
+ const conn = await listener.accept();
+ listener.close();
+ const httpConn = Deno.serveHttp(conn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request } = e;
+ request.text();
+ headers = request.headers;
+ httpConn!.close();
+ })();
+
+ const conn = await Deno.connect({ port: 2333 });
+ // Send GET request with a body + content-length.
+ const encoder = new TextEncoder();
+ const body =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\n\r\n12345`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await promise;
+ conn.close();
+ assertEquals(headers!.get("content-length"), "5");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpReadHeadersAfterClose() {
+ const promise = (async () => {
+ const listener = Deno.listen({ port: 2334 });
+ const conn = await listener.accept();
+ listener.close();
+ const httpConn = Deno.serveHttp(conn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+
+ await request.text(); // Read body
+ await respondWith(new Response("Hello World")); // Closes request
+
+ assertThrows(() => request.headers, TypeError, "request closed");
+ httpConn!.close();
+ })();
+
+ const conn = await Deno.connect({ port: 2334 });
+ // Send GET request with a body + content-length.
+ const encoder = new TextEncoder();
+ const body =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\n\r\n12345`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await promise;
+ conn.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerGetRequestBody() {
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.body, null);
+ await respondWith(new Response("", { headers: {} }));
+ })();
+
+ const conn = await Deno.connect({ port: listenPort });
+ // Send GET request with a body + content-length.
+ const encoder = new TextEncoder();
+ const body =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\nContent-Length: 5\r\n\r\n12345`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ const resp = new Uint8Array(200);
+ const readResult = await conn.read(resp);
+ assertEquals(readResult, 138);
+
+ conn.close();
+
+ await promise;
+ httpConn!.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerStreamResponse() {
+ const stream = new TransformStream();
+ const writer = stream.writable.getWriter();
+ writer.write(new TextEncoder().encode("hello "));
+ writer.write(new TextEncoder().encode("world"));
+ writer.close();
+
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const evt = await httpConn.nextRequest();
+ assert(evt);
+ const { request, respondWith } = evt;
+ assert(!request.body);
+ await respondWith(new Response(stream.readable));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ const respBody = await resp.text();
+ assertEquals("hello world", respBody);
+ await promise;
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerStreamRequest() {
+ const stream = new TransformStream();
+ const writer = stream.writable.getWriter();
+ writer.write(new TextEncoder().encode("hello "));
+ writer.write(new TextEncoder().encode("world"));
+ writer.close();
+
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const evt = await httpConn.nextRequest();
+ assert(evt);
+ const { request, respondWith } = evt;
+ const reqBody = await request.text();
+ assertEquals("hello world", reqBody);
+ await respondWith(new Response(""));
+
+ // TODO(ry) If we don't call httpConn.nextRequest() here we get "error sending
+ // request for url (https://localhost:${listenPort}/): connection closed before
+ // message completed".
+ assertEquals(await httpConn.nextRequest(), null);
+
+ listener.close();
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ body: stream.readable,
+ method: "POST",
+ headers: { "connection": "close" },
+ });
+
+ await resp.arrayBuffer();
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerStreamDuplex() {
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const evt = await httpConn.nextRequest();
+ assert(evt);
+ const { request, respondWith } = evt;
+ assert(request.body);
+ await respondWith(new Response(request.body));
+ })();
+
+ const ts = new TransformStream();
+ const writable = ts.writable.getWriter();
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ method: "POST",
+ body: ts.readable,
+ });
+ assert(resp.body);
+ const reader = resp.body.getReader();
+ await writable.write(new Uint8Array([1]));
+ const chunk1 = await reader.read();
+ assert(!chunk1.done);
+ assertEquals(chunk1.value, new Uint8Array([1]));
+ await writable.write(new Uint8Array([2]));
+ const chunk2 = await reader.read();
+ assert(!chunk2.done);
+ assertEquals(chunk2.value, new Uint8Array([2]));
+
+ await writable.close();
+ const chunk3 = await reader.read();
+ assert(chunk3.done);
+ await promise;
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function httpServerClose() {
+ const listener = Deno.listen({ port: listenPort });
+ const client = await Deno.connect({ port: listenPort });
+ const httpConn = Deno.serveHttp(await listener.accept());
+ client.close();
+ const evt = await httpConn.nextRequest();
+ assertEquals(evt, null);
+ // Note httpConn is automatically closed when "done" is reached.
+ listener.close();
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerInvalidMethod() {
+ const listener = Deno.listen({ port: listenPort });
+ const client = await Deno.connect({ port: listenPort });
+ const httpConn = Deno.serveHttp(await listener.accept());
+ await client.write(new Uint8Array([1, 2, 3]));
+ await assertRejects(
+ async () => {
+ await httpConn.nextRequest();
+ },
+ Deno.errors.Http,
+ "invalid HTTP method parsed",
+ );
+ // Note httpConn is automatically closed when it errors.
+ client.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function httpServerWithTls() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const promise = (async () => {
+ const listener = Deno.listenTls({
+ hostname,
+ port,
+ cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
+ key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
+ });
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const evt = await httpConn.nextRequest();
+ assert(evt);
+ const { respondWith } = evt;
+ await respondWith(new Response("Hello World"));
+
+ // TODO(ry) If we don't call httpConn.nextRequest() here we get "error sending
+ // request for url (https://localhost:${listenPort}/): connection closed before
+ // message completed".
+ assertEquals(await httpConn.nextRequest(), null);
+
+ listener.close();
+ })();
+
+ const caCert = Deno.readTextFileSync("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const resp = await fetch(`https://${hostname}:${port}/`, {
+ headers: { "connection": "close" },
+ client,
+ });
+ client.close();
+ const respBody = await resp.text();
+ assertEquals("Hello World", respBody);
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerRegressionHang() {
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const event = await httpConn.nextRequest();
+ assert(event);
+ const { request, respondWith } = event;
+ const reqBody = await request.text();
+ assertEquals("request", reqBody);
+ await respondWith(new Response("response"));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ method: "POST",
+ body: "request",
+ });
+ const respBody = await resp.text();
+ assertEquals("response", respBody);
+ await promise;
+
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerCancelBodyOnResponseFailure() {
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const event = await httpConn.nextRequest();
+ assert(event);
+ const { respondWith } = event;
+ let cancelReason: string;
+ await assertRejects(
+ async () => {
+ let interval = 0;
+ await respondWith(
+ new Response(
+ new ReadableStream({
+ start(controller) {
+ interval = setInterval(() => {
+ const message = `data: ${Date.now()}\n\n`;
+ controller.enqueue(new TextEncoder().encode(message));
+ }, 200);
+ },
+ cancel(reason) {
+ cancelReason = reason;
+ clearInterval(interval);
+ },
+ }),
+ ),
+ );
+ },
+ Deno.errors.Http,
+ cancelReason!,
+ );
+ assert(cancelReason!);
+ httpConn!.close();
+ listener.close();
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ await resp.body!.cancel();
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerNextRequestErrorExposedInResponse() {
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const event = await httpConn.nextRequest();
+ assert(event);
+ // Start polling for the next request before awaiting response.
+ const nextRequestPromise = httpConn.nextRequest();
+ const { respondWith } = event;
+ await assertRejects(
+ async () => {
+ let interval = 0;
+ await respondWith(
+ new Response(
+ new ReadableStream({
+ start(controller) {
+ interval = setInterval(() => {
+ const message = `data: ${Date.now()}\n\n`;
+ controller.enqueue(new TextEncoder().encode(message));
+ }, 200);
+ },
+ cancel() {
+ clearInterval(interval);
+ },
+ }),
+ ),
+ );
+ },
+ Deno.errors.Http,
+ "connection closed",
+ );
+ // The error from `op_http_accept` reroutes to `respondWith()`.
+ assertEquals(await nextRequestPromise, null);
+ listener.close();
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ await resp.body!.cancel();
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerEmptyBlobResponse() {
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const event = await httpConn.nextRequest();
+ assert(event);
+ const { respondWith } = event;
+ await respondWith(new Response(new Blob([])));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ const respBody = await resp.text();
+ assertEquals("", respBody);
+ await promise;
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerNextRequestResolvesOnClose() {
+ const httpConnList: Deno.HttpConn[] = [];
+
+ async function serve(l: Deno.Listener) {
+ for await (const conn of l) {
+ (async () => {
+ const c = Deno.serveHttp(conn);
+ httpConnList.push(c);
+ for await (const { respondWith } of c) {
+ respondWith(new Response("hello"));
+ }
+ })();
+ }
+ }
+
+ const l = Deno.listen({ port: listenPort });
+ serve(l);
+
+ await delay(300);
+ const res = await fetch(`http://localhost:${listenPort}/`);
+ const _text = await res.text();
+
+ // Close connection and listener.
+ httpConnList.forEach((conn) => conn.close());
+ l.close();
+
+ await delay(300);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ // Issue: https://github.com/denoland/deno/issues/10870
+ async function httpServerHang() {
+ // Quick and dirty way to make a readable stream from a string. Alternatively,
+ // `readableStreamFromReader(file)` could be used.
+ function stream(s: string): ReadableStream<Uint8Array> {
+ return new Response(s).body!;
+ }
+
+ const httpConns: Deno.HttpConn[] = [];
+ const promise = (async () => {
+ let count = 0;
+ const listener = Deno.listen({ port: listenPort });
+ for await (const conn of listener) {
+ (async () => {
+ const httpConn = Deno.serveHttp(conn);
+ httpConns.push(httpConn);
+ for await (const { respondWith } of httpConn) {
+ respondWith(new Response(stream("hello")));
+
+ count++;
+ if (count >= 2) {
+ listener.close();
+ }
+ }
+ })();
+ }
+ })();
+
+ const clientConn = await Deno.connect({ port: listenPort });
+
+ const r1 = await writeRequestAndReadResponse(clientConn);
+ assertEquals(r1, "hello");
+
+ const r2 = await writeRequestAndReadResponse(clientConn);
+ assertEquals(r2, "hello");
+
+ clientConn.close();
+ await promise;
+ for (const conn of httpConns) {
+ conn.close();
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ // Issue: https://github.com/denoland/deno/issues/10930
+ async function httpServerStreamingResponse() {
+ // This test enqueues a single chunk for readable
+ // stream and waits for client to read that chunk and signal
+ // it before enqueueing subsequent chunk. Issue linked above
+ // presented a situation where enqueued chunks were not
+ // written to the HTTP connection until the next chunk was enqueued.
+
+ let counter = 0;
+
+ const deferreds = [
+ Promise.withResolvers<void>(),
+ Promise.withResolvers<void>(),
+ Promise.withResolvers<void>(),
+ ];
+
+ async function writeRequest(conn: Deno.Conn) {
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ const w = new BufWriter(conn);
+ const r = new BufReader(conn);
+ const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`;
+ const writeResult = await w.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await w.flush();
+ const tpr = new TextProtoReader(r);
+ const statusLine = await tpr.readLine();
+ assert(statusLine !== null);
+ const headers = await tpr.readMimeHeader();
+ assert(headers !== null);
+
+ const chunkedReader = chunkedBodyReader(headers, r);
+ const buf = new Uint8Array(5);
+ const dest = new Buffer();
+ let result: number | null;
+ while ((result = await chunkedReader.read(buf)) !== null) {
+ const len = Math.min(buf.byteLength, result);
+ await dest.write(buf.subarray(0, len));
+ // Resolve a deferred - this will make response stream to
+ // enqueue next chunk.
+ deferreds[counter - 1].resolve();
+ }
+ return decoder.decode(dest.bytes());
+ }
+
+ function periodicStream() {
+ return new ReadableStream({
+ start(controller) {
+ controller.enqueue(`${counter}\n`);
+ counter++;
+ },
+
+ async pull(controller) {
+ if (counter >= 3) {
+ return controller.close();
+ }
+
+ await deferreds[counter - 1].promise;
+
+ controller.enqueue(`${counter}\n`);
+ counter++;
+ },
+ }).pipeThrough(new TextEncoderStream());
+ }
+
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const finished = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const requestEvent = await httpConn.nextRequest();
+ const { respondWith } = requestEvent!;
+ await respondWith(new Response(periodicStream()));
+ })();
+
+ // start a client
+ const clientConn = await Deno.connect({ port: listenPort });
+
+ const r1 = await writeRequest(clientConn);
+ assertEquals(r1, "0\n1\n2\n");
+
+ await finished;
+ clientConn.close();
+
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpRequestLatin1Headers() {
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(request.headers.get("X-Header-Test"), "á");
+ await respondWith(
+ new Response("", { headers: { "X-Header-Test": "Æ" } }),
+ );
+ })();
+
+ const clientConn = await Deno.connect({ port: listenPort });
+ const requestText =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\nX-Header-Test: á\r\n\r\n`;
+ const requestBytes = new Uint8Array(requestText.length);
+ for (let i = 0; i < requestText.length; i++) {
+ requestBytes[i] = requestText.charCodeAt(i);
+ }
+ let written = 0;
+ while (written < requestBytes.byteLength) {
+ written += await clientConn.write(requestBytes.slice(written));
+ }
+
+ let responseText = "";
+ const buf = new Uint8Array(1024);
+ let read;
+
+ while ((read = await clientConn.read(buf)) !== null) {
+ httpConn!.close();
+ for (let i = 0; i < read; i++) {
+ responseText += String.fromCharCode(buf[i]);
+ }
+ }
+
+ clientConn.close();
+
+ assert(/\r\n[Xx]-[Hh]eader-[Tt]est: Æ\r\n/.test(responseText));
+
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerRequestWithoutPath() {
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(
+ new URL(request.url).href,
+ `http://127.0.0.1:${listenPort}/`,
+ );
+ assertEquals(await request.text(), "");
+ await respondWith(new Response());
+ })();
+
+ const clientConn = await Deno.connect({ port: listenPort });
+
+ async function writeRequest(conn: Deno.Conn) {
+ const encoder = new TextEncoder();
+
+ const w = new BufWriter(conn);
+ const r = new BufReader(conn);
+ const body =
+ `CONNECT 127.0.0.1:${listenPort} HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`;
+ const writeResult = await w.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await w.flush();
+ const tpr = new TextProtoReader(r);
+ const statusLine = await tpr.readLine();
+ assert(statusLine !== null);
+ const m = statusLine.match(/^(.+?) (.+?) (.+?)$/);
+ assert(m !== null, "must be matched");
+ const [_, _proto, status, _ok] = m;
+ assertEquals(status, "200");
+ const headers = await tpr.readMimeHeader();
+ assert(headers !== null);
+ }
+
+ await writeRequest(clientConn);
+ clientConn.close();
+ await promise;
+ httpConn!.close();
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function httpServerWebSocket() {
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ const httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ const {
+ response,
+ socket,
+ } = Deno.upgradeWebSocket(request);
+ socket.onerror = () => fail();
+ socket.onmessage = (m) => {
+ socket.send(m.data);
+ socket.close(1001);
+ };
+ const close = new Promise<void>((resolve) => {
+ socket.onclose = () => resolve();
+ });
+ await respondWith(response);
+ await close;
+ })();
+
+ const def = Promise.withResolvers<void>();
+ const ws = new WebSocket(`ws://localhost:${listenPort}`);
+ ws.onmessage = (m) => assertEquals(m.data, "foo");
+ ws.onerror = () => fail();
+ ws.onclose = () => def.resolve();
+ ws.onopen = () => ws.send("foo");
+ await def.promise;
+ await promise;
+});
+
+Deno.test(function httpUpgradeWebSocket() {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "Upgrade",
+ upgrade: "websocket",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ const { response } = Deno.upgradeWebSocket(request);
+ assertEquals(response.status, 101);
+ assertEquals(response.headers.get("connection"), "Upgrade");
+ assertEquals(response.headers.get("upgrade"), "websocket");
+ assertEquals(
+ response.headers.get("sec-websocket-accept"),
+ "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",
+ );
+});
+
+Deno.test(function httpUpgradeWebSocketMultipleConnectionOptions() {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "keep-alive, upgrade",
+ upgrade: "websocket",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ const { response } = Deno.upgradeWebSocket(request);
+ assertEquals(response.status, 101);
+});
+
+Deno.test(function httpUpgradeWebSocketMultipleUpgradeOptions() {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "upgrade",
+ upgrade: "websocket, foo",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ const { response } = Deno.upgradeWebSocket(request);
+ assertEquals(response.status, 101);
+});
+
+Deno.test(function httpUpgradeWebSocketCaseInsensitiveUpgradeHeader() {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "upgrade",
+ upgrade: "Websocket",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ const { response } = Deno.upgradeWebSocket(request);
+ assertEquals(response.status, 101);
+});
+
+Deno.test(function httpUpgradeWebSocketInvalidUpgradeHeader() {
+ assertThrows(
+ () => {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "upgrade",
+ upgrade: "invalid",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ Deno.upgradeWebSocket(request);
+ },
+ TypeError,
+ "Invalid Header: 'upgrade' header must contain 'websocket'",
+ );
+});
+
+Deno.test(function httpUpgradeWebSocketWithoutUpgradeHeader() {
+ assertThrows(
+ () => {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "upgrade",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ Deno.upgradeWebSocket(request);
+ },
+ TypeError,
+ "Invalid Header: 'upgrade' header must contain 'websocket'",
+ );
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpCookieConcatenation() {
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(
+ new URL(request.url).href,
+ `http://127.0.0.1:${listenPort}/`,
+ );
+ assertEquals(await request.text(), "");
+ assertEquals(request.headers.get("cookie"), "foo=bar; bar=foo");
+ await respondWith(new Response("ok"));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ headers: [
+ ["connection", "close"],
+ ["cookie", "foo=bar"],
+ ["cookie", "bar=foo"],
+ ],
+ });
+ const text = await resp.text();
+ assertEquals(text, "ok");
+ await promise;
+ httpConn!.close();
+ },
+);
+
+// https://github.com/denoland/deno/issues/11651
+Deno.test({ permissions: { net: true } }, async function httpServerPanic() {
+ const listener = Deno.listen({ port: listenPort });
+ const client = await Deno.connect({ port: listenPort });
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+
+ // This message is incomplete on purpose, we'll forcefully close client connection
+ // after it's flushed to cause connection to error out on the server side.
+ const encoder = new TextEncoder();
+ await client.write(encoder.encode("GET / HTTP/1.1"));
+
+ httpConn.nextRequest();
+ await client.write(encoder.encode("\r\n\r\n"));
+ httpConn!.close();
+
+ client.close();
+ listener.close();
+});
+
+Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ async function httpServerCorrectSizeResponse() {
+ const tmpFile = await Deno.makeTempFile();
+ using file = await Deno.open(tmpFile, { write: true, read: true });
+ await file.write(new Uint8Array(70 * 1024).fill(1)); // 70kb sent in 64kb + 6kb chunks
+
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const ev = await httpConn.nextRequest();
+ const { respondWith } = ev!;
+ const f = await Deno.open(tmpFile, { read: true });
+ await respondWith(new Response(f.readable, { status: 200 }));
+ })();
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ const body = await resp.arrayBuffer();
+ assertEquals(body.byteLength, 70 * 1024);
+ await promise;
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ async function httpServerClosedStream() {
+ const listener = Deno.listen({ port: listenPort });
+
+ const client = await Deno.connect({ port: listenPort });
+ await client.write(new TextEncoder().encode(
+ `GET / HTTP/1.0\r\n\r\n`,
+ ));
+
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const ev = await httpConn.nextRequest();
+ const { respondWith } = ev!;
+
+ const tmpFile = await Deno.makeTempFile();
+ const file = await Deno.open(tmpFile, { write: true, read: true });
+ await file.write(new TextEncoder().encode("hello"));
+
+ const reader = await file.readable.getReader();
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ }
+
+ let didThrow = false;
+ try {
+ await respondWith(new Response(file.readable));
+ } catch {
+ // pass
+ didThrow = true;
+ }
+
+ assert(didThrow);
+ httpConn!.close();
+ listener.close();
+ client.close();
+ },
+);
+
+// https://github.com/denoland/deno/issues/11595
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerIncompleteMessage() {
+ const listener = Deno.listen({ port: listenPort });
+
+ const client = await Deno.connect({ port: listenPort });
+ await client.write(new TextEncoder().encode(
+ `GET / HTTP/1.0\r\n\r\n`,
+ ));
+
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const ev = await httpConn.nextRequest();
+ const { respondWith } = ev!;
+
+ const errors: Error[] = [];
+
+ const readable = new ReadableStream({
+ async pull(controller) {
+ client.close();
+ await delay(1000);
+ controller.enqueue(new TextEncoder().encode(
+ "written to the writable side of a TransformStream",
+ ));
+ controller.close();
+ },
+ cancel(error) {
+ errors.push(error);
+ },
+ });
+
+ const res = new Response(readable);
+
+ await respondWith(res).catch((error: Error) => errors.push(error));
+
+ httpConn!.close();
+ listener.close();
+
+ assert(errors.length >= 1);
+ for (const error of errors) {
+ assertEquals(error.name, "Http");
+ assert(error.message.includes("connection"));
+ }
+ },
+);
+
+// https://github.com/denoland/deno/issues/11743
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerDoesntLeakResources() {
+ const listener = Deno.listen({ port: listenPort });
+ const [conn, clientConn] = await Promise.all([
+ listener.accept(),
+ Deno.connect({ port: listenPort }),
+ ]);
+ const httpConn = Deno.serveHttp(conn);
+
+ await Promise.all([
+ httpConn.nextRequest(),
+ clientConn.write(new TextEncoder().encode(
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`,
+ )),
+ ]);
+
+ httpConn!.close();
+ listener.close();
+ clientConn.close();
+ },
+);
+
+// https://github.com/denoland/deno/issues/11926
+// verify that the only new resource is "httpConnection", to make
+// sure "request" resource is closed even if its body was not read
+// by server handler
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerDoesntLeakResources2() {
+ let listener: Deno.Listener;
+ let httpConn: Deno.HttpConn;
+
+ const promise = (async () => {
+ listener = Deno.listen({ port: listenPort });
+ for await (const conn of listener) {
+ httpConn = Deno.serveHttp(conn);
+ for await (const { request, respondWith } of httpConn) {
+ assertEquals(
+ new URL(request.url).href,
+ `http://127.0.0.1:${listenPort}/`,
+ );
+ // not reading request body on purpose
+ respondWith(new Response("ok"));
+ }
+ }
+ })();
+
+ const response = await fetch(`http://127.0.0.1:${listenPort}`, {
+ method: "POST",
+ body: "hello world",
+ });
+ await response.text();
+
+ listener!.close();
+ httpConn!.close();
+ await promise;
+ },
+);
+
+// https://github.com/denoland/deno/pull/12216
+Deno.test(
+ { permissions: { net: true } },
+ async function droppedConnSenderNoPanic() {
+ async function server() {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ const http = Deno.serveHttp(conn);
+ const evt = await http.nextRequest();
+ http.close();
+ try {
+ await evt!.respondWith(new Response("boom"));
+ } catch {
+ // Ignore error.
+ }
+ listener.close();
+ }
+
+ async function client() {
+ try {
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ await resp.body?.cancel();
+ } catch {
+ // Ignore error
+ }
+ }
+
+ await Promise.all([server(), client()]);
+ },
+);
+
+// https://github.com/denoland/deno/issues/12193
+Deno.test(
+ { permissions: { net: true } },
+ async function httpConnConcurrentNextRequestCalls() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ hostname, port });
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const promises = new Array(10).fill(null).map(async (_, i) => {
+ const event = await httpConn.nextRequest();
+ assert(event);
+ const { pathname } = new URL(event.request.url);
+ assertStrictEquals(pathname, `/${i}`);
+ const response = new Response(`Response #${i}`);
+ await event.respondWith(response);
+ });
+ await Promise.all(promises);
+ }
+
+ async function client() {
+ for (let i = 0; i < 10; i++) {
+ const response = await fetch(`http://${hostname}:${port}/${i}`);
+ const body = await response.text();
+ assertStrictEquals(body, `Response #${i}`);
+ }
+ }
+
+ await Promise.all([server(), delay(100).then(client)]);
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+// https://github.com/denoland/deno/pull/12704
+// https://github.com/denoland/deno/pull/12732
+Deno.test(
+ { permissions: { net: true } },
+ async function httpConnAutoCloseDelayedOnUpgrade() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ const httpConn = Deno.serveHttp(tcpConn);
+
+ const event1 = await httpConn.nextRequest() as Deno.RequestEvent;
+ const event2Promise = httpConn.nextRequest();
+
+ const { socket, response } = Deno.upgradeWebSocket(event1.request);
+ socket.onmessage = (event) => socket.send(event.data);
+ const socketClosed = new Promise<void>((resolve) => {
+ socket.onclose = () => resolve();
+ });
+ event1.respondWith(response);
+
+ const event2 = await event2Promise;
+ assertStrictEquals(event2, null);
+
+ listener.close();
+ await socketClosed;
+ }
+
+ async function client() {
+ const socket = new WebSocket(`ws://${hostname}:${port}/`);
+ socket.onopen = () => socket.send("bla bla");
+ const closed = new Promise<void>((resolve) => {
+ socket.onclose = () => resolve();
+ });
+ const { data } = await new Promise<MessageEvent<string>>((res) =>
+ socket.onmessage = res
+ );
+ assertStrictEquals(data, "bla bla");
+ socket.close();
+ await closed;
+ }
+
+ await Promise.all([server(), client()]);
+ },
+);
+
+// https://github.com/denoland/deno/issues/12741
+// https://github.com/denoland/deno/pull/12746
+// https://github.com/denoland/deno/pull/12798
+Deno.test(
+ { permissions: { net: true, run: true } },
+ async function httpServerDeleteRequestHasBody() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ hostname, port });
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const event = await httpConn.nextRequest() as Deno.RequestEvent;
+ assert(event.request.body);
+ const response = new Response();
+ await event.respondWith(response);
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = ["-X", "DELETE", url];
+ const { success } = await new Deno.Command("curl", {
+ args,
+ stdout: "null",
+ stderr: "null",
+ }).output();
+ assert(success);
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerRespondNonAsciiUint8Array() {
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.body, null);
+ await respondWith(
+ new Response(new Uint8Array([128]), {}),
+ );
+ })();
+
+ const resp = await fetch(`http://localhost:${listenPort}/`);
+ assertEquals(resp.status, 200);
+ const body = await resp.arrayBuffer();
+ assertEquals(new Uint8Array(body), new Uint8Array([128]));
+
+ await promise;
+ httpConn!.close();
+ },
+);
+
+function tmpUnixSocketPath(): string {
+ const folder = Deno.makeTempDirSync();
+ return join(folder, "socket");
+}
+
+// https://github.com/denoland/deno/pull/13628
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function httpServerOnUnixSocket() {
+ const filePath = tmpUnixSocketPath();
+
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ path: filePath, transport: "unix" });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ const url = new URL(request.url);
+ assertEquals(url.protocol, "http+unix:");
+ assertEquals(decodeURIComponent(url.host), filePath);
+ assertEquals(url.pathname, "/path/name");
+ await respondWith(new Response("", { headers: {} }));
+ })();
+
+ // fetch() does not supports unix domain sockets yet https://github.com/denoland/deno/issues/8821
+ const conn = await Deno.connect({ path: filePath, transport: "unix" });
+ const encoder = new TextEncoder();
+ // The Host header must be present and empty if it is not a Internet host name (RFC2616, Section 14.23)
+ const body = `GET /path/name HTTP/1.1\r\nHost:\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ const resp = new Uint8Array(200);
+ const readResult = await conn.read(resp);
+ assertEquals(readResult, 138);
+
+ conn.close();
+
+ await promise;
+ httpConn!.close();
+ },
+);
+
+/* Automatic Body Compression */
+
+const decoder = new TextDecoder();
+
+Deno.test({
+ name: "http server compresses body - check headers",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ const listener = Deno.listen({ hostname, port });
+
+ const data = { hello: "deno", now: "with", compressed: "body" };
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(JSON.stringify(data), {
+ headers: { "content-type": "application/json" },
+ });
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: gzip\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server compresses body - check body",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ const listener = Deno.listen({ hostname, port });
+
+ const data = { hello: "deno", now: "with", compressed: "body" };
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(JSON.stringify(data), {
+ headers: { "content-type": "application/json" },
+ });
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const proc = new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).spawn();
+ const status = await proc.status;
+ assert(status.success);
+ const stdout = proc.stdout
+ .pipeThrough(new DecompressionStream("gzip"))
+ .pipeThrough(new TextDecoderStream());
+ let body = "";
+ for await (const chunk of stdout) {
+ body += chunk;
+ }
+ assertEquals(JSON.parse(body), data);
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server doesn't compress small body",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno" }),
+ {
+ headers: { "content-type": "application/json" },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout).toLocaleLowerCase();
+ assert(output.includes("vary: accept-encoding\r\n"));
+ assert(!output.includes("content-encoding: "));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server respects accept-encoding weights",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(
+ request.headers.get("Accept-Encoding"),
+ "gzip;q=0.8, br;q=1.0, *;q=0.1",
+ );
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: { "content-type": "application/json" },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip;q=0.8, br;q=1.0, *;q=0.1",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: br\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server augments vary header",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: { "content-type": "application/json", vary: "Accept" },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding, Accept\r\n"));
+ assert(output.includes("content-encoding: gzip\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server weakens etag header",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: {
+ "content-type": "application/json",
+ etag: "33a64df551425fcc55e4d42a148795d9f25f89d4",
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "curl",
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(
+ output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"),
+ );
+ assert(output.includes("content-encoding: gzip\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server passes through weak etag header",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: {
+ "content-type": "application/json",
+ etag: "W/33a64df551425fcc55e4d42a148795d9f25f89d4",
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(
+ output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"),
+ );
+ assert(output.includes("content-encoding: gzip\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server doesn't compress body when no-transform is set",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: {
+ "content-type": "application/json",
+ "cache-control": "no-transform",
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(!output.includes("content-encoding: "));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server doesn't compress body when content-range is set",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: {
+ "content-type": "application/json",
+ "content-range": "bytes 200-100/67589",
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(!output.includes("content-encoding: "));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server compresses streamed bodies - check headers",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const encoder = new TextEncoder();
+ const listener = Deno.listen({ hostname, port });
+
+ const data = { hello: "deno", now: "with", compressed: "body" };
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const bodyInit = new ReadableStream({
+ start(controller) {
+ controller.enqueue(encoder.encode(JSON.stringify(data)));
+ controller.close();
+ },
+ });
+ const response = new Response(
+ bodyInit,
+ { headers: { "content-type": "application/json" } },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "curl",
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: gzip\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server compresses streamed bodies - check body",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const encoder = new TextEncoder();
+ const listener = Deno.listen({ hostname, port });
+
+ const data = { hello: "deno", now: "with", compressed: "body" };
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const bodyInit = new ReadableStream({
+ start(controller) {
+ controller.enqueue(encoder.encode(JSON.stringify(data)));
+ controller.close();
+ },
+ });
+ const response = new Response(
+ bodyInit,
+ { headers: { "content-type": "application/json" } },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const proc = new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).spawn();
+ const status = await proc.status;
+ assert(status.success);
+ const stdout = proc.stdout
+ .pipeThrough(new DecompressionStream("gzip"))
+ .pipeThrough(new TextDecoderStream());
+ let body = "";
+ for await (const chunk of stdout) {
+ body += chunk;
+ }
+ assertEquals(JSON.parse(body), data);
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server updates content-length header if compression is applied",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ let contentLength: string;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const body = JSON.stringify({
+ hello: "deno",
+ now: "with",
+ compressed: "body",
+ });
+ contentLength = String(body.length);
+ const response = new Response(
+ body,
+ {
+ headers: {
+ "content-type": "application/json",
+ "content-length": contentLength,
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: gzip\r\n"));
+ // Ensure the content-length header is updated (but don't check the exact length).
+ assert(!output.includes(`content-length: ${contentLength}\r\n`));
+ assert(output.includes("content-length: "));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server compresses when accept-encoding is deflate, gzip",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ let contentLength: string;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "deflate, gzip");
+ const body = "x".repeat(10000);
+ contentLength = String(body.length);
+ const response = new Response(
+ body,
+ {
+ headers: {
+ "content-length": contentLength,
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const cmd = [
+ "curl",
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ // "--compressed", // Windows curl does not support --compressed
+ "--header",
+ "Accept-Encoding: deflate, gzip",
+ ];
+ // deno-lint-ignore no-deprecated-deno-api
+ const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" });
+ const status = await proc.status();
+ assert(status.success);
+ const output = decoder.decode(await proc.output());
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: gzip\r\n"));
+ // Ensure the content-length header is updated.
+ assert(!output.includes(`content-length: ${contentLength}\r\n`));
+ assert(output.includes("content-length: "));
+ proc.close();
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server custom content-encoding is left untouched",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ let contentLength: string;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "deflate, gzip");
+ const body = new Uint8Array([3, 1, 4, 1]);
+ contentLength = String(body.length);
+ const response = new Response(
+ body,
+ {
+ headers: {
+ "content-length": contentLength,
+ "content-encoding": "arbitrary",
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const cmd = [
+ "curl",
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ // "--compressed", // Windows curl does not support --compressed
+ "--header",
+ "Accept-Encoding: deflate, gzip",
+ ];
+ // deno-lint-ignore no-deprecated-deno-api
+ const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" });
+ const status = await proc.status();
+ assert(status.success);
+ const output = decoder.decode(await proc.output());
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: arbitrary\r\n"));
+ proc.close();
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerReadLargeBodyWithContentLength() {
+ const TLS_PACKET_SIZE = 16 * 1024 + 256;
+ // We want the body to be read in multiple packets
+ const body = "aa\n" + "deno.land large body\n".repeat(TLS_PACKET_SIZE) +
+ "zz";
+
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(await request.text(), body);
+ await respondWith(new Response(body));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ method: "POST",
+ headers: { "connection": "close" },
+ body,
+ });
+ const text = await resp.text();
+ assertEquals(text, body);
+ await promise;
+
+ httpConn!.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerReadLargeBodyWithTransferChunked() {
+ const TLS_PACKET_SIZE = 16 * 1024 + 256;
+
+ // We want the body to be read in multiple packets
+ const chunks = [
+ "aa\n",
+ "deno.land large body\n".repeat(TLS_PACKET_SIZE),
+ "zz",
+ ];
+
+ const body = chunks.join("");
+
+ const stream = new TransformStream();
+ const writer = stream.writable.getWriter();
+ for (const chunk of chunks) {
+ writer.write(new TextEncoder().encode(chunk));
+ }
+ writer.close();
+
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(await request.text(), body);
+ await respondWith(new Response(body));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ method: "POST",
+ headers: { "connection": "close" },
+ body: stream.readable,
+ });
+ const text = await resp.text();
+ assertEquals(text, body);
+ await promise;
+
+ httpConn!.close();
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function httpServerWithoutExclusiveAccessToTcp() {
+ const port = listenPort;
+ const listener = Deno.listen({ port });
+
+ const [clientConn, serverConn] = await Promise.all([
+ Deno.connect({ port }),
+ listener.accept(),
+ ]);
+
+ const buf = new Uint8Array(128);
+ const readPromise = serverConn.read(buf);
+ assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource);
+
+ clientConn.close();
+ listener.close();
+ await readPromise;
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true, read: true },
+ },
+ async function httpServerWithoutExclusiveAccessToTls() {
+ const hostname = "localhost";
+ const port = listenPort;
+ const listener = Deno.listenTls({
+ hostname,
+ port,
+ cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"),
+ key: await Deno.readTextFile("tests/testdata/tls/localhost.key"),
+ });
+
+ const caCerts = [
+ await Deno.readTextFile("tests/testdata/tls/RootCA.pem"),
+ ];
+ const [clientConn, serverConn] = await Promise.all([
+ Deno.connectTls({ hostname, port, caCerts }),
+ listener.accept(),
+ ]);
+ await Promise.all([clientConn.handshake(), serverConn.handshake()]);
+
+ const buf = new Uint8Array(128);
+ const readPromise = serverConn.read(buf);
+ assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource);
+
+ clientConn.close();
+ listener.close();
+ await readPromise;
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function httpServerWithoutExclusiveAccessToUnixSocket() {
+ const filePath = tmpUnixSocketPath();
+ const listener = Deno.listen({ path: filePath, transport: "unix" });
+
+ const [clientConn, serverConn] = await Promise.all([
+ Deno.connect({ path: filePath, transport: "unix" }),
+ listener.accept(),
+ ]);
+
+ const buf = new Uint8Array(128);
+ const readPromise = serverConn.read(buf);
+ assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource);
+
+ clientConn.close();
+ listener.close();
+ await readPromise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerRequestResponseClone() {
+ const body = "deno".repeat(64 * 1024);
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ const clone = request.clone();
+ const reader = clone.body!.getReader();
+
+ // get first chunk from branch2
+ const clonedChunks = [];
+ const { value, done } = await reader.read();
+ assert(!done);
+ clonedChunks.push(value);
+
+ // consume request after first chunk single read
+ // readAll should read correctly the rest of the body.
+ // firstChunk should be in the stream internal buffer
+ const body1 = await request.text();
+
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ clonedChunks.push(value);
+ }
+ let offset = 0;
+ const body2 = new Uint8Array(body.length);
+ for (const chunk of clonedChunks) {
+ body2.set(chunk, offset);
+ offset += chunk.byteLength;
+ }
+
+ assertEquals(body1, body);
+ assertEquals(body1, new TextDecoder().decode(body2));
+ await respondWith(new Response(body));
+ })();
+
+ const response = await fetch(`http://localhost:${listenPort}`, {
+ body,
+ method: "POST",
+ });
+ const clone = response.clone();
+ assertEquals(await response.text(), await clone.text());
+
+ await promise;
+ httpConn!.close();
+ },
+);
+
+Deno.test({
+ name: "http server compresses and flushes each chunk of a streamed resource",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ const port2 = listenPort2;
+
+ const encoder = new TextEncoder();
+ const listener = Deno.listen({ hostname, port });
+ const listener2 = Deno.listen({ hostname, port: port2 });
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const resp = await fetch(`http://${hostname}:${port2}/`);
+ await respondWith(resp);
+ listener.close();
+ }
+
+ const ts = new TransformStream();
+ const writer = ts.writable.getWriter();
+ writer.write(encoder.encode("hello"));
+
+ let httpConn2: Deno.HttpConn;
+ async function server2() {
+ const tcpConn = await listener2.accept();
+ httpConn2 = Deno.serveHttp(tcpConn);
+ const e = await httpConn2.nextRequest();
+ assert(e);
+ await e.respondWith(
+ new Response(ts.readable, {
+ headers: { "Content-Type": "text/plain" },
+ }),
+ );
+ listener2.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ "--no-buffer",
+ ];
+ const proc = new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).spawn();
+ const stdout = proc.stdout
+ .pipeThrough(new DecompressionStream("gzip"))
+ .pipeThrough(new TextDecoderStream());
+ let body = "";
+ for await (const chunk of stdout) {
+ body += chunk;
+ if (body === "hello") {
+ writer.write(encoder.encode(" world"));
+ writer.close();
+ }
+ }
+ assertEquals(body, "hello world");
+ const status = await proc.status;
+ assert(status.success);
+ }
+
+ await Promise.all([server(), server2(), client()]);
+ httpConn!.close();
+ httpConn2!.close();
+ },
+});
+
+Deno.test("case insensitive comma value finder", async (t) => {
+ const cases = /** @type {[string, boolean][]} */ ([
+ ["websocket", true],
+ ["wEbSOcKET", true],
+ [",wEbSOcKET", true],
+ [",wEbSOcKET,", true],
+ [", wEbSOcKET ,", true],
+ ["test, wEbSOcKET ,", true],
+ ["test ,\twEbSOcKET\t\t ,", true],
+ ["test , wEbSOcKET", true],
+ ["test, asdf,web,wEbSOcKET", true],
+ ["test, asdf,web,wEbSOcKETs", false],
+ ["test, asdf,awebsocket,wEbSOcKETs", false],
+ ]);
+
+ const findValue = buildCaseInsensitiveCommaValueFinder("websocket");
+ for (const [input, expected] of cases) {
+ await t.step(input.toString(), () => {
+ const actual = findValue(input);
+ assertEquals(actual, expected);
+ });
+ }
+});
+
+async function httpServerWithErrorBody(
+ listener: Deno.Listener,
+ compression: boolean,
+): Promise<Deno.HttpConn> {
+ const conn = await listener.accept();
+ listener.close();
+ const httpConn = Deno.serveHttp(conn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { respondWith } = e;
+ const originalErr = new Error("boom");
+ const rs = new ReadableStream({
+ async start(controller) {
+ controller.enqueue(new Uint8Array([65]));
+ await delay(1000);
+ controller.error(originalErr);
+ },
+ });
+ const init = compression ? { headers: { "content-type": "text/plain" } } : {};
+ const response = new Response(rs, init);
+ const err = await assertRejects(() => respondWith(response));
+ assert(err === originalErr);
+ return httpConn;
+}
+
+for (const compression of [true, false]) {
+ Deno.test({
+ name: `http server errors stream if response body errors (http/1.1${
+ compression ? " + compression" : ""
+ })`,
+ permissions: { net: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const listener = Deno.listen({ hostname, port });
+ const server = httpServerWithErrorBody(listener, compression);
+
+ const conn = await Deno.connect({ hostname, port });
+ const msg = new TextEncoder().encode(
+ `GET / HTTP/1.1\r\nHost: ${hostname}:${port}\r\n\r\n`,
+ );
+ const nwritten = await conn.write(msg);
+ assertEquals(nwritten, msg.byteLength);
+
+ const buf = new Uint8Array(1024);
+ const nread = await conn.read(buf);
+ assert(nread);
+ const data = new TextDecoder().decode(buf.subarray(0, nread));
+ assert(data.endsWith("1\r\nA\r\n"));
+ const nread2 = await conn.read(buf); // connection should be closed now because the stream errored
+ assertEquals(nread2, null);
+ conn.close();
+
+ const httpConn = await server;
+ httpConn.close();
+ },
+ });
+
+ Deno.test({
+ name: `http server errors stream if response body errors (http/1.1 + fetch${
+ compression ? " + compression" : ""
+ })`,
+ permissions: { net: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const listener = Deno.listen({ hostname, port });
+ const server = httpServerWithErrorBody(listener, compression);
+
+ const resp = await fetch(`http://${hostname}:${port}/`);
+ assert(resp.body);
+ const reader = resp.body.getReader();
+ const result = await reader.read();
+ assert(!result.done);
+ assertEquals(result.value, new Uint8Array([65]));
+ const err = await assertRejects(() => reader.read());
+ assert(err instanceof TypeError);
+ assert(err.message.includes("unexpected EOF"));
+
+ const httpConn = await server;
+ httpConn.close();
+ },
+ });
+
+ Deno.test({
+ name: `http server errors stream if response body errors (http/2 + fetch${
+ compression ? " + compression" : ""
+ }))`,
+ permissions: { net: true, read: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const listener = Deno.listenTls({
+ hostname,
+ port,
+ cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"),
+ key: await Deno.readTextFile("tests/testdata/tls/localhost.key"),
+ alpnProtocols: ["h2"],
+ });
+ const server = httpServerWithErrorBody(listener, compression);
+
+ const caCert = Deno.readTextFileSync("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const resp = await fetch(`https://${hostname}:${port}/`, { client });
+ client.close();
+ assert(resp.body);
+ const reader = resp.body.getReader();
+ const result = await reader.read();
+ assert(!result.done);
+ assertEquals(result.value, new Uint8Array([65]));
+ const err = await assertRejects(() => reader.read());
+ assert(err instanceof TypeError);
+ assert(err.message.includes("unexpected internal error encountered"));
+
+ const httpConn = await server;
+ httpConn.close();
+ },
+ });
+}
+
+Deno.test({
+ name: "request signal is aborted when response errors",
+ permissions: { net: true },
+ async fn() {
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const ev = await httpConn.nextRequest();
+ const { request, respondWith } = ev!;
+
+ await delay(300);
+ await assertRejects(() => respondWith(new Response("Hello World")));
+ assert(request.signal.aborted);
+ })();
+
+ const abortController = new AbortController();
+
+ fetch(`http://127.0.0.1:${listenPort}/`, {
+ signal: abortController.signal,
+ }).catch(() => {
+ // ignore
+ });
+
+ await delay(100);
+ abortController.abort();
+ await promise;
+ httpConn!.close();
+ },
+});
+
+Deno.test(
+ async function httpConnExplicitResourceManagement() {
+ let promise;
+
+ {
+ const listen = Deno.listen({ port: listenPort });
+ promise = fetch(`http://localhost:${listenPort}/`).catch(() => null);
+ const serverConn = await listen.accept();
+ listen.close();
+
+ using _httpConn = Deno.serveHttp(serverConn);
+ }
+
+ const response = await promise;
+ assertEquals(response, null);
+ },
+);
+
+function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
+ // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6
+ const tp = new TextProtoReader(r);
+ let finished = false;
+ const chunks: Array<{
+ offset: number;
+ data: Uint8Array;
+ }> = [];
+ async function read(buf: Uint8Array): Promise<number | null> {
+ if (finished) return null;
+ const [chunk] = chunks;
+ if (chunk) {
+ const chunkRemaining = chunk.data.byteLength - chunk.offset;
+ const readLength = Math.min(chunkRemaining, buf.byteLength);
+ for (let i = 0; i < readLength; i++) {
+ buf[i] = chunk.data[chunk.offset + i];
+ }
+ chunk.offset += readLength;
+ if (chunk.offset === chunk.data.byteLength) {
+ chunks.shift();
+ // Consume \r\n;
+ if ((await tp.readLine()) === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ }
+ return readLength;
+ }
+ const line = await tp.readLine();
+ if (line === null) throw new Deno.errors.UnexpectedEof();
+ // TODO(bartlomieju): handle chunk extension
+ const [chunkSizeString] = line.split(";");
+ const chunkSize = parseInt(chunkSizeString, 16);
+ if (Number.isNaN(chunkSize) || chunkSize < 0) {
+ throw new Deno.errors.InvalidData("Invalid chunk size");
+ }
+ if (chunkSize > 0) {
+ if (chunkSize > buf.byteLength) {
+ let eof = await r.readFull(buf);
+ if (eof === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ const restChunk = new Uint8Array(chunkSize - buf.byteLength);
+ eof = await r.readFull(restChunk);
+ if (eof === null) {
+ throw new Deno.errors.UnexpectedEof();
+ } else {
+ chunks.push({
+ offset: 0,
+ data: restChunk,
+ });
+ }
+ return buf.byteLength;
+ } else {
+ const bufToFill = buf.subarray(0, chunkSize);
+ const eof = await r.readFull(bufToFill);
+ if (eof === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ // Consume \r\n
+ if ((await tp.readLine()) === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ return chunkSize;
+ }
+ } else {
+ assert(chunkSize === 0);
+ // Consume \r\n
+ if ((await r.readLine()) === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ await readTrailers(h, r);
+ finished = true;
+ return null;
+ }
+ }
+ return { read };
+}
+
+async function readTrailers(
+ headers: Headers,
+ r: BufReader,
+) {
+ const trailers = parseTrailer(headers.get("trailer"));
+ if (trailers == null) return;
+ const trailerNames = [...trailers.keys()];
+ const tp = new TextProtoReader(r);
+ const result = await tp.readMimeHeader();
+ if (result == null) {
+ throw new Deno.errors.InvalidData("Missing trailer header.");
+ }
+ const undeclared = [...result.keys()].filter(
+ (k) => !trailerNames.includes(k),
+ );
+ if (undeclared.length > 0) {
+ throw new Deno.errors.InvalidData(
+ `Undeclared trailers: ${Deno.inspect(undeclared)}.`,
+ );
+ }
+ for (const [k, v] of result) {
+ headers.append(k, v);
+ }
+ const missingTrailers = trailerNames.filter((k) => !result.has(k));
+ if (missingTrailers.length > 0) {
+ throw new Deno.errors.InvalidData(
+ `Missing trailers: ${Deno.inspect(missingTrailers)}.`,
+ );
+ }
+ headers.delete("trailer");
+}
+
+function parseTrailer(field: string | null): Headers | undefined {
+ if (field == null) {
+ return undefined;
+ }
+ const trailerNames = field.split(",").map((v) => v.trim().toLowerCase());
+ if (trailerNames.length === 0) {
+ throw new Deno.errors.InvalidData("Empty trailer header.");
+ }
+ const prohibited = trailerNames.filter((k) => isProhibitedForTrailer(k));
+ if (prohibited.length > 0) {
+ throw new Deno.errors.InvalidData(
+ `Prohibited trailer names: ${Deno.inspect(prohibited)}.`,
+ );
+ }
+ return new Headers(trailerNames.map((key) => [key, ""]));
+}
+
+function isProhibitedForTrailer(key: string): boolean {
+ const s = new Set(["transfer-encoding", "content-length", "trailer"]);
+ return s.has(key.toLowerCase());
+}
diff --git a/tests/unit/image_bitmap_test.ts b/tests/unit/image_bitmap_test.ts
new file mode 100644
index 000000000..364f2a167
--- /dev/null
+++ b/tests/unit/image_bitmap_test.ts
@@ -0,0 +1,92 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assertEquals } from "./test_util.ts";
+
+function generateNumberedData(n: number): Uint8ClampedArray {
+ return new Uint8ClampedArray(
+ Array.from({ length: n }, (_, i) => [i + 1, 0, 0, 1]).flat(),
+ );
+}
+
+Deno.test(async function imageBitmapDirect() {
+ const data = generateNumberedData(3);
+ const imageData = new ImageData(data, 3, 1);
+ const imageBitmap = await createImageBitmap(imageData);
+ assertEquals(
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ Deno[Deno.internal].getBitmapData(imageBitmap),
+ new Uint8Array(data.buffer),
+ );
+});
+
+Deno.test(async function imageBitmapCrop() {
+ const data = generateNumberedData(3 * 3);
+ const imageData = new ImageData(data, 3, 3);
+ const imageBitmap = await createImageBitmap(imageData, 1, 1, 1, 1);
+ assertEquals(
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ Deno[Deno.internal].getBitmapData(imageBitmap),
+ new Uint8Array([5, 0, 0, 1]),
+ );
+});
+
+Deno.test(async function imageBitmapCropPartialNegative() {
+ const data = generateNumberedData(3 * 3);
+ const imageData = new ImageData(data, 3, 3);
+ const imageBitmap = await createImageBitmap(imageData, -1, -1, 2, 2);
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ // deno-fmt-ignore
+ assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 1, 0, 0, 1
+ ]));
+});
+
+Deno.test(async function imageBitmapCropGreater() {
+ const data = generateNumberedData(3 * 3);
+ const imageData = new ImageData(data, 3, 3);
+ const imageBitmap = await createImageBitmap(imageData, -1, -1, 5, 5);
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ // deno-fmt-ignore
+ assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 0, 0, 0, 0,
+ 0, 0, 0, 0, 4, 0, 0, 1, 5, 0, 0, 1, 6, 0, 0, 1, 0, 0, 0, 0,
+ 0, 0, 0, 0, 7, 0, 0, 1, 8, 0, 0, 1, 9, 0, 0, 1, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ ]));
+});
+
+Deno.test(async function imageBitmapScale() {
+ const data = generateNumberedData(3);
+ const imageData = new ImageData(data, 3, 1);
+ const imageBitmap = await createImageBitmap(imageData, {
+ resizeHeight: 5,
+ resizeWidth: 5,
+ resizeQuality: "pixelated",
+ });
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ // deno-fmt-ignore
+ assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
+ 1, 0, 0, 1, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 3, 0, 0, 1,
+ 1, 0, 0, 1, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 3, 0, 0, 1,
+ 1, 0, 0, 1, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 3, 0, 0, 1,
+ 1, 0, 0, 1, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 3, 0, 0, 1,
+ 1, 0, 0, 1, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 3, 0, 0, 1
+ ]));
+});
+
+Deno.test(async function imageBitmapFlipY() {
+ const data = generateNumberedData(9);
+ const imageData = new ImageData(data, 3, 3);
+ const imageBitmap = await createImageBitmap(imageData, {
+ imageOrientation: "flipY",
+ });
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ // deno-fmt-ignore
+ assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
+ 7, 0, 0, 1, 8, 0, 0, 1, 9, 0, 0, 1,
+ 4, 0, 0, 1, 5, 0, 0, 1, 6, 0, 0, 1,
+ 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1,
+ ]));
+});
diff --git a/tests/unit/image_data_test.ts b/tests/unit/image_data_test.ts
new file mode 100644
index 000000000..7156301a0
--- /dev/null
+++ b/tests/unit/image_data_test.ts
@@ -0,0 +1,53 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assertEquals } from "./test_util.ts";
+
+Deno.test(function imageDataInitializedWithSourceWidthAndHeight() {
+ const imageData = new ImageData(16, 9);
+
+ assertEquals(imageData.width, 16);
+ assertEquals(imageData.height, 9);
+ assertEquals(imageData.data.length, 16 * 9 * 4); // width * height * 4 (RGBA pixels)
+ assertEquals(imageData.colorSpace, "srgb");
+});
+
+Deno.test(function imageDataInitializedWithImageDataAndWidth() {
+ const imageData = new ImageData(new Uint8ClampedArray(16 * 9 * 4), 16);
+
+ assertEquals(imageData.width, 16);
+ assertEquals(imageData.height, 9);
+ assertEquals(imageData.data.length, 16 * 9 * 4); // width * height * 4 (RGBA pixels)
+ assertEquals(imageData.colorSpace, "srgb");
+});
+
+Deno.test(
+ function imageDataInitializedWithImageDataAndWidthAndHeightAndColorSpace() {
+ const imageData = new ImageData(new Uint8ClampedArray(16 * 9 * 4), 16, 9, {
+ colorSpace: "display-p3",
+ });
+
+ assertEquals(imageData.width, 16);
+ assertEquals(imageData.height, 9);
+ assertEquals(imageData.data.length, 16 * 9 * 4); // width * height * 4 (RGBA pixels)
+ assertEquals(imageData.colorSpace, "display-p3");
+ },
+);
+
+Deno.test(
+ async function imageDataUsedInWorker() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const url = import.meta.resolve(
+ "../testdata/workers/image_data_worker.ts",
+ );
+ const expectedData = 16;
+
+ const worker = new Worker(url, { type: "module" });
+ worker.onmessage = function (e) {
+ assertEquals(expectedData, e.data);
+ worker.terminate();
+ resolve();
+ };
+
+ await promise;
+ },
+);
diff --git a/tests/unit/internals_test.ts b/tests/unit/internals_test.ts
new file mode 100644
index 000000000..bb4c21793
--- /dev/null
+++ b/tests/unit/internals_test.ts
@@ -0,0 +1,10 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert } from "./test_util.ts";
+
+Deno.test(function internalsExists() {
+ const {
+ inspectArgs,
+ // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
+ } = Deno[Deno.internal];
+ assert(!!inspectArgs);
+});
diff --git a/tests/unit/intl_test.ts b/tests/unit/intl_test.ts
new file mode 100644
index 000000000..6e4de378c
--- /dev/null
+++ b/tests/unit/intl_test.ts
@@ -0,0 +1,7 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+Deno.test("Intl.v8BreakIterator should be undefined", () => {
+ // @ts-expect-error Intl.v8BreakIterator is not a standard API
+ assertEquals(Intl.v8BreakIterator, undefined);
+});
diff --git a/tests/unit/io_test.ts b/tests/unit/io_test.ts
new file mode 100644
index 000000000..04c9dab4b
--- /dev/null
+++ b/tests/unit/io_test.ts
@@ -0,0 +1,77 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+import { Buffer } from "@test_util/std/io/buffer.ts";
+
+const DEFAULT_BUF_SIZE = 32 * 1024;
+
+type Spy = { calls: number };
+
+function repeat(c: string, bytes: number): Uint8Array {
+ assertEquals(c.length, 1);
+ const ui8 = new Uint8Array(bytes);
+ ui8.fill(c.charCodeAt(0));
+ return ui8;
+}
+
+function spyRead(obj: Buffer): Spy {
+ const spy: Spy = {
+ calls: 0,
+ };
+
+ const orig = obj.read.bind(obj);
+
+ obj.read = (p: Uint8Array): Promise<number | null> => {
+ spy.calls++;
+ return orig(p);
+ };
+
+ return spy;
+}
+
+Deno.test(async function copyWithDefaultBufferSize() {
+ const xBytes = repeat("b", DEFAULT_BUF_SIZE);
+ const reader = new Buffer(xBytes.buffer as ArrayBuffer);
+ const write = new Buffer();
+
+ const readSpy = spyRead(reader);
+
+ // deno-lint-ignore no-deprecated-deno-api
+ const n = await Deno.copy(reader, write);
+
+ assertEquals(n, xBytes.length);
+ assertEquals(write.length, xBytes.length);
+ assertEquals(readSpy.calls, 2); // read with DEFAULT_BUF_SIZE bytes + read with 0 bytes
+});
+
+Deno.test(async function copyWithCustomBufferSize() {
+ const bufSize = 1024;
+ const xBytes = repeat("b", DEFAULT_BUF_SIZE);
+ const reader = new Buffer(xBytes.buffer as ArrayBuffer);
+ const write = new Buffer();
+
+ const readSpy = spyRead(reader);
+
+ // deno-lint-ignore no-deprecated-deno-api
+ const n = await Deno.copy(reader, write, { bufSize });
+
+ assertEquals(n, xBytes.length);
+ assertEquals(write.length, xBytes.length);
+ assertEquals(readSpy.calls, DEFAULT_BUF_SIZE / bufSize + 1);
+});
+
+Deno.test({ permissions: { write: true } }, async function copyBufferToFile() {
+ const filePath = "test-file.txt";
+ // bigger than max File possible buffer 16kb
+ const bufSize = 32 * 1024;
+ const xBytes = repeat("b", bufSize);
+ const reader = new Buffer(xBytes.buffer as ArrayBuffer);
+ const write = await Deno.open(filePath, { write: true, create: true });
+
+ // deno-lint-ignore no-deprecated-deno-api
+ const n = await Deno.copy(reader, write, { bufSize });
+
+ assertEquals(n, xBytes.length);
+
+ write.close();
+ await Deno.remove(filePath);
+});
diff --git a/tests/unit/jupyter_test.ts b/tests/unit/jupyter_test.ts
new file mode 100644
index 000000000..07defe230
--- /dev/null
+++ b/tests/unit/jupyter_test.ts
@@ -0,0 +1,79 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assertEquals, assertThrows } from "./test_util.ts";
+
+// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
+const format = Deno[Deno.internal].jupyter.formatInner;
+
+Deno.test("Deno.jupyter is not available", () => {
+ assertThrows(
+ () => Deno.jupyter,
+ "Deno.jupyter is only available in `deno jupyter` subcommand.",
+ );
+});
+
+export async function assertFormattedAs(obj: unknown, result: object) {
+ const formatted = await format(obj);
+ assertEquals(formatted, result);
+}
+
+Deno.test("display(canvas) creates a PNG", async () => {
+ // Let's make a fake Canvas with a fake Data URL
+ class FakeCanvas {
+ toDataURL() {
+ return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAAVSURBVAiZY/zPwPCfAQ0woQtQQRAAzqkCCB/D3o0AAAAASUVORK5CYII=";
+ }
+ }
+ const canvas = new FakeCanvas();
+
+ await assertFormattedAs(canvas, {
+ "image/png":
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAAVSURBVAiZY/zPwPCfAQ0woQtQQRAAzqkCCB/D3o0AAAAASUVORK5CYII=",
+ });
+});
+
+Deno.test(
+ "class with a Symbol.for('Jupyter.display') function gets displayed",
+ async () => {
+ class Example {
+ x: number;
+
+ constructor(x: number) {
+ this.x = x;
+ }
+
+ [Symbol.for("Jupyter.display")]() {
+ return { "application/json": { x: this.x } };
+ }
+ }
+
+ const example = new Example(5);
+
+ // Now to check on the broadcast call being made
+ await assertFormattedAs(example, { "application/json": { x: 5 } });
+ },
+);
+
+Deno.test(
+ "class with an async Symbol.for('Jupyter.display') function gets displayed",
+ async () => {
+ class Example {
+ x: number;
+
+ constructor(x: number) {
+ this.x = x;
+ }
+
+ async [Symbol.for("Jupyter.display")]() {
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ return { "application/json": { x: this.x } };
+ }
+ }
+
+ const example = new Example(3);
+
+ // Now to check on the broadcast call being made
+ await assertFormattedAs(example, { "application/json": { x: 3 } });
+ },
+);
diff --git a/tests/unit/kv_queue_test.ts b/tests/unit/kv_queue_test.ts
new file mode 100644
index 000000000..e052dcbf7
--- /dev/null
+++ b/tests/unit/kv_queue_test.ts
@@ -0,0 +1,13 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals, assertFalse } from "./test_util.ts";
+
+Deno.test({}, async function queueTestDbClose() {
+ const db: Deno.Kv = await Deno.openKv(":memory:");
+ db.close();
+ try {
+ await db.listenQueue(() => {});
+ assertFalse(false);
+ } catch (e) {
+ assertEquals(e.message, "already closed");
+ }
+});
diff --git a/tests/unit/kv_queue_test_no_db_close.ts b/tests/unit/kv_queue_test_no_db_close.ts
new file mode 100644
index 000000000..947e1c5e6
--- /dev/null
+++ b/tests/unit/kv_queue_test_no_db_close.ts
@@ -0,0 +1,20 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals, assertNotEquals } from "./test_util.ts";
+
+Deno.test({
+ sanitizeOps: false,
+ sanitizeResources: false,
+}, async function queueTestNoDbClose() {
+ const db: Deno.Kv = await Deno.openKv(":memory:");
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let dequeuedMessage: unknown = null;
+ db.listenQueue((msg) => {
+ dequeuedMessage = msg;
+ resolve();
+ });
+ const res = await db.enqueue("test");
+ assert(res.ok);
+ assertNotEquals(res.versionstamp, null);
+ await promise;
+ assertEquals(dequeuedMessage, "test");
+});
diff --git a/tests/unit/kv_queue_undelivered_test.ts b/tests/unit/kv_queue_undelivered_test.ts
new file mode 100644
index 000000000..1fcefe7e2
--- /dev/null
+++ b/tests/unit/kv_queue_undelivered_test.ts
@@ -0,0 +1,59 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
+
+// TODO(igorzi): https://github.com/denoland/deno/issues/21437
+// let isCI: boolean;
+// try {
+// isCI = Deno.env.get("CI") !== undefined;
+// } catch {
+// isCI = true;
+// }
+
+function queueTest(name: string, fn: (db: Deno.Kv) => Promise<void>) {
+ // TODO(igorzi): https://github.com/denoland/deno/issues/21437
+ Deno.test.ignore({
+ name,
+ // https://github.com/denoland/deno/issues/18363
+ // ignore: Deno.build.os === "darwin" && isCI,
+ async fn() {
+ const db: Deno.Kv = await Deno.openKv(
+ ":memory:",
+ );
+ await fn(db);
+ },
+ });
+}
+
+async function collect<T>(
+ iter: Deno.KvListIterator<T>,
+): Promise<Deno.KvEntry<T>[]> {
+ const entries: Deno.KvEntry<T>[] = [];
+ for await (const entry of iter) {
+ entries.push(entry);
+ }
+ return entries;
+}
+
+queueTest("queue with undelivered", async (db) => {
+ const listener = db.listenQueue((_msg) => {
+ throw new TypeError("dequeue error");
+ });
+ try {
+ await db.enqueue("test", {
+ keysIfUndelivered: [["queue_failed", "a"], ["queue_failed", "b"]],
+ backoffSchedule: [10, 20],
+ });
+ await sleep(3000);
+ const undelivered = await collect(db.list({ prefix: ["queue_failed"] }));
+ assertEquals(undelivered.length, 2);
+ assertEquals(undelivered[0].key, ["queue_failed", "a"]);
+ assertEquals(undelivered[0].value, "test");
+ assertEquals(undelivered[1].key, ["queue_failed", "b"]);
+ assertEquals(undelivered[1].value, "test");
+ } finally {
+ db.close();
+ await listener;
+ }
+});
diff --git a/tests/unit/kv_test.ts b/tests/unit/kv_test.ts
new file mode 100644
index 000000000..5780d9900
--- /dev/null
+++ b/tests/unit/kv_test.ts
@@ -0,0 +1,2321 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ AssertionError,
+ assertNotEquals,
+ assertRejects,
+ assertThrows,
+} from "./test_util.ts";
+import { assertType, IsExact } from "@test_util/std/testing/types.ts";
+
+const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
+
+let isCI: boolean;
+try {
+ isCI = Deno.env.get("CI") !== undefined;
+} catch {
+ isCI = true;
+}
+
+// Defined in test_util/src/lib.rs
+Deno.env.set("DENO_KV_ACCESS_TOKEN", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+
+Deno.test({
+ name: "openKv :memory: no permissions",
+ permissions: {},
+ async fn() {
+ const db = await Deno.openKv(":memory:");
+ await db.close();
+ },
+});
+
+Deno.test({
+ name: "openKv invalid filenames",
+ permissions: {},
+ async fn() {
+ await assertRejects(
+ async () => await Deno.openKv(""),
+ TypeError,
+ "Filename cannot be empty",
+ );
+ await assertRejects(
+ async () => await Deno.openKv(":foo"),
+ TypeError,
+ "Filename cannot start with ':' unless prefixed with './'",
+ );
+ },
+});
+
+function dbTest(name: string, fn: (db: Deno.Kv) => Promise<void> | void) {
+ Deno.test({
+ name,
+ // https://github.com/denoland/deno/issues/18363
+ ignore: Deno.build.os === "darwin" && isCI,
+ async fn() {
+ const db: Deno.Kv = await Deno.openKv(":memory:");
+ try {
+ await fn(db);
+ } finally {
+ db.close();
+ }
+ },
+ });
+}
+
+function queueTest(name: string, fn: (db: Deno.Kv) => Promise<void>) {
+ Deno.test({
+ name,
+ // https://github.com/denoland/deno/issues/18363
+ ignore: Deno.build.os === "darwin" && isCI,
+ async fn() {
+ const db: Deno.Kv = await Deno.openKv(":memory:");
+ await fn(db);
+ },
+ });
+}
+
+const ZERO_VERSIONSTAMP = "00000000000000000000";
+
+dbTest("basic read-write-delete and versionstamps", async (db) => {
+ const result1 = await db.get(["a"]);
+ assertEquals(result1.key, ["a"]);
+ assertEquals(result1.value, null);
+ assertEquals(result1.versionstamp, null);
+
+ const setRes = await db.set(["a"], "b");
+ assert(setRes.ok);
+ assert(setRes.versionstamp > ZERO_VERSIONSTAMP);
+ const result2 = await db.get(["a"]);
+ assertEquals(result2.key, ["a"]);
+ assertEquals(result2.value, "b");
+ assertEquals(result2.versionstamp, setRes.versionstamp);
+
+ const setRes2 = await db.set(["a"], "c");
+ assert(setRes2.ok);
+ assert(setRes2.versionstamp > setRes.versionstamp);
+ const result3 = await db.get(["a"]);
+ assertEquals(result3.key, ["a"]);
+ assertEquals(result3.value, "c");
+ assertEquals(result3.versionstamp, setRes2.versionstamp);
+
+ await db.delete(["a"]);
+ const result4 = await db.get(["a"]);
+ assertEquals(result4.key, ["a"]);
+ assertEquals(result4.value, null);
+ assertEquals(result4.versionstamp, null);
+});
+
+const VALUE_CASES = [
+ { name: "string", value: "hello" },
+ { name: "number", value: 42 },
+ { name: "bigint", value: 42n },
+ { name: "boolean", value: true },
+ { name: "null", value: null },
+ { name: "undefined", value: undefined },
+ { name: "Date", value: new Date(0) },
+ { name: "Uint8Array", value: new Uint8Array([1, 2, 3]) },
+ { name: "ArrayBuffer", value: new ArrayBuffer(3) },
+ { name: "array", value: [1, 2, 3] },
+ { name: "object", value: { a: 1, b: 2 } },
+ { name: "nested array", value: [[1, 2], [3, 4]] },
+ { name: "nested object", value: { a: { b: 1 } } },
+];
+
+for (const { name, value } of VALUE_CASES) {
+ dbTest(`set and get ${name} value`, async (db) => {
+ await db.set(["a"], value);
+ const result = await db.get(["a"]);
+ assertEquals(result.key, ["a"]);
+ assertEquals(result.value, value);
+ });
+}
+
+dbTest("set and get recursive object", async (db) => {
+ // deno-lint-ignore no-explicit-any
+ const value: any = { a: undefined };
+ value.a = value;
+ await db.set(["a"], value);
+ const result = await db.get(["a"]);
+ assertEquals(result.key, ["a"]);
+ // deno-lint-ignore no-explicit-any
+ const resultValue: any = result.value;
+ assert(resultValue.a === resultValue);
+});
+
+// invalid values (as per structured clone algorithm with _for storage_, NOT JSON)
+const INVALID_VALUE_CASES = [
+ { name: "function", value: () => {} },
+ { name: "symbol", value: Symbol() },
+ { name: "WeakMap", value: new WeakMap() },
+ { name: "WeakSet", value: new WeakSet() },
+ {
+ name: "WebAssembly.Module",
+ value: new WebAssembly.Module(
+ new Uint8Array([0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]),
+ ),
+ },
+ {
+ name: "SharedArrayBuffer",
+ value: new SharedArrayBuffer(3),
+ },
+];
+
+for (const { name, value } of INVALID_VALUE_CASES) {
+ dbTest(`set and get ${name} value (invalid)`, async (db) => {
+ await assertRejects(
+ async () => await db.set(["a"], value),
+ Error,
+ );
+ const res = await db.get(["a"]);
+ assertEquals(res.key, ["a"]);
+ assertEquals(res.value, null);
+ });
+}
+
+const keys = [
+ ["a"],
+ ["a", "b"],
+ ["a", "b", "c"],
+ [1],
+ ["a", 1],
+ ["a", 1, "b"],
+ [1n],
+ ["a", 1n],
+ ["a", 1n, "b"],
+ [true],
+ ["a", true],
+ ["a", true, "b"],
+ [new Uint8Array([1, 2, 3])],
+ ["a", new Uint8Array([1, 2, 3])],
+ ["a", new Uint8Array([1, 2, 3]), "b"],
+ [1, 1n, true, new Uint8Array([1, 2, 3]), "a"],
+];
+
+for (const key of keys) {
+ dbTest(`set and get ${Deno.inspect(key)} key`, async (db) => {
+ await db.set(key, "b");
+ const result = await db.get(key);
+ assertEquals(result.key, key);
+ assertEquals(result.value, "b");
+ });
+}
+
+const INVALID_KEYS = [
+ [null],
+ [undefined],
+ [],
+ [{}],
+ [new Date()],
+ [new ArrayBuffer(3)],
+ [new Uint8Array([1, 2, 3]).buffer],
+ [["a", "b"]],
+];
+
+for (const key of INVALID_KEYS) {
+ dbTest(`set and get invalid key ${Deno.inspect(key)}`, async (db) => {
+ await assertRejects(
+ async () => {
+ // @ts-ignore - we are testing invalid keys
+ await db.set(key, "b");
+ },
+ Error,
+ );
+ });
+}
+
+dbTest("compare and mutate", async (db) => {
+ await db.set(["t"], "1");
+
+ const currentValue = await db.get(["t"]);
+ assert(currentValue.versionstamp);
+ assert(currentValue.versionstamp > ZERO_VERSIONSTAMP);
+
+ let res = await db.atomic()
+ .check({ key: ["t"], versionstamp: currentValue.versionstamp })
+ .set(currentValue.key, "2")
+ .commit();
+ assert(res.ok);
+ assert(res.versionstamp > currentValue.versionstamp);
+
+ const newValue = await db.get(["t"]);
+ assertEquals(newValue.versionstamp, res.versionstamp);
+ assertEquals(newValue.value, "2");
+
+ res = await db.atomic()
+ .check({ key: ["t"], versionstamp: currentValue.versionstamp })
+ .set(currentValue.key, "3")
+ .commit();
+ assert(!res.ok);
+
+ const newValue2 = await db.get(["t"]);
+ assertEquals(newValue2.versionstamp, newValue.versionstamp);
+ assertEquals(newValue2.value, "2");
+});
+
+dbTest("compare and mutate not exists", async (db) => {
+ let res = await db.atomic()
+ .check({ key: ["t"], versionstamp: null })
+ .set(["t"], "1")
+ .commit();
+ assert(res.ok);
+ assert(res.versionstamp > ZERO_VERSIONSTAMP);
+
+ const newValue = await db.get(["t"]);
+ assertEquals(newValue.versionstamp, res.versionstamp);
+ assertEquals(newValue.value, "1");
+
+ res = await db.atomic()
+ .check({ key: ["t"], versionstamp: null })
+ .set(["t"], "2")
+ .commit();
+ assert(!res.ok);
+});
+
+dbTest("atomic mutation helper (sum)", async (db) => {
+ await db.set(["t"], new Deno.KvU64(42n));
+ assertEquals((await db.get(["t"])).value, new Deno.KvU64(42n));
+
+ await db.atomic().sum(["t"], 1n).commit();
+ assertEquals((await db.get(["t"])).value, new Deno.KvU64(43n));
+});
+
+dbTest("atomic mutation helper (min)", async (db) => {
+ await db.set(["t"], new Deno.KvU64(42n));
+ assertEquals((await db.get(["t"])).value, new Deno.KvU64(42n));
+
+ await db.atomic().min(["t"], 1n).commit();
+ assertEquals((await db.get(["t"])).value, new Deno.KvU64(1n));
+
+ await db.atomic().min(["t"], 2n).commit();
+ assertEquals((await db.get(["t"])).value, new Deno.KvU64(1n));
+});
+
+dbTest("atomic mutation helper (max)", async (db) => {
+ await db.set(["t"], new Deno.KvU64(42n));
+ assertEquals((await db.get(["t"])).value, new Deno.KvU64(42n));
+
+ await db.atomic().max(["t"], 41n).commit();
+ assertEquals((await db.get(["t"])).value, new Deno.KvU64(42n));
+
+ await db.atomic().max(["t"], 43n).commit();
+ assertEquals((await db.get(["t"])).value, new Deno.KvU64(43n));
+});
+
+dbTest("compare multiple and mutate", async (db) => {
+ const setRes1 = await db.set(["t1"], "1");
+ const setRes2 = await db.set(["t2"], "2");
+ assert(setRes1.ok);
+ assert(setRes1.versionstamp > ZERO_VERSIONSTAMP);
+ assert(setRes2.ok);
+ assert(setRes2.versionstamp > ZERO_VERSIONSTAMP);
+
+ const currentValue1 = await db.get(["t1"]);
+ assertEquals(currentValue1.versionstamp, setRes1.versionstamp);
+ const currentValue2 = await db.get(["t2"]);
+ assertEquals(currentValue2.versionstamp, setRes2.versionstamp);
+
+ const res = await db.atomic()
+ .check({ key: ["t1"], versionstamp: currentValue1.versionstamp })
+ .check({ key: ["t2"], versionstamp: currentValue2.versionstamp })
+ .set(currentValue1.key, "3")
+ .set(currentValue2.key, "4")
+ .commit();
+ assert(res.ok);
+ assert(res.versionstamp > setRes2.versionstamp);
+
+ const newValue1 = await db.get(["t1"]);
+ assertEquals(newValue1.versionstamp, res.versionstamp);
+ assertEquals(newValue1.value, "3");
+ const newValue2 = await db.get(["t2"]);
+ assertEquals(newValue2.versionstamp, res.versionstamp);
+ assertEquals(newValue2.value, "4");
+
+ // just one of the two checks failed
+ const res2 = await db.atomic()
+ .check({ key: ["t1"], versionstamp: newValue1.versionstamp })
+ .check({ key: ["t2"], versionstamp: null })
+ .set(newValue1.key, "5")
+ .set(newValue2.key, "6")
+ .commit();
+ assert(!res2.ok);
+
+ const newValue3 = await db.get(["t1"]);
+ assertEquals(newValue3.versionstamp, res.versionstamp);
+ assertEquals(newValue3.value, "3");
+ const newValue4 = await db.get(["t2"]);
+ assertEquals(newValue4.versionstamp, res.versionstamp);
+ assertEquals(newValue4.value, "4");
+});
+
+dbTest("atomic mutation ordering (set before delete)", async (db) => {
+ await db.set(["a"], "1");
+ const res = await db.atomic()
+ .set(["a"], "2")
+ .delete(["a"])
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assertEquals(result.value, null);
+});
+
+dbTest("atomic mutation ordering (delete before set)", async (db) => {
+ await db.set(["a"], "1");
+ const res = await db.atomic()
+ .delete(["a"])
+ .set(["a"], "2")
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assertEquals(result.value, "2");
+});
+
+dbTest("atomic mutation type=set", async (db) => {
+ const res = await db.atomic()
+ .mutate({ key: ["a"], value: "1", type: "set" })
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assertEquals(result.value, "1");
+});
+
+dbTest("atomic mutation type=set overwrite", async (db) => {
+ await db.set(["a"], "1");
+ const res = await db.atomic()
+ .mutate({ key: ["a"], value: "2", type: "set" })
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assertEquals(result.value, "2");
+});
+
+dbTest("atomic mutation type=delete", async (db) => {
+ await db.set(["a"], "1");
+ const res = await db.atomic()
+ .mutate({ key: ["a"], type: "delete" })
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assertEquals(result.value, null);
+});
+
+dbTest("atomic mutation type=delete no exists", async (db) => {
+ const res = await db.atomic()
+ .mutate({ key: ["a"], type: "delete" })
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assertEquals(result.value, null);
+});
+
+dbTest("atomic mutation type=sum", async (db) => {
+ await db.set(["a"], new Deno.KvU64(10n));
+ const res = await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "sum" })
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assertEquals(result.value, new Deno.KvU64(11n));
+});
+
+dbTest("atomic mutation type=sum no exists", async (db) => {
+ const res = await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "sum" })
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assert(result.value);
+ assertEquals(result.value, new Deno.KvU64(1n));
+});
+
+dbTest("atomic mutation type=sum wrap around", async (db) => {
+ await db.set(["a"], new Deno.KvU64(0xffffffffffffffffn));
+ const res = await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(10n), type: "sum" })
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assertEquals(result.value, new Deno.KvU64(9n));
+
+ const res2 = await db.atomic()
+ .mutate({
+ key: ["a"],
+ value: new Deno.KvU64(0xffffffffffffffffn),
+ type: "sum",
+ })
+ .commit();
+ assert(res2);
+ const result2 = await db.get(["a"]);
+ assertEquals(result2.value, new Deno.KvU64(8n));
+});
+
+dbTest("atomic mutation type=sum wrong type in db", async (db) => {
+ await db.set(["a"], 1);
+ await assertRejects(
+ async () => {
+ await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "sum" })
+ .commit();
+ },
+ TypeError,
+ "Failed to perform 'sum' mutation on a non-U64 value in the database",
+ );
+});
+
+dbTest("atomic mutation type=sum wrong type in mutation", async (db) => {
+ await db.set(["a"], new Deno.KvU64(1n));
+ await assertRejects(
+ async () => {
+ await db.atomic()
+ // @ts-expect-error wrong type is intentional
+ .mutate({ key: ["a"], value: 1, type: "sum" })
+ .commit();
+ },
+ TypeError,
+ "Failed to perform 'sum' mutation on a non-U64 operand",
+ );
+});
+
+dbTest("atomic mutation type=min", async (db) => {
+ await db.set(["a"], new Deno.KvU64(10n));
+ const res = await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(5n), type: "min" })
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assertEquals(result.value, new Deno.KvU64(5n));
+
+ const res2 = await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(15n), type: "min" })
+ .commit();
+ assert(res2);
+ const result2 = await db.get(["a"]);
+ assertEquals(result2.value, new Deno.KvU64(5n));
+});
+
+dbTest("atomic mutation type=min no exists", async (db) => {
+ const res = await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "min" })
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assert(result.value);
+ assertEquals(result.value, new Deno.KvU64(1n));
+});
+
+dbTest("atomic mutation type=min wrong type in db", async (db) => {
+ await db.set(["a"], 1);
+ await assertRejects(
+ async () => {
+ await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "min" })
+ .commit();
+ },
+ TypeError,
+ "Failed to perform 'min' mutation on a non-U64 value in the database",
+ );
+});
+
+dbTest("atomic mutation type=min wrong type in mutation", async (db) => {
+ await db.set(["a"], new Deno.KvU64(1n));
+ await assertRejects(
+ async () => {
+ await db.atomic()
+ // @ts-expect-error wrong type is intentional
+ .mutate({ key: ["a"], value: 1, type: "min" })
+ .commit();
+ },
+ TypeError,
+ "Failed to perform 'min' mutation on a non-U64 operand",
+ );
+});
+
+dbTest("atomic mutation type=max", async (db) => {
+ await db.set(["a"], new Deno.KvU64(10n));
+ const res = await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(5n), type: "max" })
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assertEquals(result.value, new Deno.KvU64(10n));
+
+ const res2 = await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(15n), type: "max" })
+ .commit();
+ assert(res2);
+ const result2 = await db.get(["a"]);
+ assertEquals(result2.value, new Deno.KvU64(15n));
+});
+
+dbTest("atomic mutation type=max no exists", async (db) => {
+ const res = await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "max" })
+ .commit();
+ assert(res.ok);
+ const result = await db.get(["a"]);
+ assert(result.value);
+ assertEquals(result.value, new Deno.KvU64(1n));
+});
+
+dbTest("atomic mutation type=max wrong type in db", async (db) => {
+ await db.set(["a"], 1);
+ await assertRejects(
+ async () => {
+ await db.atomic()
+ .mutate({ key: ["a"], value: new Deno.KvU64(1n), type: "max" })
+ .commit();
+ },
+ TypeError,
+ "Failed to perform 'max' mutation on a non-U64 value in the database",
+ );
+});
+
+dbTest("atomic mutation type=max wrong type in mutation", async (db) => {
+ await db.set(["a"], new Deno.KvU64(1n));
+ await assertRejects(
+ async () => {
+ await db.atomic()
+ // @ts-expect-error wrong type is intentional
+ .mutate({ key: ["a"], value: 1, type: "max" })
+ .commit();
+ },
+ TypeError,
+ "Failed to perform 'max' mutation on a non-U64 operand",
+ );
+});
+
+Deno.test("KvU64 comparison", () => {
+ const a = new Deno.KvU64(1n);
+ const b = new Deno.KvU64(1n);
+ assertEquals(a, b);
+ assertThrows(() => {
+ assertEquals(a, new Deno.KvU64(2n));
+ }, AssertionError);
+});
+
+Deno.test("KvU64 overflow", () => {
+ assertThrows(() => {
+ new Deno.KvU64(2n ** 64n);
+ }, RangeError);
+});
+
+Deno.test("KvU64 underflow", () => {
+ assertThrows(() => {
+ new Deno.KvU64(-1n);
+ }, RangeError);
+});
+
+Deno.test("KvU64 unbox", () => {
+ const a = new Deno.KvU64(1n);
+ assertEquals(a.value, 1n);
+});
+
+Deno.test("KvU64 unbox with valueOf", () => {
+ const a = new Deno.KvU64(1n);
+ assertEquals(a.valueOf(), 1n);
+});
+
+Deno.test("KvU64 auto-unbox", () => {
+ const a = new Deno.KvU64(1n);
+ assertEquals(a as unknown as bigint + 1n, 2n);
+});
+
+Deno.test("KvU64 toString", () => {
+ const a = new Deno.KvU64(1n);
+ assertEquals(a.toString(), "1");
+});
+
+Deno.test("KvU64 inspect", () => {
+ const a = new Deno.KvU64(1n);
+ assertEquals(Deno.inspect(a), "[Deno.KvU64: 1n]");
+});
+
+async function collect<T>(
+ iter: Deno.KvListIterator<T>,
+): Promise<Deno.KvEntry<T>[]> {
+ const entries: Deno.KvEntry<T>[] = [];
+ for await (const entry of iter) {
+ entries.push(entry);
+ }
+ return entries;
+}
+
+async function setupData(db: Deno.Kv): Promise<string> {
+ const res = await db.atomic()
+ .set(["a"], -1)
+ .set(["a", "a"], 0)
+ .set(["a", "b"], 1)
+ .set(["a", "c"], 2)
+ .set(["a", "d"], 3)
+ .set(["a", "e"], 4)
+ .set(["b"], 99)
+ .set(["b", "a"], 100)
+ .commit();
+ assert(res.ok);
+ return res.versionstamp;
+}
+
+dbTest("get many", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await db.getMany([["b", "a"], ["a"], ["c"]]);
+ assertEquals(entries, [
+ { key: ["b", "a"], value: 100, versionstamp },
+ { key: ["a"], value: -1, versionstamp },
+ { key: ["c"], value: null, versionstamp: null },
+ ]);
+});
+
+dbTest("list prefix", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(db.list({ prefix: ["a"] }));
+ assertEquals(entries, [
+ { key: ["a", "a"], value: 0, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "e"], value: 4, versionstamp },
+ ]);
+});
+
+dbTest("list prefix empty", async (db) => {
+ await setupData(db);
+ const entries = await collect(db.list({ prefix: ["c"] }));
+ assertEquals(entries.length, 0);
+
+ const entries2 = await collect(db.list({ prefix: ["a", "f"] }));
+ assertEquals(entries2.length, 0);
+});
+
+dbTest("list prefix with start", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(db.list({ prefix: ["a"], start: ["a", "c"] }));
+ assertEquals(entries, [
+ { key: ["a", "c"], value: 2, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "e"], value: 4, versionstamp },
+ ]);
+});
+
+dbTest("list prefix with start empty", async (db) => {
+ await setupData(db);
+ const entries = await collect(db.list({ prefix: ["a"], start: ["a", "f"] }));
+ assertEquals(entries.length, 0);
+});
+
+dbTest("list prefix with start equal to prefix", async (db) => {
+ await setupData(db);
+ await assertRejects(
+ async () => await collect(db.list({ prefix: ["a"], start: ["a"] })),
+ TypeError,
+ "start key is not in the keyspace defined by prefix",
+ );
+});
+
+dbTest("list prefix with start out of bounds", async (db) => {
+ await setupData(db);
+ await assertRejects(
+ async () => await collect(db.list({ prefix: ["b"], start: ["a"] })),
+ TypeError,
+ "start key is not in the keyspace defined by prefix",
+ );
+});
+
+dbTest("list prefix with end", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(db.list({ prefix: ["a"], end: ["a", "c"] }));
+ assertEquals(entries, [
+ { key: ["a", "a"], value: 0, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ ]);
+});
+
+dbTest("list prefix with end empty", async (db) => {
+ await setupData(db);
+ const entries = await collect(db.list({ prefix: ["a"], end: ["a", "a"] }));
+ assertEquals(entries.length, 0);
+});
+
+dbTest("list prefix with end equal to prefix", async (db) => {
+ await setupData(db);
+ await assertRejects(
+ async () => await collect(db.list({ prefix: ["a"], end: ["a"] })),
+ TypeError,
+ "end key is not in the keyspace defined by prefix",
+ );
+});
+
+dbTest("list prefix with end out of bounds", async (db) => {
+ await setupData(db);
+ await assertRejects(
+ async () => await collect(db.list({ prefix: ["a"], end: ["b"] })),
+ TypeError,
+ "end key is not in the keyspace defined by prefix",
+ );
+});
+
+dbTest("list prefix with empty prefix", async (db) => {
+ const res = await db.set(["a"], 1);
+ const entries = await collect(db.list({ prefix: [] }));
+ assertEquals(entries, [
+ { key: ["a"], value: 1, versionstamp: res.versionstamp },
+ ]);
+});
+
+dbTest("list prefix reverse", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(db.list({ prefix: ["a"] }, { reverse: true }));
+ assertEquals(entries, [
+ { key: ["a", "e"], value: 4, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "a"], value: 0, versionstamp },
+ ]);
+});
+
+dbTest("list prefix reverse with start", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(
+ db.list({ prefix: ["a"], start: ["a", "c"] }, { reverse: true }),
+ );
+ assertEquals(entries, [
+ { key: ["a", "e"], value: 4, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ ]);
+});
+
+dbTest("list prefix reverse with start empty", async (db) => {
+ await setupData(db);
+ const entries = await collect(
+ db.list({ prefix: ["a"], start: ["a", "f"] }, { reverse: true }),
+ );
+ assertEquals(entries.length, 0);
+});
+
+dbTest("list prefix reverse with end", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(
+ db.list({ prefix: ["a"], end: ["a", "c"] }, { reverse: true }),
+ );
+ assertEquals(entries, [
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "a"], value: 0, versionstamp },
+ ]);
+});
+
+dbTest("list prefix reverse with end empty", async (db) => {
+ await setupData(db);
+ const entries = await collect(
+ db.list({ prefix: ["a"], end: ["a", "a"] }, { reverse: true }),
+ );
+ assertEquals(entries.length, 0);
+});
+
+dbTest("list prefix limit", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(db.list({ prefix: ["a"] }, { limit: 2 }));
+ assertEquals(entries, [
+ { key: ["a", "a"], value: 0, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ ]);
+});
+
+dbTest("list prefix limit reverse", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(
+ db.list({ prefix: ["a"] }, { limit: 2, reverse: true }),
+ );
+ assertEquals(entries, [
+ { key: ["a", "e"], value: 4, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ ]);
+});
+
+dbTest("list prefix with small batch size", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(db.list({ prefix: ["a"] }, { batchSize: 2 }));
+ assertEquals(entries, [
+ { key: ["a", "a"], value: 0, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "e"], value: 4, versionstamp },
+ ]);
+});
+
+dbTest("list prefix with small batch size reverse", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(
+ db.list({ prefix: ["a"] }, { batchSize: 2, reverse: true }),
+ );
+ assertEquals(entries, [
+ { key: ["a", "e"], value: 4, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "a"], value: 0, versionstamp },
+ ]);
+});
+
+dbTest("list prefix with small batch size and limit", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(
+ db.list({ prefix: ["a"] }, { batchSize: 2, limit: 3 }),
+ );
+ assertEquals(entries, [
+ { key: ["a", "a"], value: 0, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ ]);
+});
+
+dbTest("list prefix with small batch size and limit reverse", async (db) => {
+ const versionstamp = await setupData(db);
+ const entries = await collect(
+ db.list({ prefix: ["a"] }, { batchSize: 2, limit: 3, reverse: true }),
+ );
+ assertEquals(entries, [
+ { key: ["a", "e"], value: 4, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ ]);
+});
+
+dbTest("list prefix with manual cursor", async (db) => {
+ const versionstamp = await setupData(db);
+ const iterator = db.list({ prefix: ["a"] }, { limit: 2 });
+ const values = await collect(iterator);
+ assertEquals(values, [
+ { key: ["a", "a"], value: 0, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ ]);
+
+ const cursor = iterator.cursor;
+ assertEquals(cursor, "AmIA");
+
+ const iterator2 = db.list({ prefix: ["a"] }, { cursor });
+ const values2 = await collect(iterator2);
+ assertEquals(values2, [
+ { key: ["a", "c"], value: 2, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "e"], value: 4, versionstamp },
+ ]);
+});
+
+dbTest("list prefix with manual cursor reverse", async (db) => {
+ const versionstamp = await setupData(db);
+
+ const iterator = db.list({ prefix: ["a"] }, { limit: 2, reverse: true });
+ const values = await collect(iterator);
+ assertEquals(values, [
+ { key: ["a", "e"], value: 4, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ ]);
+
+ const cursor = iterator.cursor;
+ assertEquals(cursor, "AmQA");
+
+ const iterator2 = db.list({ prefix: ["a"] }, { cursor, reverse: true });
+ const values2 = await collect(iterator2);
+ assertEquals(values2, [
+ { key: ["a", "c"], value: 2, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "a"], value: 0, versionstamp },
+ ]);
+});
+
+dbTest("list range", async (db) => {
+ const versionstamp = await setupData(db);
+
+ const entries = await collect(
+ db.list({ start: ["a", "a"], end: ["a", "z"] }),
+ );
+ assertEquals(entries, [
+ { key: ["a", "a"], value: 0, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "e"], value: 4, versionstamp },
+ ]);
+});
+
+dbTest("list range reverse", async (db) => {
+ const versionstamp = await setupData(db);
+
+ const entries = await collect(
+ db.list({ start: ["a", "a"], end: ["a", "z"] }, { reverse: true }),
+ );
+ assertEquals(entries, [
+ { key: ["a", "e"], value: 4, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "a"], value: 0, versionstamp },
+ ]);
+});
+
+dbTest("list range with limit", async (db) => {
+ const versionstamp = await setupData(db);
+
+ const entries = await collect(
+ db.list({ start: ["a", "a"], end: ["a", "z"] }, { limit: 3 }),
+ );
+ assertEquals(entries, [
+ { key: ["a", "a"], value: 0, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ ]);
+});
+
+dbTest("list range with limit reverse", async (db) => {
+ const versionstamp = await setupData(db);
+
+ const entries = await collect(
+ db.list({ start: ["a", "a"], end: ["a", "z"] }, {
+ limit: 3,
+ reverse: true,
+ }),
+ );
+ assertEquals(entries, [
+ { key: ["a", "e"], value: 4, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ ]);
+});
+
+dbTest("list range nesting", async (db) => {
+ const versionstamp = await setupData(db);
+
+ const entries = await collect(db.list({ start: ["a"], end: ["a", "d"] }));
+ assertEquals(entries, [
+ { key: ["a"], value: -1, versionstamp },
+ { key: ["a", "a"], value: 0, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ ]);
+});
+
+dbTest("list range short", async (db) => {
+ const versionstamp = await setupData(db);
+
+ const entries = await collect(
+ db.list({ start: ["a", "b"], end: ["a", "d"] }),
+ );
+ assertEquals(entries, [
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ ]);
+});
+
+dbTest("list range with manual cursor", async (db) => {
+ const versionstamp = await setupData(db);
+
+ const iterator = db.list({ start: ["a", "b"], end: ["a", "z"] }, {
+ limit: 2,
+ });
+ const entries = await collect(iterator);
+ assertEquals(entries, [
+ { key: ["a", "b"], value: 1, versionstamp },
+ { key: ["a", "c"], value: 2, versionstamp },
+ ]);
+
+ const cursor = iterator.cursor;
+ const iterator2 = db.list({ start: ["a", "b"], end: ["a", "z"] }, {
+ cursor,
+ });
+ const entries2 = await collect(iterator2);
+ assertEquals(entries2, [
+ { key: ["a", "d"], value: 3, versionstamp },
+ { key: ["a", "e"], value: 4, versionstamp },
+ ]);
+});
+
+dbTest("list range with manual cursor reverse", async (db) => {
+ const versionstamp = await setupData(db);
+
+ const iterator = db.list({ start: ["a", "b"], end: ["a", "z"] }, {
+ limit: 2,
+ reverse: true,
+ });
+ const entries = await collect(iterator);
+ assertEquals(entries, [
+ { key: ["a", "e"], value: 4, versionstamp },
+ { key: ["a", "d"], value: 3, versionstamp },
+ ]);
+
+ const cursor = iterator.cursor;
+ const iterator2 = db.list({ start: ["a", "b"], end: ["a", "z"] }, {
+ cursor,
+ reverse: true,
+ });
+ const entries2 = await collect(iterator2);
+ assertEquals(entries2, [
+ { key: ["a", "c"], value: 2, versionstamp },
+ { key: ["a", "b"], value: 1, versionstamp },
+ ]);
+});
+
+dbTest("list range with start greater than end", async (db) => {
+ await setupData(db);
+ await assertRejects(
+ async () => await collect(db.list({ start: ["b"], end: ["a"] })),
+ TypeError,
+ "start key is greater than end key",
+ );
+});
+
+dbTest("list range with start equal to end", async (db) => {
+ await setupData(db);
+ const entries = await collect(db.list({ start: ["a"], end: ["a"] }));
+ assertEquals(entries.length, 0);
+});
+
+dbTest("list invalid selector", async (db) => {
+ await setupData(db);
+
+ await assertRejects(async () => {
+ await collect(
+ db.list({ prefix: ["a"], start: ["a", "b"], end: ["a", "c"] }),
+ );
+ }, TypeError);
+
+ await assertRejects(async () => {
+ await collect(
+ // @ts-expect-error missing end
+ db.list({ start: ["a", "b"] }),
+ );
+ }, TypeError);
+
+ await assertRejects(async () => {
+ await collect(
+ // @ts-expect-error missing start
+ db.list({ end: ["a", "b"] }),
+ );
+ }, TypeError);
+});
+
+dbTest("invalid versionstamp in atomic check rejects", async (db) => {
+ await assertRejects(async () => {
+ await db.atomic().check({ key: ["a"], versionstamp: "" }).commit();
+ }, TypeError);
+
+ await assertRejects(async () => {
+ await db.atomic().check({ key: ["a"], versionstamp: "xx".repeat(10) })
+ .commit();
+ }, TypeError);
+
+ await assertRejects(async () => {
+ await db.atomic().check({ key: ["a"], versionstamp: "aa".repeat(11) })
+ .commit();
+ }, TypeError);
+});
+
+dbTest("invalid mutation type rejects", async (db) => {
+ await assertRejects(async () => {
+ await db.atomic()
+ // @ts-expect-error invalid type + value combo
+ .mutate({ key: ["a"], type: "set" })
+ .commit();
+ }, TypeError);
+
+ await assertRejects(async () => {
+ await db.atomic()
+ // @ts-expect-error invalid type + value combo
+ .mutate({ key: ["a"], type: "delete", value: "123" })
+ .commit();
+ }, TypeError);
+
+ await assertRejects(async () => {
+ await db.atomic()
+ // @ts-expect-error invalid type
+ .mutate({ key: ["a"], type: "foobar" })
+ .commit();
+ }, TypeError);
+
+ await assertRejects(async () => {
+ await db.atomic()
+ // @ts-expect-error invalid type
+ .mutate({ key: ["a"], type: "foobar", value: "123" })
+ .commit();
+ }, TypeError);
+});
+
+dbTest("key ordering", async (db) => {
+ await db.atomic()
+ .set([new Uint8Array(0x1)], 0)
+ .set(["a"], 0)
+ .set([1n], 0)
+ .set([3.14], 0)
+ .set([false], 0)
+ .set([true], 0)
+ .commit();
+
+ assertEquals((await collect(db.list({ prefix: [] }))).map((x) => x.key), [
+ [new Uint8Array(0x1)],
+ ["a"],
+ [1n],
+ [3.14],
+ [false],
+ [true],
+ ]);
+});
+
+dbTest("key size limit", async (db) => {
+ // 1 byte prefix + 1 byte suffix + 2045 bytes key
+ const lastValidKey = new Uint8Array(2046).fill(1);
+ const firstInvalidKey = new Uint8Array(2047).fill(1);
+
+ const res = await db.set([lastValidKey], 1);
+
+ assertEquals(await db.get([lastValidKey]), {
+ key: [lastValidKey],
+ value: 1,
+ versionstamp: res.versionstamp,
+ });
+
+ await assertRejects(
+ async () => await db.set([firstInvalidKey], 1),
+ TypeError,
+ "key too large for write (max 2048 bytes)",
+ );
+
+ await assertRejects(
+ async () => await db.get([firstInvalidKey]),
+ TypeError,
+ "key too large for read (max 2049 bytes)",
+ );
+});
+
+dbTest("value size limit", async (db) => {
+ const lastValidValue = new Uint8Array(65536);
+ const firstInvalidValue = new Uint8Array(65537);
+
+ const res = await db.set(["a"], lastValidValue);
+ assertEquals(await db.get(["a"]), {
+ key: ["a"],
+ value: lastValidValue,
+ versionstamp: res.versionstamp,
+ });
+
+ await assertRejects(
+ async () => await db.set(["b"], firstInvalidValue),
+ TypeError,
+ "value too large (max 65536 bytes)",
+ );
+});
+
+dbTest("operation size limit", async (db) => {
+ const lastValidKeys: Deno.KvKey[] = new Array(10).fill(0).map((
+ _,
+ i,
+ ) => ["a", i]);
+ const firstInvalidKeys: Deno.KvKey[] = new Array(11).fill(0).map((
+ _,
+ i,
+ ) => ["a", i]);
+ const invalidCheckKeys: Deno.KvKey[] = new Array(101).fill(0).map((
+ _,
+ i,
+ ) => ["a", i]);
+
+ const res = await db.getMany(lastValidKeys);
+ assertEquals(res.length, 10);
+
+ await assertRejects(
+ async () => await db.getMany(firstInvalidKeys),
+ TypeError,
+ "too many ranges (max 10)",
+ );
+
+ const res2 = await collect(db.list({ prefix: ["a"] }, { batchSize: 1000 }));
+ assertEquals(res2.length, 0);
+
+ await assertRejects(
+ async () => await collect(db.list({ prefix: ["a"] }, { batchSize: 1001 })),
+ TypeError,
+ "too many entries (max 1000)",
+ );
+
+ // when batchSize is not specified, limit is used but is clamped to 500
+ assertEquals(
+ (await collect(db.list({ prefix: ["a"] }, { limit: 1001 }))).length,
+ 0,
+ );
+
+ const res3 = await db.atomic()
+ .check(...lastValidKeys.map((key) => ({
+ key,
+ versionstamp: null,
+ })))
+ .mutate(...lastValidKeys.map((key) => ({
+ key,
+ type: "set",
+ value: 1,
+ } satisfies Deno.KvMutation)))
+ .commit();
+ assert(res3);
+
+ await assertRejects(
+ async () => {
+ await db.atomic()
+ .check(...invalidCheckKeys.map((key) => ({
+ key,
+ versionstamp: null,
+ })))
+ .mutate(...lastValidKeys.map((key) => ({
+ key,
+ type: "set",
+ value: 1,
+ } satisfies Deno.KvMutation)))
+ .commit();
+ },
+ TypeError,
+ "too many checks (max 100)",
+ );
+
+ const validMutateKeys: Deno.KvKey[] = new Array(1000).fill(0).map((
+ _,
+ i,
+ ) => ["a", i]);
+ const invalidMutateKeys: Deno.KvKey[] = new Array(1001).fill(0).map((
+ _,
+ i,
+ ) => ["a", i]);
+
+ const res4 = await db.atomic()
+ .check(...lastValidKeys.map((key) => ({
+ key,
+ versionstamp: null,
+ })))
+ .mutate(...validMutateKeys.map((key) => ({
+ key,
+ type: "set",
+ value: 1,
+ } satisfies Deno.KvMutation)))
+ .commit();
+ assert(res4);
+
+ await assertRejects(
+ async () => {
+ await db.atomic()
+ .check(...lastValidKeys.map((key) => ({
+ key,
+ versionstamp: null,
+ })))
+ .mutate(...invalidMutateKeys.map((key) => ({
+ key,
+ type: "set",
+ value: 1,
+ } satisfies Deno.KvMutation)))
+ .commit();
+ },
+ TypeError,
+ "too many mutations (max 1000)",
+ );
+});
+
+dbTest("total mutation size limit", async (db) => {
+ const keys: Deno.KvKey[] = new Array(1000).fill(0).map((
+ _,
+ i,
+ ) => ["a", i]);
+
+ const atomic = db.atomic();
+ for (const key of keys) {
+ atomic.set(key, "foo");
+ }
+ const res = await atomic.commit();
+ assert(res);
+
+ // Use bigger values to trigger "total mutation size too large" error
+ await assertRejects(
+ async () => {
+ const value = new Array(3000).fill("a").join("");
+ const atomic = db.atomic();
+ for (const key of keys) {
+ atomic.set(key, value);
+ }
+ await atomic.commit();
+ },
+ TypeError,
+ "total mutation size too large (max 819200 bytes)",
+ );
+});
+
+dbTest("total key size limit", async (db) => {
+ const longString = new Array(1100).fill("a").join("");
+ const keys: Deno.KvKey[] = new Array(80).fill(0).map(() => [longString]);
+
+ const atomic = db.atomic();
+ for (const key of keys) {
+ atomic.set(key, "foo");
+ }
+ await assertRejects(
+ () => atomic.commit(),
+ TypeError,
+ "total key size too large (max 81920 bytes)",
+ );
+});
+
+dbTest("keys must be arrays", async (db) => {
+ await assertRejects(
+ // @ts-expect-error invalid type
+ async () => await db.get("a"),
+ TypeError,
+ );
+
+ await assertRejects(
+ // @ts-expect-error invalid type
+ async () => await db.getMany(["a"]),
+ TypeError,
+ );
+
+ await assertRejects(
+ // @ts-expect-error invalid type
+ async () => await db.set("a", 1),
+ TypeError,
+ );
+
+ await assertRejects(
+ // @ts-expect-error invalid type
+ async () => await db.delete("a"),
+ TypeError,
+ );
+
+ await assertRejects(
+ async () =>
+ await db.atomic()
+ // @ts-expect-error invalid type
+ .mutate({ key: "a", type: "set", value: 1 } satisfies Deno.KvMutation)
+ .commit(),
+ TypeError,
+ );
+
+ await assertRejects(
+ async () =>
+ await db.atomic()
+ // @ts-expect-error invalid type
+ .check({ key: "a", versionstamp: null })
+ .set(["a"], 1)
+ .commit(),
+ TypeError,
+ );
+});
+
+Deno.test("Deno.Kv constructor throws", () => {
+ assertThrows(() => {
+ new Deno.Kv();
+ });
+});
+
+// This function is never called, it is just used to check that all the types
+// are behaving as expected.
+async function _typeCheckingTests() {
+ const kv = new Deno.Kv();
+
+ const a = await kv.get(["a"]);
+ assertType<IsExact<typeof a, Deno.KvEntryMaybe<unknown>>>(true);
+
+ const b = await kv.get<string>(["b"]);
+ assertType<IsExact<typeof b, Deno.KvEntryMaybe<string>>>(true);
+
+ const c = await kv.getMany([["a"], ["b"]]);
+ assertType<
+ IsExact<typeof c, [Deno.KvEntryMaybe<unknown>, Deno.KvEntryMaybe<unknown>]>
+ >(true);
+
+ const d = await kv.getMany([["a"], ["b"]] as const);
+ assertType<
+ IsExact<typeof d, [Deno.KvEntryMaybe<unknown>, Deno.KvEntryMaybe<unknown>]>
+ >(true);
+
+ const e = await kv.getMany<[string, number]>([["a"], ["b"]]);
+ assertType<
+ IsExact<typeof e, [Deno.KvEntryMaybe<string>, Deno.KvEntryMaybe<number>]>
+ >(true);
+
+ const keys: Deno.KvKey[] = [["a"], ["b"]];
+ const f = await kv.getMany(keys);
+ assertType<IsExact<typeof f, Deno.KvEntryMaybe<unknown>[]>>(true);
+
+ const g = kv.list({ prefix: ["a"] });
+ assertType<IsExact<typeof g, Deno.KvListIterator<unknown>>>(true);
+ const h = await g.next();
+ assert(!h.done);
+ assertType<IsExact<typeof h.value, Deno.KvEntry<unknown>>>(true);
+
+ const i = kv.list<string>({ prefix: ["a"] });
+ assertType<IsExact<typeof i, Deno.KvListIterator<string>>>(true);
+ const j = await i.next();
+ assert(!j.done);
+ assertType<IsExact<typeof j.value, Deno.KvEntry<string>>>(true);
+}
+
+queueTest("basic listenQueue and enqueue", async (db) => {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let dequeuedMessage: unknown = null;
+ const listener = db.listenQueue((msg) => {
+ dequeuedMessage = msg;
+ resolve();
+ });
+ try {
+ const res = await db.enqueue("test");
+ assert(res.ok);
+ assertNotEquals(res.versionstamp, null);
+ await promise;
+ assertEquals(dequeuedMessage, "test");
+ } finally {
+ db.close();
+ await listener;
+ }
+});
+
+for (const { name, value } of VALUE_CASES) {
+ queueTest(`listenQueue and enqueue ${name}`, async (db) => {
+ const numEnqueues = 10;
+ let count = 0;
+ const deferreds: ReturnType<typeof Promise.withResolvers<unknown>>[] = [];
+ const listeners: Promise<void>[] = [];
+ listeners.push(db.listenQueue((msg: unknown) => {
+ deferreds[count++].resolve(msg);
+ }));
+ try {
+ for (let i = 0; i < numEnqueues; i++) {
+ deferreds.push(Promise.withResolvers<unknown>());
+ await db.enqueue(value);
+ }
+ const dequeuedMessages = await Promise.all(
+ deferreds.map(({ promise }) => promise),
+ );
+ for (let i = 0; i < numEnqueues; i++) {
+ assertEquals(dequeuedMessages[i], value);
+ }
+ } finally {
+ db.close();
+ for (const listener of listeners) {
+ await listener;
+ }
+ }
+ });
+}
+
+queueTest("queue mixed types", async (db) => {
+ let deferred: ReturnType<typeof Promise.withResolvers<void>>;
+ let dequeuedMessage: unknown = null;
+ const listener = db.listenQueue((msg: unknown) => {
+ dequeuedMessage = msg;
+ deferred.resolve();
+ });
+ try {
+ for (const item of VALUE_CASES) {
+ deferred = Promise.withResolvers<void>();
+ await db.enqueue(item.value);
+ await deferred.promise;
+ assertEquals(dequeuedMessage, item.value);
+ }
+ } finally {
+ db.close();
+ await listener;
+ }
+});
+
+queueTest("queue delay", async (db) => {
+ let dequeueTime: number | undefined;
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let dequeuedMessage: unknown = null;
+ const listener = db.listenQueue((msg) => {
+ dequeueTime = Date.now();
+ dequeuedMessage = msg;
+ resolve();
+ });
+ try {
+ const enqueueTime = Date.now();
+ await db.enqueue("test", { delay: 1000 });
+ await promise;
+ assertEquals(dequeuedMessage, "test");
+ assert(dequeueTime !== undefined);
+ assert(dequeueTime - enqueueTime >= 1000);
+ } finally {
+ db.close();
+ await listener;
+ }
+});
+
+queueTest("queue delay with atomic", async (db) => {
+ let dequeueTime: number | undefined;
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let dequeuedMessage: unknown = null;
+ const listener = db.listenQueue((msg) => {
+ dequeueTime = Date.now();
+ dequeuedMessage = msg;
+ resolve();
+ });
+ try {
+ const enqueueTime = Date.now();
+ const res = await db.atomic()
+ .enqueue("test", { delay: 1000 })
+ .commit();
+ assert(res.ok);
+
+ await promise;
+ assertEquals(dequeuedMessage, "test");
+ assert(dequeueTime !== undefined);
+ assert(dequeueTime - enqueueTime >= 1000);
+ } finally {
+ db.close();
+ await listener;
+ }
+});
+
+queueTest("queue delay and now", async (db) => {
+ let count = 0;
+ let dequeueTime: number | undefined;
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let dequeuedMessage: unknown = null;
+ const listener = db.listenQueue((msg) => {
+ count += 1;
+ if (count == 2) {
+ dequeueTime = Date.now();
+ dequeuedMessage = msg;
+ resolve();
+ }
+ });
+ try {
+ const enqueueTime = Date.now();
+ await db.enqueue("test-1000", { delay: 1000 });
+ await db.enqueue("test");
+ await promise;
+ assertEquals(dequeuedMessage, "test-1000");
+ assert(dequeueTime !== undefined);
+ assert(dequeueTime - enqueueTime >= 1000);
+ } finally {
+ db.close();
+ await listener;
+ }
+});
+
+dbTest("queue negative delay", async (db) => {
+ await assertRejects(async () => {
+ await db.enqueue("test", { delay: -100 });
+ }, TypeError);
+});
+
+dbTest("queue nan delay", async (db) => {
+ await assertRejects(async () => {
+ await db.enqueue("test", { delay: Number.NaN });
+ }, TypeError);
+});
+
+dbTest("queue large delay", async (db) => {
+ await db.enqueue("test", { delay: 30 * 24 * 60 * 60 * 1000 });
+ await assertRejects(async () => {
+ await db.enqueue("test", { delay: 30 * 24 * 60 * 60 * 1000 + 1 });
+ }, TypeError);
+});
+
+queueTest("listenQueue with async callback", async (db) => {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let dequeuedMessage: unknown = null;
+ const listener = db.listenQueue(async (msg) => {
+ dequeuedMessage = msg;
+ await sleep(100);
+ resolve();
+ });
+ try {
+ await db.enqueue("test");
+ await promise;
+ assertEquals(dequeuedMessage, "test");
+ } finally {
+ db.close();
+ await listener;
+ }
+});
+
+queueTest("queue retries", async (db) => {
+ let count = 0;
+ const listener = db.listenQueue(async (_msg) => {
+ count += 1;
+ await sleep(10);
+ throw new TypeError("dequeue error");
+ });
+ try {
+ await db.enqueue("test");
+ await sleep(10000);
+ } finally {
+ db.close();
+ await listener;
+ }
+
+ // There should have been 1 attempt + 3 retries in the 10 seconds
+ assertEquals(4, count);
+});
+
+queueTest("queue retries with backoffSchedule", async (db) => {
+ let count = 0;
+ const listener = db.listenQueue((_msg) => {
+ count += 1;
+ throw new TypeError("dequeue error");
+ });
+ try {
+ await db.enqueue("test", { backoffSchedule: [1] });
+ await sleep(2000);
+ } finally {
+ db.close();
+ await listener;
+ }
+
+ // There should have been 1 attempt + 1 retry
+ assertEquals(2, count);
+});
+
+queueTest("multiple listenQueues", async (db) => {
+ const numListens = 10;
+ let count = 0;
+ const deferreds: ReturnType<typeof Promise.withResolvers<void>>[] = [];
+ const dequeuedMessages: unknown[] = [];
+ const listeners: Promise<void>[] = [];
+ for (let i = 0; i < numListens; i++) {
+ listeners.push(db.listenQueue((msg) => {
+ dequeuedMessages.push(msg);
+ deferreds[count++].resolve();
+ }));
+ }
+ try {
+ for (let i = 0; i < numListens; i++) {
+ deferreds.push(Promise.withResolvers<void>());
+ await db.enqueue("msg_" + i);
+ await deferreds[i].promise;
+ const msg = dequeuedMessages[i];
+ assertEquals("msg_" + i, msg);
+ }
+ } finally {
+ db.close();
+ for (let i = 0; i < numListens; i++) {
+ await listeners[i];
+ }
+ }
+});
+
+queueTest("enqueue with atomic", async (db) => {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let dequeuedMessage: unknown = null;
+ const listener = db.listenQueue((msg) => {
+ dequeuedMessage = msg;
+ resolve();
+ });
+
+ try {
+ await db.set(["t"], "1");
+
+ let currentValue = await db.get(["t"]);
+ assertEquals("1", currentValue.value);
+
+ const res = await db.atomic()
+ .check(currentValue)
+ .set(currentValue.key, "2")
+ .enqueue("test")
+ .commit();
+ assert(res.ok);
+
+ await promise;
+ assertEquals("test", dequeuedMessage);
+
+ currentValue = await db.get(["t"]);
+ assertEquals("2", currentValue.value);
+ } finally {
+ db.close();
+ await listener;
+ }
+});
+
+queueTest("enqueue with atomic nonce", async (db) => {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let dequeuedMessage: unknown = null;
+
+ const nonce = crypto.randomUUID();
+
+ const listener = db.listenQueue(async (val) => {
+ const message = val as { msg: string; nonce: string };
+ const nonce = message.nonce;
+ const nonceValue = await db.get(["nonces", nonce]);
+ if (nonceValue.versionstamp === null) {
+ dequeuedMessage = message.msg;
+ resolve();
+ return;
+ }
+
+ assertNotEquals(nonceValue.versionstamp, null);
+ const res = await db.atomic()
+ .check(nonceValue)
+ .delete(["nonces", nonce])
+ .set(["a", "b"], message.msg)
+ .commit();
+ if (res.ok) {
+ // Simulate an error so that the message has to be redelivered
+ throw new Error("injected error");
+ }
+ });
+
+ try {
+ const res = await db.atomic()
+ .check({ key: ["nonces", nonce], versionstamp: null })
+ .set(["nonces", nonce], true)
+ .enqueue({ msg: "test", nonce })
+ .commit();
+ assert(res.ok);
+
+ await promise;
+ assertEquals("test", dequeuedMessage);
+
+ const currentValue = await db.get(["a", "b"]);
+ assertEquals("test", currentValue.value);
+
+ const nonceValue = await db.get(["nonces", nonce]);
+ assertEquals(nonceValue.versionstamp, null);
+ } finally {
+ db.close();
+ await listener;
+ }
+});
+
+Deno.test({
+ name: "queue persistence with inflight messages",
+ sanitizeOps: false,
+ sanitizeResources: false,
+ async fn() {
+ const filename = await Deno.makeTempFile({ prefix: "queue_db" });
+ try {
+ let db: Deno.Kv = await Deno.openKv(filename);
+
+ let count = 0;
+ let deferred = Promise.withResolvers<void>();
+
+ // Register long-running handler.
+ let listener = db.listenQueue(async (_msg) => {
+ count += 1;
+ if (count == 3) {
+ deferred.resolve();
+ }
+ await new Promise(() => {});
+ });
+
+ // Enqueue 3 messages.
+ await db.enqueue("msg0");
+ await db.enqueue("msg1");
+ await db.enqueue("msg2");
+ await deferred.promise;
+
+ // Close the database and wait for the listener to finish.
+ db.close();
+ await listener;
+
+ // Wait at least MESSAGE_DEADLINE_TIMEOUT before reopening the database.
+ // This ensures that inflight messages are requeued immediately after
+ // the database is reopened.
+ // https://github.com/denoland/denokv/blob/efb98a1357d37291a225ed5cf1fc4ecc7c737fab/sqlite/backend.rs#L120
+ await sleep(6000);
+
+ // Now reopen the database.
+ db = await Deno.openKv(filename);
+
+ count = 0;
+ deferred = Promise.withResolvers<void>();
+
+ // Register a handler that will complete quickly.
+ listener = db.listenQueue((_msg) => {
+ count += 1;
+ if (count == 3) {
+ deferred.resolve();
+ }
+ });
+
+ // Wait for the handlers to finish.
+ await deferred.promise;
+ assertEquals(3, count);
+ db.close();
+ await listener;
+ } finally {
+ try {
+ await Deno.remove(filename);
+ } catch {
+ // pass
+ }
+ }
+ },
+});
+
+Deno.test({
+ name: "queue persistence with delay messages",
+ async fn() {
+ const filename = await Deno.makeTempFile({ prefix: "queue_db" });
+ try {
+ await Deno.remove(filename);
+ } catch {
+ // pass
+ }
+ try {
+ let db: Deno.Kv = await Deno.openKv(filename);
+
+ let count = 0;
+ let deferred = Promise.withResolvers<void>();
+
+ // Register long-running handler.
+ let listener = db.listenQueue((_msg) => {});
+
+ // Enqueue 3 messages into the future.
+ await db.enqueue("msg0", { delay: 10000 });
+ await db.enqueue("msg1", { delay: 10000 });
+ await db.enqueue("msg2", { delay: 10000 });
+
+ // Close the database and wait for the listener to finish.
+ db.close();
+ await listener;
+
+ // Now reopen the database.
+ db = await Deno.openKv(filename);
+
+ count = 0;
+ deferred = Promise.withResolvers<void>();
+
+ // Register a handler that will complete quickly.
+ listener = db.listenQueue((_msg) => {
+ count += 1;
+ if (count == 3) {
+ deferred.resolve();
+ }
+ });
+
+ // Wait for the handlers to finish.
+ await deferred.promise;
+ assertEquals(3, count);
+ db.close();
+ await listener;
+ } finally {
+ try {
+ await Deno.remove(filename);
+ } catch {
+ // pass
+ }
+ }
+ },
+});
+
+Deno.test({
+ name: "different kv instances for enqueue and queueListen",
+ async fn() {
+ const filename = await Deno.makeTempFile({ prefix: "queue_db" });
+ try {
+ const db0 = await Deno.openKv(filename);
+ const db1 = await Deno.openKv(filename);
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let dequeuedMessage: unknown = null;
+ const listener = db0.listenQueue((msg) => {
+ dequeuedMessage = msg;
+ resolve();
+ });
+ try {
+ const res = await db1.enqueue("test");
+ assert(res.ok);
+ assertNotEquals(res.versionstamp, null);
+ await promise;
+ assertEquals(dequeuedMessage, "test");
+ } finally {
+ db0.close();
+ await listener;
+ db1.close();
+ }
+ } finally {
+ try {
+ await Deno.remove(filename);
+ } catch {
+ // pass
+ }
+ }
+ },
+});
+
+Deno.test({
+ name: "queue graceful close",
+ async fn() {
+ const db: Deno.Kv = await Deno.openKv(":memory:");
+ const listener = db.listenQueue((_msg) => {});
+ db.close();
+ await listener;
+ },
+});
+
+dbTest("invalid backoffSchedule", async (db) => {
+ await assertRejects(
+ async () => {
+ await db.enqueue("foo", { backoffSchedule: [1, 1, 1, 1, 1, 1] });
+ },
+ TypeError,
+ "invalid backoffSchedule",
+ );
+ await assertRejects(
+ async () => {
+ await db.enqueue("foo", { backoffSchedule: [3600001] });
+ },
+ TypeError,
+ "invalid backoffSchedule",
+ );
+});
+
+dbTest("atomic operation is exposed", (db) => {
+ assert(Deno.AtomicOperation);
+ const ao = db.atomic();
+ assert(ao instanceof Deno.AtomicOperation);
+});
+
+Deno.test({
+ name: "racy open",
+ async fn() {
+ for (let i = 0; i < 100; i++) {
+ const filename = await Deno.makeTempFile({ prefix: "racy_open_db" });
+ try {
+ const [db1, db2, db3] = await Promise.all([
+ Deno.openKv(filename),
+ Deno.openKv(filename),
+ Deno.openKv(filename),
+ ]);
+ db1.close();
+ db2.close();
+ db3.close();
+ } finally {
+ await Deno.remove(filename);
+ }
+ }
+ },
+});
+
+Deno.test({
+ name: "racy write",
+ async fn() {
+ const filename = await Deno.makeTempFile({ prefix: "racy_write_db" });
+ const concurrency = 20;
+ const iterations = 5;
+ try {
+ const dbs = await Promise.all(
+ Array(concurrency).fill(0).map(() => Deno.openKv(filename)),
+ );
+ try {
+ for (let i = 0; i < iterations; i++) {
+ await Promise.all(
+ dbs.map((db) => db.atomic().sum(["counter"], 1n).commit()),
+ );
+ }
+ assertEquals(
+ ((await dbs[0].get(["counter"])).value as Deno.KvU64).value,
+ BigInt(concurrency * iterations),
+ );
+ } finally {
+ dbs.forEach((db) => db.close());
+ }
+ } finally {
+ await Deno.remove(filename);
+ }
+ },
+});
+
+Deno.test({
+ name: "kv expiration",
+ async fn() {
+ const filename = await Deno.makeTempFile({ prefix: "kv_expiration_db" });
+ try {
+ await Deno.remove(filename);
+ } catch {
+ // pass
+ }
+ let db: Deno.Kv | null = null;
+
+ try {
+ db = await Deno.openKv(filename);
+
+ await db.set(["a"], 1, { expireIn: 1000 });
+ await db.set(["b"], 2, { expireIn: 1000 });
+ assertEquals((await db.get(["a"])).value, 1);
+ assertEquals((await db.get(["b"])).value, 2);
+
+ // Value overwrite should also reset expiration
+ await db.set(["b"], 2, { expireIn: 3600 * 1000 });
+
+ // Wait for expiration
+ await sleep(1000);
+
+ // Re-open to trigger immediate cleanup
+ db.close();
+ db = null;
+ db = await Deno.openKv(filename);
+
+ let ok = false;
+ for (let i = 0; i < 50; i++) {
+ await sleep(100);
+ if (
+ JSON.stringify(
+ (await db.getMany([["a"], ["b"]])).map((x) => x.value),
+ ) === "[null,2]"
+ ) {
+ ok = true;
+ break;
+ }
+ }
+
+ if (!ok) {
+ throw new Error("Values did not expire");
+ }
+ } finally {
+ if (db) {
+ try {
+ db.close();
+ } catch {
+ // pass
+ }
+ }
+ try {
+ await Deno.remove(filename);
+ } catch {
+ // pass
+ }
+ }
+ },
+});
+
+Deno.test({
+ name: "kv expiration with atomic",
+ async fn() {
+ const filename = await Deno.makeTempFile({ prefix: "kv_expiration_db" });
+ try {
+ await Deno.remove(filename);
+ } catch {
+ // pass
+ }
+ let db: Deno.Kv | null = null;
+
+ try {
+ db = await Deno.openKv(filename);
+
+ await db.atomic().set(["a"], 1, { expireIn: 1000 }).set(["b"], 2, {
+ expireIn: 1000,
+ }).commit();
+ assertEquals((await db.getMany([["a"], ["b"]])).map((x) => x.value), [
+ 1,
+ 2,
+ ]);
+
+ // Wait for expiration
+ await sleep(1000);
+
+ // Re-open to trigger immediate cleanup
+ db.close();
+ db = null;
+ db = await Deno.openKv(filename);
+
+ let ok = false;
+ for (let i = 0; i < 50; i++) {
+ await sleep(100);
+ if (
+ JSON.stringify(
+ (await db.getMany([["a"], ["b"]])).map((x) => x.value),
+ ) === "[null,null]"
+ ) {
+ ok = true;
+ break;
+ }
+ }
+
+ if (!ok) {
+ throw new Error("Values did not expire");
+ }
+ } finally {
+ if (db) {
+ try {
+ db.close();
+ } catch {
+ // pass
+ }
+ }
+ try {
+ await Deno.remove(filename);
+ } catch {
+ // pass
+ }
+ }
+ },
+});
+
+Deno.test({
+ name: "remote backend",
+ async fn() {
+ const db = await Deno.openKv("http://localhost:4545/kv_remote_authorize");
+ try {
+ await db.set(["some-key"], 1);
+ const entry = await db.get(["some-key"]);
+ assertEquals(entry.value, null);
+ assertEquals(entry.versionstamp, null);
+ } finally {
+ db.close();
+ }
+ },
+});
+
+Deno.test({
+ name: "remote backend invalid format",
+ async fn() {
+ const db = await Deno.openKv(
+ "http://localhost:4545/kv_remote_authorize_invalid_format",
+ );
+
+ await assertRejects(
+ async () => {
+ await db.set(["some-key"], 1);
+ },
+ Error,
+ "Failed to parse metadata: ",
+ );
+
+ db.close();
+ },
+});
+
+Deno.test({
+ name: "remote backend invalid version",
+ async fn() {
+ const db = await Deno.openKv(
+ "http://localhost:4545/kv_remote_authorize_invalid_version",
+ );
+
+ await assertRejects(
+ async () => {
+ await db.set(["some-key"], 1);
+ },
+ Error,
+ "Failed to parse metadata: unsupported metadata version: 1000",
+ );
+
+ db.close();
+ },
+});
+
+Deno.test(
+ { permissions: { read: true } },
+ async function kvExplicitResourceManagement() {
+ let kv2: Deno.Kv;
+
+ {
+ using kv = await Deno.openKv(":memory:");
+ kv2 = kv;
+
+ const res = await kv.get(["a"]);
+ assertEquals(res.versionstamp, null);
+ }
+
+ await assertRejects(() => kv2.get(["a"]), Deno.errors.BadResource);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function kvExplicitResourceManagementManualClose() {
+ using kv = await Deno.openKv(":memory:");
+ kv.close();
+ await assertRejects(() => kv.get(["a"]), Deno.errors.BadResource);
+ // calling [Symbol.dispose] after manual close is a no-op
+ },
+);
+
+dbTest("key watch", async (db) => {
+ const changeHistory: Deno.KvEntryMaybe<number>[] = [];
+ const watcher: ReadableStream<Deno.KvEntryMaybe<number>[]> = db.watch<
+ number[]
+ >([["key"]]);
+
+ const reader = watcher.getReader();
+ const expectedChanges = 2;
+
+ const work = (async () => {
+ for (let i = 0; i < expectedChanges; i++) {
+ const message = await reader.read();
+ if (message.done) {
+ throw new Error("Unexpected end of stream");
+ }
+ changeHistory.push(message.value[0]);
+ }
+
+ await reader.cancel();
+ })();
+
+ while (changeHistory.length !== 1) {
+ await sleep(100);
+ }
+ assertEquals(changeHistory[0], {
+ key: ["key"],
+ value: null,
+ versionstamp: null,
+ });
+
+ const { versionstamp } = await db.set(["key"], 1);
+ while (changeHistory.length as number !== 2) {
+ await sleep(100);
+ }
+ assertEquals(changeHistory[1], {
+ key: ["key"],
+ value: 1,
+ versionstamp,
+ });
+
+ await work;
+ await reader.cancel();
+});
+
+dbTest("set with key versionstamp suffix", async (db) => {
+ const result1 = await Array.fromAsync(db.list({ prefix: ["a"] }));
+ assertEquals(result1, []);
+
+ const setRes1 = await db.set(["a", db.commitVersionstamp()], "b");
+ assert(setRes1.ok);
+ assert(setRes1.versionstamp > ZERO_VERSIONSTAMP);
+
+ const result2 = await Array.fromAsync(db.list({ prefix: ["a"] }));
+ assertEquals(result2.length, 1);
+ assertEquals(result2[0].key[1], setRes1.versionstamp);
+ assertEquals(result2[0].value, "b");
+ assertEquals(result2[0].versionstamp, setRes1.versionstamp);
+
+ const setRes2 = await db.atomic().set(["a", db.commitVersionstamp()], "c")
+ .commit();
+ assert(setRes2.ok);
+ assert(setRes2.versionstamp > setRes1.versionstamp);
+
+ const result3 = await Array.fromAsync(db.list({ prefix: ["a"] }));
+ assertEquals(result3.length, 2);
+ assertEquals(result3[1].key[1], setRes2.versionstamp);
+ assertEquals(result3[1].value, "c");
+ assertEquals(result3[1].versionstamp, setRes2.versionstamp);
+
+ await assertRejects(
+ async () => await db.set(["a", db.commitVersionstamp(), "a"], "x"),
+ TypeError,
+ "expected string, number, bigint, ArrayBufferView, boolean",
+ );
+});
+
+Deno.test({
+ name: "watch should stop when db closed",
+ async fn() {
+ const db = await Deno.openKv(":memory:");
+
+ const watch = db.watch([["a"]]);
+ const completion = (async () => {
+ for await (const _item of watch) {
+ // pass
+ }
+ })();
+
+ setTimeout(() => {
+ db.close();
+ }, 100);
+
+ await completion;
+ },
+});
diff --git a/tests/unit/link_test.ts b/tests/unit/link_test.ts
new file mode 100644
index 000000000..6048b8add
--- /dev/null
+++ b/tests/unit/link_test.ts
@@ -0,0 +1,195 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function linkSyncSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const oldData = "Hardlink";
+ const oldName = testDir + "/oldname";
+ const newName = testDir + "/newname";
+ Deno.writeFileSync(oldName, new TextEncoder().encode(oldData));
+ // Create the hard link.
+ Deno.linkSync(oldName, newName);
+ // We should expect reading the same content.
+ const newData = Deno.readTextFileSync(newName);
+ assertEquals(oldData, newData);
+ // Writing to newname also affects oldname.
+ const newData2 = "Modified";
+ Deno.writeFileSync(newName, new TextEncoder().encode(newData2));
+ assertEquals(
+ newData2,
+ Deno.readTextFileSync(oldName),
+ );
+ // Writing to oldname also affects newname.
+ const newData3 = "ModifiedAgain";
+ Deno.writeFileSync(oldName, new TextEncoder().encode(newData3));
+ assertEquals(
+ newData3,
+ Deno.readTextFileSync(newName),
+ );
+ // Remove oldname. File still accessible through newname.
+ Deno.removeSync(oldName);
+ const newNameStat = Deno.statSync(newName);
+ assert(newNameStat.isFile);
+ assert(!newNameStat.isSymlink); // Not a symlink.
+ assertEquals(
+ newData3,
+ Deno.readTextFileSync(newName),
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function linkSyncExists() {
+ const testDir = Deno.makeTempDirSync();
+ const oldName = testDir + "/oldname";
+ const newName = testDir + "/newname";
+ Deno.writeFileSync(oldName, new TextEncoder().encode("oldName"));
+ // newname is already created.
+ Deno.writeFileSync(newName, new TextEncoder().encode("newName"));
+
+ assertThrows(
+ () => {
+ Deno.linkSync(oldName, newName);
+ },
+ Deno.errors.AlreadyExists,
+ `link '${oldName}' -> '${newName}'`,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function linkSyncNotFound() {
+ const testDir = Deno.makeTempDirSync();
+ const oldName = testDir + "/oldname";
+ const newName = testDir + "/newname";
+
+ assertThrows(
+ () => {
+ Deno.linkSync(oldName, newName);
+ },
+ Deno.errors.NotFound,
+ `link '${oldName}' -> '${newName}'`,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: false, write: true } },
+ function linkSyncReadPerm() {
+ assertThrows(() => {
+ Deno.linkSync("oldbaddir", "newbaddir");
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: false } },
+ function linkSyncWritePerm() {
+ assertThrows(() => {
+ Deno.linkSync("oldbaddir", "newbaddir");
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function linkSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const oldData = "Hardlink";
+ const oldName = testDir + "/oldname";
+ const newName = testDir + "/newname";
+ Deno.writeFileSync(oldName, new TextEncoder().encode(oldData));
+ // Create the hard link.
+ await Deno.link(oldName, newName);
+ // We should expect reading the same content.
+ const newData = Deno.readTextFileSync(newName);
+ assertEquals(oldData, newData);
+ // Writing to newname also affects oldname.
+ const newData2 = "Modified";
+ Deno.writeFileSync(newName, new TextEncoder().encode(newData2));
+ assertEquals(
+ newData2,
+ Deno.readTextFileSync(oldName),
+ );
+ // Writing to oldname also affects newname.
+ const newData3 = "ModifiedAgain";
+ Deno.writeFileSync(oldName, new TextEncoder().encode(newData3));
+ assertEquals(
+ newData3,
+ Deno.readTextFileSync(newName),
+ );
+ // Remove oldname. File still accessible through newname.
+ Deno.removeSync(oldName);
+ const newNameStat = Deno.statSync(newName);
+ assert(newNameStat.isFile);
+ assert(!newNameStat.isSymlink); // Not a symlink.
+ assertEquals(
+ newData3,
+ Deno.readTextFileSync(newName),
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function linkExists() {
+ const testDir = Deno.makeTempDirSync();
+ const oldName = testDir + "/oldname";
+ const newName = testDir + "/newname";
+ Deno.writeFileSync(oldName, new TextEncoder().encode("oldName"));
+ // newname is already created.
+ Deno.writeFileSync(newName, new TextEncoder().encode("newName"));
+
+ await assertRejects(
+ async () => {
+ await Deno.link(oldName, newName);
+ },
+ Deno.errors.AlreadyExists,
+ `link '${oldName}' -> '${newName}'`,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function linkNotFound() {
+ const testDir = Deno.makeTempDirSync();
+ const oldName = testDir + "/oldname";
+ const newName = testDir + "/newname";
+
+ await assertRejects(
+ async () => {
+ await Deno.link(oldName, newName);
+ },
+ Deno.errors.NotFound,
+ `link '${oldName}' -> '${newName}'`,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: false, write: true } },
+ async function linkReadPerm() {
+ await assertRejects(async () => {
+ await Deno.link("oldbaddir", "newbaddir");
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: false } },
+ async function linkWritePerm() {
+ await assertRejects(async () => {
+ await Deno.link("oldbaddir", "newbaddir");
+ }, Deno.errors.PermissionDenied);
+ },
+);
diff --git a/tests/unit/make_temp_test.ts b/tests/unit/make_temp_test.ts
new file mode 100644
index 000000000..cbbae8dfe
--- /dev/null
+++ b/tests/unit/make_temp_test.ts
@@ -0,0 +1,157 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test({ permissions: { write: true } }, function makeTempDirSyncSuccess() {
+ const dir1 = Deno.makeTempDirSync({ prefix: "hello", suffix: "world" });
+ const dir2 = Deno.makeTempDirSync({ prefix: "hello", suffix: "world" });
+ // Check that both dirs are different.
+ assert(dir1 !== dir2);
+ for (const dir of [dir1, dir2]) {
+ // Check that the prefix and suffix are applied.
+ const lastPart = dir.replace(/^.*[\\\/]/, "");
+ assert(lastPart.startsWith("hello"));
+ assert(lastPart.endsWith("world"));
+ }
+ // Check that the `dir` option works.
+ const dir3 = Deno.makeTempDirSync({ dir: dir1 });
+ assert(dir3.startsWith(dir1));
+ assert(/^[\\\/]/.test(dir3.slice(dir1.length)));
+ // Check that creating a temp dir inside a nonexisting directory fails.
+ assertThrows(() => {
+ Deno.makeTempDirSync({ dir: "/baddir" });
+ }, Deno.errors.NotFound);
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function makeTempDirSyncMode() {
+ const path = Deno.makeTempDirSync();
+ const pathInfo = Deno.statSync(path);
+ if (Deno.build.os !== "windows") {
+ assertEquals(pathInfo.mode! & 0o777, 0o700 & ~Deno.umask());
+ }
+ },
+);
+
+Deno.test({ permissions: { write: false } }, function makeTempDirSyncPerm() {
+ // makeTempDirSync should require write permissions (for now).
+ assertThrows(() => {
+ Deno.makeTempDirSync({ dir: "/baddir" });
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { write: true } },
+ async function makeTempDirSuccess() {
+ const dir1 = await Deno.makeTempDir({ prefix: "hello", suffix: "world" });
+ const dir2 = await Deno.makeTempDir({ prefix: "hello", suffix: "world" });
+ // Check that both dirs are different.
+ assert(dir1 !== dir2);
+ for (const dir of [dir1, dir2]) {
+ // Check that the prefix and suffix are applied.
+ const lastPart = dir.replace(/^.*[\\\/]/, "");
+ assert(lastPart.startsWith("hello"));
+ assert(lastPart.endsWith("world"));
+ }
+ // Check that the `dir` option works.
+ const dir3 = await Deno.makeTempDir({ dir: dir1 });
+ assert(dir3.startsWith(dir1));
+ assert(/^[\\\/]/.test(dir3.slice(dir1.length)));
+ // Check that creating a temp dir inside a nonexisting directory fails.
+ await assertRejects(async () => {
+ await Deno.makeTempDir({ dir: "/baddir" });
+ }, Deno.errors.NotFound);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function makeTempDirMode() {
+ const path = await Deno.makeTempDir();
+ const pathInfo = Deno.statSync(path);
+ if (Deno.build.os !== "windows") {
+ assertEquals(pathInfo.mode! & 0o777, 0o700 & ~Deno.umask());
+ }
+ },
+);
+
+Deno.test({ permissions: { write: true } }, function makeTempFileSyncSuccess() {
+ const file1 = Deno.makeTempFileSync({ prefix: "hello", suffix: "world" });
+ const file2 = Deno.makeTempFileSync({ prefix: "hello", suffix: "world" });
+ // Check that both dirs are different.
+ assert(file1 !== file2);
+ for (const dir of [file1, file2]) {
+ // Check that the prefix and suffix are applied.
+ const lastPart = dir.replace(/^.*[\\\/]/, "");
+ assert(lastPart.startsWith("hello"));
+ assert(lastPart.endsWith("world"));
+ }
+ // Check that the `dir` option works.
+ const dir = Deno.makeTempDirSync({ prefix: "tempdir" });
+ const file3 = Deno.makeTempFileSync({ dir });
+ assert(file3.startsWith(dir));
+ assert(/^[\\\/]/.test(file3.slice(dir.length)));
+ // Check that creating a temp file inside a nonexisting directory fails.
+ assertThrows(() => {
+ Deno.makeTempFileSync({ dir: "/baddir" });
+ }, Deno.errors.NotFound);
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function makeTempFileSyncMode() {
+ const path = Deno.makeTempFileSync();
+ const pathInfo = Deno.statSync(path);
+ if (Deno.build.os !== "windows") {
+ assertEquals(pathInfo.mode! & 0o777, 0o600 & ~Deno.umask());
+ }
+ },
+);
+
+Deno.test({ permissions: { write: false } }, function makeTempFileSyncPerm() {
+ // makeTempFileSync should require write permissions (for now).
+ assertThrows(() => {
+ Deno.makeTempFileSync({ dir: "/baddir" });
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { write: true } },
+ async function makeTempFileSuccess() {
+ const file1 = await Deno.makeTempFile({ prefix: "hello", suffix: "world" });
+ const file2 = await Deno.makeTempFile({ prefix: "hello", suffix: "world" });
+ // Check that both dirs are different.
+ assert(file1 !== file2);
+ for (const dir of [file1, file2]) {
+ // Check that the prefix and suffix are applied.
+ const lastPart = dir.replace(/^.*[\\\/]/, "");
+ assert(lastPart.startsWith("hello"));
+ assert(lastPart.endsWith("world"));
+ }
+ // Check that the `dir` option works.
+ const dir = Deno.makeTempDirSync({ prefix: "tempdir" });
+ const file3 = await Deno.makeTempFile({ dir });
+ assert(file3.startsWith(dir));
+ assert(/^[\\\/]/.test(file3.slice(dir.length)));
+ // Check that creating a temp file inside a nonexisting directory fails.
+ await assertRejects(async () => {
+ await Deno.makeTempFile({ dir: "/baddir" });
+ }, Deno.errors.NotFound);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function makeTempFileMode() {
+ const path = await Deno.makeTempFile();
+ const pathInfo = Deno.statSync(path);
+ if (Deno.build.os !== "windows") {
+ assertEquals(pathInfo.mode! & 0o777, 0o600 & ~Deno.umask());
+ }
+ },
+);
diff --git a/tests/unit/message_channel_test.ts b/tests/unit/message_channel_test.ts
new file mode 100644
index 000000000..88fb1ba11
--- /dev/null
+++ b/tests/unit/message_channel_test.ts
@@ -0,0 +1,55 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+// NOTE: these are just sometests to test the TypeScript types. Real coverage is
+// provided by WPT.
+import { assert, assertEquals } from "@test_util/std/assert/mod.ts";
+
+Deno.test("messagechannel", async () => {
+ const mc = new MessageChannel();
+ const mc2 = new MessageChannel();
+ assert(mc.port1);
+ assert(mc.port2);
+
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ mc.port2.onmessage = (e) => {
+ assertEquals(e.data, "hello");
+ assertEquals(e.ports.length, 1);
+ assert(e.ports[0] instanceof MessagePort);
+ e.ports[0].close();
+ resolve();
+ };
+
+ mc.port1.postMessage("hello", [mc2.port1]);
+ mc.port1.close();
+
+ await promise;
+
+ mc.port2.close();
+ mc2.port2.close();
+});
+
+Deno.test("messagechannel clone port", async () => {
+ const mc = new MessageChannel();
+ const mc2 = new MessageChannel();
+ assert(mc.port1);
+ assert(mc.port2);
+
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ mc.port2.onmessage = (e) => {
+ const { port } = e.data;
+ assertEquals(e.ports.length, 1);
+ assert(e.ports[0] instanceof MessagePort);
+ assertEquals(e.ports[0], port);
+ e.ports[0].close();
+ resolve();
+ };
+
+ mc.port1.postMessage({ port: mc2.port1 }, [mc2.port1]);
+ mc.port1.close();
+
+ await promise;
+
+ mc.port2.close();
+ mc2.port2.close();
+});
diff --git a/tests/unit/mkdir_test.ts b/tests/unit/mkdir_test.ts
new file mode 100644
index 000000000..0948a1a84
--- /dev/null
+++ b/tests/unit/mkdir_test.ts
@@ -0,0 +1,235 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+ pathToAbsoluteFileUrl,
+} from "./test_util.ts";
+
+function assertDirectory(path: string, mode?: number) {
+ const info = Deno.lstatSync(path);
+ assert(info.isDirectory);
+ if (Deno.build.os !== "windows" && mode !== undefined) {
+ assertEquals(info.mode! & 0o777, mode & ~Deno.umask());
+ }
+}
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function mkdirSyncSuccess() {
+ const path = Deno.makeTempDirSync() + "/dir";
+ Deno.mkdirSync(path);
+ assertDirectory(path);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function mkdirSyncMode() {
+ const path = Deno.makeTempDirSync() + "/dir";
+ Deno.mkdirSync(path, { mode: 0o737 });
+ assertDirectory(path, 0o737);
+ },
+);
+
+Deno.test({ permissions: { write: false } }, function mkdirSyncPerm() {
+ assertThrows(() => {
+ Deno.mkdirSync("/baddir");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function mkdirSuccess() {
+ const path = Deno.makeTempDirSync() + "/dir";
+ await Deno.mkdir(path);
+ assertDirectory(path);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function mkdirMode() {
+ const path = Deno.makeTempDirSync() + "/dir";
+ await Deno.mkdir(path, { mode: 0o737 });
+ assertDirectory(path, 0o737);
+ },
+);
+
+Deno.test({ permissions: { write: true } }, function mkdirErrSyncIfExists() {
+ assertThrows(
+ () => {
+ Deno.mkdirSync(".");
+ },
+ Deno.errors.AlreadyExists,
+ `mkdir '.'`,
+ );
+});
+
+Deno.test({ permissions: { write: true } }, async function mkdirErrIfExists() {
+ await assertRejects(
+ async () => {
+ await Deno.mkdir(".");
+ },
+ Deno.errors.AlreadyExists,
+ `mkdir '.'`,
+ );
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function mkdirSyncRecursive() {
+ const path = Deno.makeTempDirSync() + "/nested/directory";
+ Deno.mkdirSync(path, { recursive: true });
+ assertDirectory(path);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function mkdirRecursive() {
+ const path = Deno.makeTempDirSync() + "/nested/directory";
+ await Deno.mkdir(path, { recursive: true });
+ assertDirectory(path);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function mkdirSyncRecursiveMode() {
+ const nested = Deno.makeTempDirSync() + "/nested";
+ const path = nested + "/dir";
+ Deno.mkdirSync(path, { mode: 0o737, recursive: true });
+ assertDirectory(path, 0o737);
+ assertDirectory(nested, 0o737);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function mkdirRecursiveMode() {
+ const nested = Deno.makeTempDirSync() + "/nested";
+ const path = nested + "/dir";
+ await Deno.mkdir(path, { mode: 0o737, recursive: true });
+ assertDirectory(path, 0o737);
+ assertDirectory(nested, 0o737);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function mkdirSyncRecursiveIfExists() {
+ const path = Deno.makeTempDirSync() + "/dir";
+ Deno.mkdirSync(path, { mode: 0o737 });
+ Deno.mkdirSync(path, { recursive: true });
+ Deno.mkdirSync(path, { recursive: true, mode: 0o731 });
+ assertDirectory(path, 0o737);
+ if (Deno.build.os !== "windows") {
+ const pathLink = path + "Link";
+ Deno.symlinkSync(path, pathLink);
+ Deno.mkdirSync(pathLink, { recursive: true });
+ Deno.mkdirSync(pathLink, { recursive: true, mode: 0o731 });
+ assertDirectory(path, 0o737);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function mkdirRecursiveIfExists() {
+ const path = Deno.makeTempDirSync() + "/dir";
+ await Deno.mkdir(path, { mode: 0o737 });
+ await Deno.mkdir(path, { recursive: true });
+ await Deno.mkdir(path, { recursive: true, mode: 0o731 });
+ assertDirectory(path, 0o737);
+ if (Deno.build.os !== "windows") {
+ const pathLink = path + "Link";
+ Deno.symlinkSync(path, pathLink);
+ await Deno.mkdir(pathLink, { recursive: true });
+ await Deno.mkdir(pathLink, { recursive: true, mode: 0o731 });
+ assertDirectory(path, 0o737);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function mkdirSyncErrors() {
+ const testDir = Deno.makeTempDirSync();
+ const emptydir = testDir + "/empty";
+ const fulldir = testDir + "/dir";
+ const file = fulldir + "/file";
+ Deno.mkdirSync(emptydir);
+ Deno.mkdirSync(fulldir);
+ Deno.createSync(file).close();
+
+ assertThrows(() => {
+ Deno.mkdirSync(emptydir, { recursive: false });
+ }, Deno.errors.AlreadyExists);
+ assertThrows(() => {
+ Deno.mkdirSync(fulldir, { recursive: false });
+ }, Deno.errors.AlreadyExists);
+ assertThrows(() => {
+ Deno.mkdirSync(file, { recursive: false });
+ }, Deno.errors.AlreadyExists);
+ assertThrows(() => {
+ Deno.mkdirSync(file, { recursive: true });
+ }, Deno.errors.AlreadyExists);
+
+ if (Deno.build.os !== "windows") {
+ const fileLink = testDir + "/fileLink";
+ const dirLink = testDir + "/dirLink";
+ const danglingLink = testDir + "/danglingLink";
+ Deno.symlinkSync(file, fileLink);
+ Deno.symlinkSync(emptydir, dirLink);
+ Deno.symlinkSync(testDir + "/nonexistent", danglingLink);
+
+ assertThrows(() => {
+ Deno.mkdirSync(dirLink, { recursive: false });
+ }, Deno.errors.AlreadyExists);
+ assertThrows(() => {
+ Deno.mkdirSync(fileLink, { recursive: false });
+ }, Deno.errors.AlreadyExists);
+ assertThrows(() => {
+ Deno.mkdirSync(fileLink, { recursive: true });
+ }, Deno.errors.AlreadyExists);
+ assertThrows(() => {
+ Deno.mkdirSync(danglingLink, { recursive: false });
+ }, Deno.errors.AlreadyExists);
+ assertThrows(() => {
+ Deno.mkdirSync(danglingLink, { recursive: true });
+ }, Deno.errors.AlreadyExists);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function mkdirSyncRelativeUrlPath() {
+ const testDir = Deno.makeTempDirSync();
+ const nestedDir = testDir + "/nested";
+ // Add trailing slash so base path is treated as a directory. pathToAbsoluteFileUrl removes trailing slashes.
+ const path = new URL("../dir", pathToAbsoluteFileUrl(nestedDir) + "/");
+
+ Deno.mkdirSync(nestedDir);
+ Deno.mkdirSync(path);
+
+ assertDirectory(testDir + "/dir");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function mkdirRelativeUrlPath() {
+ const testDir = Deno.makeTempDirSync();
+ const nestedDir = testDir + "/nested";
+ // Add trailing slash so base path is treated as a directory. pathToAbsoluteFileUrl removes trailing slashes.
+ const path = new URL("../dir", pathToAbsoluteFileUrl(nestedDir) + "/");
+
+ await Deno.mkdir(nestedDir);
+ await Deno.mkdir(path);
+
+ assertDirectory(testDir + "/dir");
+ },
+);
diff --git a/tests/unit/navigator_test.ts b/tests/unit/navigator_test.ts
new file mode 100644
index 000000000..5dcc423fa
--- /dev/null
+++ b/tests/unit/navigator_test.ts
@@ -0,0 +1,11 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert } from "./test_util.ts";
+
+Deno.test(function navigatorNumCpus() {
+ assert(navigator.hardwareConcurrency > 0);
+});
+
+Deno.test(function navigatorUserAgent() {
+ const pattern = /Deno\/\d+\.\d+\.\d+/;
+ assert(pattern.test(navigator.userAgent));
+});
diff --git a/tests/unit/net_test.ts b/tests/unit/net_test.ts
new file mode 100644
index 000000000..eae1ae533
--- /dev/null
+++ b/tests/unit/net_test.ts
@@ -0,0 +1,1274 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertNotEquals,
+ assertRejects,
+ assertThrows,
+ delay,
+ execCode,
+ execCode2,
+ tmpUnixSocketPath,
+} from "./test_util.ts";
+
+// Since these tests may run in parallel, ensure this port is unique to this file
+const listenPort = 4503;
+const listenPort2 = 4504;
+
+let isCI: boolean;
+try {
+ isCI = Deno.env.get("CI") !== undefined;
+} catch {
+ isCI = true;
+}
+
+Deno.test({ permissions: { net: true } }, function netTcpListenClose() {
+ const listener = Deno.listen({ hostname: "127.0.0.1", port: listenPort });
+ assert(listener.addr.transport === "tcp");
+ assertEquals(listener.addr.hostname, "127.0.0.1");
+ assertEquals(listener.addr.port, listenPort);
+ assertNotEquals(listener.rid, 0);
+ listener.close();
+});
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ function netUdpListenClose() {
+ const socket = Deno.listenDatagram({
+ hostname: "127.0.0.1",
+ port: listenPort,
+ transport: "udp",
+ });
+ assert(socket.addr.transport === "udp");
+ assertEquals(socket.addr.hostname, "127.0.0.1");
+ assertEquals(socket.addr.port, listenPort);
+ socket.close();
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ function netUnixListenClose() {
+ const filePath = tmpUnixSocketPath();
+ const socket = Deno.listen({
+ path: filePath,
+ transport: "unix",
+ });
+ assert(socket.addr.transport === "unix");
+ assertEquals(socket.addr.path, filePath);
+ socket.close();
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ function netUnixPacketListenClose() {
+ const filePath = tmpUnixSocketPath();
+ const socket = Deno.listenDatagram({
+ path: filePath,
+ transport: "unixpacket",
+ });
+ assert(socket.addr.transport === "unixpacket");
+ assertEquals(socket.addr.path, filePath);
+ socket.close();
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: false },
+ },
+ function netUnixListenWritePermission() {
+ assertThrows(() => {
+ const filePath = tmpUnixSocketPath();
+ const socket = Deno.listen({
+ path: filePath,
+ transport: "unix",
+ });
+ assert(socket.addr.transport === "unix");
+ assertEquals(socket.addr.path, filePath);
+ socket.close();
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: false },
+ },
+ function netUnixPacketListenWritePermission() {
+ assertThrows(() => {
+ const filePath = tmpUnixSocketPath();
+ const socket = Deno.listenDatagram({
+ path: filePath,
+ transport: "unixpacket",
+ });
+ assert(socket.addr.transport === "unixpacket");
+ assertEquals(socket.addr.path, filePath);
+ socket.close();
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function netTcpCloseWhileAccept() {
+ const listener = Deno.listen({ port: listenPort });
+ const p = listener.accept();
+ listener.close();
+ // TODO(piscisaureus): the error type should be `Interrupted` here, which
+ // gets thrown, but then ext/net catches it and rethrows `BadResource`.
+ await assertRejects(
+ () => p,
+ Deno.errors.BadResource,
+ "Listener has been closed",
+ );
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function netUnixCloseWhileAccept() {
+ const filePath = tmpUnixSocketPath();
+ const listener = Deno.listen({
+ path: filePath,
+ transport: "unix",
+ });
+ const p = listener.accept();
+ listener.close();
+ await assertRejects(
+ () => p,
+ Deno.errors.BadResource,
+ "Listener has been closed",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function netTcpConcurrentAccept() {
+ const listener = Deno.listen({ port: 4510 });
+ let acceptErrCount = 0;
+ const checkErr = (e: Error) => {
+ if (e.message === "Listener has been closed") {
+ assertEquals(acceptErrCount, 1);
+ } else if (e.message === "Another accept task is ongoing") {
+ acceptErrCount++;
+ } else {
+ throw new Error("Unexpected error message");
+ }
+ };
+ const p = listener.accept().catch(checkErr);
+ const p1 = listener.accept().catch(checkErr);
+ await Promise.race([p, p1]);
+ listener.close();
+ await Promise.all([p, p1]);
+ assertEquals(acceptErrCount, 1);
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function netUnixConcurrentAccept() {
+ const filePath = tmpUnixSocketPath();
+ const listener = Deno.listen({ transport: "unix", path: filePath });
+ let acceptErrCount = 0;
+ const checkErr = (e: Error) => {
+ if (e.message === "Listener has been closed") {
+ assertEquals(acceptErrCount, 1);
+ } else if (e instanceof Deno.errors.Busy) { // "Listener already in use"
+ acceptErrCount++;
+ } else {
+ throw e;
+ }
+ };
+ const p = listener.accept().catch(checkErr);
+ const p1 = listener.accept().catch(checkErr);
+ await Promise.race([p, p1]);
+ listener.close();
+ await Promise.all([p, p1]);
+ assertEquals(acceptErrCount, 1);
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function netTcpDialListen() {
+ const listener = Deno.listen({ port: listenPort });
+ listener.accept().then(
+ async (conn) => {
+ assert(conn.remoteAddr != null);
+ assert(conn.localAddr.transport === "tcp");
+ assertEquals(conn.localAddr.hostname, "127.0.0.1");
+ assertEquals(conn.localAddr.port, listenPort);
+ await conn.write(new Uint8Array([1, 2, 3]));
+ conn.close();
+ },
+ );
+
+ const conn = await Deno.connect({ hostname: "127.0.0.1", port: listenPort });
+ assert(conn.remoteAddr.transport === "tcp");
+ assertEquals(conn.remoteAddr.hostname, "127.0.0.1");
+ assertEquals(conn.remoteAddr.port, listenPort);
+ assert(conn.localAddr != null);
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assertEquals(3, readResult);
+ assertEquals(1, buf[0]);
+ assertEquals(2, buf[1]);
+ assertEquals(3, buf[2]);
+ assert(conn.rid > 0);
+
+ assert(readResult !== null);
+
+ const readResult2 = await conn.read(buf);
+ assertEquals(readResult2, null);
+
+ listener.close();
+ conn.close();
+});
+
+Deno.test({ permissions: { net: true } }, async function netTcpSetNoDelay() {
+ const listener = Deno.listen({ port: listenPort });
+ listener.accept().then(
+ async (conn) => {
+ assert(conn.remoteAddr != null);
+ assert(conn.localAddr.transport === "tcp");
+ assertEquals(conn.localAddr.hostname, "127.0.0.1");
+ assertEquals(conn.localAddr.port, listenPort);
+ await conn.write(new Uint8Array([1, 2, 3]));
+ conn.close();
+ },
+ );
+
+ const conn = await Deno.connect({ hostname: "127.0.0.1", port: listenPort });
+ conn.setNoDelay(true);
+ assert(conn.remoteAddr.transport === "tcp");
+ assertEquals(conn.remoteAddr.hostname, "127.0.0.1");
+ assertEquals(conn.remoteAddr.port, listenPort);
+ assert(conn.localAddr != null);
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assertEquals(3, readResult);
+ assertEquals(1, buf[0]);
+ assertEquals(2, buf[1]);
+ assertEquals(3, buf[2]);
+ assert(conn.rid > 0);
+
+ assert(readResult !== null);
+
+ const readResult2 = await conn.read(buf);
+ assertEquals(readResult2, null);
+
+ listener.close();
+ conn.close();
+});
+
+Deno.test({ permissions: { net: true } }, async function netTcpSetKeepAlive() {
+ const listener = Deno.listen({ port: listenPort });
+ listener.accept().then(
+ async (conn) => {
+ assert(conn.remoteAddr != null);
+ assert(conn.localAddr.transport === "tcp");
+ assertEquals(conn.localAddr.hostname, "127.0.0.1");
+ assertEquals(conn.localAddr.port, listenPort);
+ await conn.write(new Uint8Array([1, 2, 3]));
+ conn.close();
+ },
+ );
+
+ const conn = await Deno.connect({ hostname: "127.0.0.1", port: listenPort });
+ conn.setKeepAlive(true);
+ assert(conn.remoteAddr.transport === "tcp");
+ assertEquals(conn.remoteAddr.hostname, "127.0.0.1");
+ assertEquals(conn.remoteAddr.port, listenPort);
+ assert(conn.localAddr != null);
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assertEquals(3, readResult);
+ assertEquals(1, buf[0]);
+ assertEquals(2, buf[1]);
+ assertEquals(3, buf[2]);
+ assert(conn.rid > 0);
+
+ assert(readResult !== null);
+
+ const readResult2 = await conn.read(buf);
+ assertEquals(readResult2, null);
+
+ listener.close();
+ conn.close();
+});
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function netUnixDialListen() {
+ const filePath = tmpUnixSocketPath();
+ const listener = Deno.listen({ path: filePath, transport: "unix" });
+ listener.accept().then(
+ async (conn) => {
+ assert(conn.remoteAddr != null);
+ assert(conn.localAddr.transport === "unix");
+ assertEquals(conn.localAddr.path, filePath);
+ await conn.write(new Uint8Array([1, 2, 3]));
+ conn.close();
+ },
+ );
+ const conn = await Deno.connect({ path: filePath, transport: "unix" });
+ assert(conn.remoteAddr.transport === "unix");
+ assertEquals(conn.remoteAddr.path, filePath);
+ assert(conn.remoteAddr != null);
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assertEquals(3, readResult);
+ assertEquals(1, buf[0]);
+ assertEquals(2, buf[1]);
+ assertEquals(3, buf[2]);
+ assert(conn.rid > 0);
+
+ assert(readResult !== null);
+
+ const readResult2 = await conn.read(buf);
+ assertEquals(readResult2, null);
+
+ listener.close();
+ conn.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function netUdpSendReceive() {
+ const alice = Deno.listenDatagram({ port: listenPort, transport: "udp" });
+ assert(alice.addr.transport === "udp");
+ assertEquals(alice.addr.port, listenPort);
+ assertEquals(alice.addr.hostname, "127.0.0.1");
+
+ const bob = Deno.listenDatagram({ port: listenPort2, transport: "udp" });
+ assert(bob.addr.transport === "udp");
+ assertEquals(bob.addr.port, listenPort2);
+ assertEquals(bob.addr.hostname, "127.0.0.1");
+
+ const sent = new Uint8Array([1, 2, 3]);
+ const byteLength = await alice.send(sent, bob.addr);
+
+ assertEquals(byteLength, 3);
+
+ const [recvd, remote] = await bob.receive();
+ assert(remote.transport === "udp");
+ assertEquals(remote.port, listenPort);
+ assertEquals(recvd.length, 3);
+ assertEquals(1, recvd[0]);
+ assertEquals(2, recvd[1]);
+ assertEquals(3, recvd[2]);
+ alice.close();
+ bob.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true }, ignore: true },
+ async function netUdpSendReceiveBroadcast() {
+ // Must bind sender to an address that can send to the broadcast address on MacOS.
+ // Macos will give us error 49 when sending the broadcast packet if we omit hostname here.
+ const alice = Deno.listenDatagram({
+ port: listenPort,
+ transport: "udp",
+ hostname: "0.0.0.0",
+ });
+
+ const bob = Deno.listenDatagram({
+ port: listenPort,
+ transport: "udp",
+ hostname: "0.0.0.0",
+ });
+ assert(bob.addr.transport === "udp");
+ assertEquals(bob.addr.port, listenPort);
+ assertEquals(bob.addr.hostname, "0.0.0.0");
+
+ const broadcastAddr = { ...bob.addr, hostname: "255.255.255.255" };
+
+ const sent = new Uint8Array([1, 2, 3]);
+ const byteLength = await alice.send(sent, broadcastAddr);
+
+ assertEquals(byteLength, 3);
+ const [recvd, remote] = await bob.receive();
+ assert(remote.transport === "udp");
+ assertEquals(remote.port, listenPort);
+ assertEquals(recvd.length, 3);
+ assertEquals(1, recvd[0]);
+ assertEquals(2, recvd[1]);
+ assertEquals(3, recvd[2]);
+ alice.close();
+ bob.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true }, ignore: true },
+ async function netUdpMulticastV4() {
+ const listener = Deno.listenDatagram({
+ hostname: "0.0.0.0",
+ port: 5353,
+ transport: "udp",
+ reuseAddress: true,
+ });
+
+ const membership = await listener.joinMulticastV4(
+ "224.0.0.251",
+ "127.0.0.1",
+ );
+
+ membership.setLoopback(true);
+ membership.setLoopback(false);
+ membership.setTTL(50);
+ membership.leave();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true }, ignore: true },
+ async function netUdpMulticastV6() {
+ const listener = Deno.listenDatagram({
+ hostname: "::",
+ port: 5353,
+ transport: "udp",
+ reuseAddress: true,
+ });
+
+ const membership = await listener.joinMulticastV6(
+ "ff02::fb",
+ 1,
+ );
+
+ membership.setLoopback(true);
+ membership.setLoopback(false);
+ membership.leave();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true }, ignore: true },
+ async function netUdpSendReceiveMulticastv4() {
+ const alice = Deno.listenDatagram({
+ hostname: "0.0.0.0",
+ port: 5353,
+ transport: "udp",
+ reuseAddress: true,
+ loopback: true,
+ });
+
+ const bob = Deno.listenDatagram({
+ hostname: "0.0.0.0",
+ port: 5353,
+ transport: "udp",
+ reuseAddress: true,
+ });
+
+ const aliceMembership = await alice.joinMulticastV4(
+ "224.0.0.1",
+ "0.0.0.0",
+ );
+
+ const bobMembership = await bob.joinMulticastV4("224.0.0.1", "0.0.0.0");
+
+ const sent = new Uint8Array([1, 2, 3]);
+
+ await alice.send(sent, {
+ hostname: "224.0.0.1",
+ port: 5353,
+ transport: "udp",
+ });
+
+ const [recvd, remote] = await bob.receive();
+
+ assert(remote.transport === "udp");
+ assertEquals(remote.port, 5353);
+ assertEquals(recvd.length, 3);
+ assertEquals(1, recvd[0]);
+ assertEquals(2, recvd[1]);
+ assertEquals(3, recvd[2]);
+
+ aliceMembership.leave();
+ bobMembership.leave();
+
+ alice.close();
+ bob.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true }, ignore: true },
+ async function netUdpMulticastLoopbackOption() {
+ // Must bind sender to an address that can send to the broadcast address on MacOS.
+ // Macos will give us error 49 when sending the broadcast packet if we omit hostname here.
+ const listener = Deno.listenDatagram({
+ port: 5353,
+ transport: "udp",
+ hostname: "0.0.0.0",
+ loopback: true,
+ reuseAddress: true,
+ });
+
+ const membership = await listener.joinMulticastV4(
+ "224.0.0.1",
+ "0.0.0.0",
+ );
+
+ // await membership.setLoopback(true);
+
+ const sent = new Uint8Array([1, 2, 3]);
+ const byteLength = await listener.send(sent, {
+ hostname: "224.0.0.1",
+ port: 5353,
+ transport: "udp",
+ });
+
+ assertEquals(byteLength, 3);
+ const [recvd, remote] = await listener.receive();
+ assert(remote.transport === "udp");
+ assertEquals(remote.port, 5353);
+ assertEquals(recvd.length, 3);
+ assertEquals(1, recvd[0]);
+ assertEquals(2, recvd[1]);
+ assertEquals(3, recvd[2]);
+ membership.leave();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function netUdpConcurrentSendReceive() {
+ const socket = Deno.listenDatagram({ port: listenPort, transport: "udp" });
+ assert(socket.addr.transport === "udp");
+ assertEquals(socket.addr.port, listenPort);
+ assertEquals(socket.addr.hostname, "127.0.0.1");
+
+ const recvPromise = socket.receive();
+
+ const sendBuf = new Uint8Array([1, 2, 3]);
+ const sendLen = await socket.send(sendBuf, socket.addr);
+ assertEquals(sendLen, 3);
+
+ const [recvBuf, _recvAddr] = await recvPromise;
+ assertEquals(recvBuf.length, 3);
+ assertEquals(1, recvBuf[0]);
+ assertEquals(2, recvBuf[1]);
+ assertEquals(3, recvBuf[2]);
+
+ socket.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function netUdpBorrowMutError() {
+ const socket = Deno.listenDatagram({
+ port: listenPort,
+ transport: "udp",
+ });
+ // Panic happened on second send: BorrowMutError
+ const a = socket.send(new Uint8Array(), socket.addr);
+ const b = socket.send(new Uint8Array(), socket.addr);
+ await Promise.all([a, b]);
+ socket.close();
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function netUnixPacketSendReceive() {
+ const aliceFilePath = tmpUnixSocketPath();
+ const alice = Deno.listenDatagram({
+ path: aliceFilePath,
+ transport: "unixpacket",
+ });
+ assert(alice.addr.transport === "unixpacket");
+ assertEquals(alice.addr.path, aliceFilePath);
+
+ const bobFilePath = tmpUnixSocketPath();
+ const bob = Deno.listenDatagram({
+ path: bobFilePath,
+ transport: "unixpacket",
+ });
+ assert(bob.addr.transport === "unixpacket");
+ assertEquals(bob.addr.path, bobFilePath);
+
+ const sent = new Uint8Array([1, 2, 3]);
+ const byteLength = await alice.send(sent, bob.addr);
+ assertEquals(byteLength, 3);
+
+ const [recvd, remote] = await bob.receive();
+ assert(remote.transport === "unixpacket");
+ assertEquals(remote.path, aliceFilePath);
+ assertEquals(recvd.length, 3);
+ assertEquals(1, recvd[0]);
+ assertEquals(2, recvd[1]);
+ assertEquals(3, recvd[2]);
+ alice.close();
+ bob.close();
+ },
+);
+
+// TODO(lucacasonato): support concurrent reads and writes on unixpacket sockets
+Deno.test(
+ { ignore: true, permissions: { read: true, write: true } },
+ async function netUnixPacketConcurrentSendReceive() {
+ const filePath = tmpUnixSocketPath();
+ const socket = Deno.listenDatagram({
+ path: filePath,
+ transport: "unixpacket",
+ });
+ assert(socket.addr.transport === "unixpacket");
+ assertEquals(socket.addr.path, filePath);
+
+ const recvPromise = socket.receive();
+
+ const sendBuf = new Uint8Array([1, 2, 3]);
+ const sendLen = await socket.send(sendBuf, socket.addr);
+ assertEquals(sendLen, 3);
+
+ const [recvBuf, _recvAddr] = await recvPromise;
+ assertEquals(recvBuf.length, 3);
+ assertEquals(1, recvBuf[0]);
+ assertEquals(2, recvBuf[1]);
+ assertEquals(3, recvBuf[2]);
+
+ socket.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function netTcpListenIteratorBreakClosesResource() {
+ async function iterate(listener: Deno.Listener) {
+ let i = 0;
+
+ for await (const conn of listener) {
+ conn.close();
+ i++;
+
+ if (i > 1) {
+ break;
+ }
+ }
+ }
+
+ const addr = { hostname: "127.0.0.1", port: 8888 };
+ const listener = Deno.listen(addr);
+ const iteratePromise = iterate(listener);
+
+ await delay(100);
+ const conn1 = await Deno.connect(addr);
+ conn1.close();
+ const conn2 = await Deno.connect(addr);
+ conn2.close();
+
+ await iteratePromise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function netTcpListenCloseWhileIterating() {
+ const listener = Deno.listen({ port: 8001 });
+ const nextWhileClosing = listener[Symbol.asyncIterator]().next();
+ listener.close();
+ assertEquals(await nextWhileClosing, { value: undefined, done: true });
+
+ const nextAfterClosing = listener[Symbol.asyncIterator]().next();
+ assertEquals(await nextAfterClosing, { value: undefined, done: true });
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function netUdpListenCloseWhileIterating() {
+ const socket = Deno.listenDatagram({ port: 8000, transport: "udp" });
+ const nextWhileClosing = socket[Symbol.asyncIterator]().next();
+ socket.close();
+ assertEquals(await nextWhileClosing, { value: undefined, done: true });
+
+ const nextAfterClosing = socket[Symbol.asyncIterator]().next();
+ assertEquals(await nextAfterClosing, { value: undefined, done: true });
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function netUnixListenCloseWhileIterating() {
+ const filePath = tmpUnixSocketPath();
+ const socket = Deno.listen({ path: filePath, transport: "unix" });
+ const nextWhileClosing = socket[Symbol.asyncIterator]().next();
+ socket.close();
+ assertEquals(await nextWhileClosing, { value: undefined, done: true });
+
+ const nextAfterClosing = socket[Symbol.asyncIterator]().next();
+ assertEquals(await nextAfterClosing, { value: undefined, done: true });
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function netUnixPacketListenCloseWhileIterating() {
+ const filePath = tmpUnixSocketPath();
+ const socket = Deno.listenDatagram({
+ path: filePath,
+ transport: "unixpacket",
+ });
+ const nextWhileClosing = socket[Symbol.asyncIterator]().next();
+ socket.close();
+ assertEquals(await nextWhileClosing, { value: undefined, done: true });
+
+ const nextAfterClosing = socket[Symbol.asyncIterator]().next();
+ assertEquals(await nextAfterClosing, { value: undefined, done: true });
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function netListenAsyncIterator() {
+ const addr = { hostname: "127.0.0.1", port: listenPort };
+ const listener = Deno.listen(addr);
+ const runAsyncIterator = async () => {
+ for await (const conn of listener) {
+ await conn.write(new Uint8Array([1, 2, 3]));
+ conn.close();
+ }
+ };
+ runAsyncIterator();
+ const conn = await Deno.connect(addr);
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assertEquals(3, readResult);
+ assertEquals(1, buf[0]);
+ assertEquals(2, buf[1]);
+ assertEquals(3, buf[2]);
+ assert(conn.rid > 0);
+
+ assert(readResult !== null);
+
+ const readResult2 = await conn.read(buf);
+ assertEquals(readResult2, null);
+
+ listener.close();
+ conn.close();
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function netCloseWriteSuccess() {
+ const addr = { hostname: "127.0.0.1", port: listenPort };
+ const listener = Deno.listen(addr);
+ const { promise: closePromise, resolve } = Promise.withResolvers<void>();
+ listener.accept().then(async (conn) => {
+ await conn.write(new Uint8Array([1, 2, 3]));
+ await closePromise;
+ conn.close();
+ });
+ const conn = await Deno.connect(addr);
+ conn.closeWrite(); // closing write
+ const buf = new Uint8Array(1024);
+ // Check read not impacted
+ const readResult = await conn.read(buf);
+ assertEquals(3, readResult);
+ assertEquals(1, buf[0]);
+ assertEquals(2, buf[1]);
+ assertEquals(3, buf[2]);
+ // Verify that the write end of the socket is closed.
+ // TODO(piscisaureus): assert that thrown error is of a specific type.
+ await assertRejects(async () => {
+ await conn.write(new Uint8Array([1, 2, 3]));
+ });
+ resolve();
+ listener.close();
+ conn.close();
+ },
+);
+
+Deno.test(
+ {
+ // https://github.com/denoland/deno/issues/11580
+ ignore: Deno.build.os === "darwin" && isCI,
+ permissions: { net: true },
+ },
+ async function netHangsOnClose() {
+ let acceptedConn: Deno.Conn;
+
+ async function iteratorReq(listener: Deno.Listener) {
+ const p = new Uint8Array(10);
+ const conn = await listener.accept();
+ acceptedConn = conn;
+
+ try {
+ while (true) {
+ const nread = await conn.read(p);
+ if (nread === null) {
+ break;
+ }
+ await conn.write(new Uint8Array([1, 2, 3]));
+ }
+ } catch (err) {
+ assert(err);
+ assert(err instanceof Deno.errors.Interrupted);
+ }
+ }
+
+ const addr = { hostname: "127.0.0.1", port: listenPort };
+ const listener = Deno.listen(addr);
+ const listenerPromise = iteratorReq(listener);
+ const connectionPromise = (async () => {
+ const conn = await Deno.connect(addr);
+ await conn.write(new Uint8Array([1, 2, 3, 4]));
+ const buf = new Uint8Array(10);
+ await conn.read(buf);
+ conn!.close();
+ acceptedConn!.close();
+ listener.close();
+ })();
+
+ await Promise.all([
+ listenerPromise,
+ connectionPromise,
+ ]);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ function netExplicitUndefinedHostname() {
+ const listener = Deno.listen({ hostname: undefined, port: 8080 });
+ assertEquals((listener.addr as Deno.NetAddr).hostname, "0.0.0.0");
+ listener.close();
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os !== "linux",
+ permissions: { read: true, write: true },
+ },
+ function netUnixAbstractPathShouldNotPanic() {
+ const listener = Deno.listen({
+ path: "\0aaa",
+ transport: "unix",
+ });
+ assert("not panic");
+ listener.close();
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function whatwgStreams() {
+ const server = (async () => {
+ const listener = Deno.listen({ hostname: "127.0.0.1", port: listenPort });
+ const conn = await listener.accept();
+ await conn.readable.pipeTo(conn.writable);
+ listener.close();
+ })();
+
+ const conn = await Deno.connect({ hostname: "127.0.0.1", port: listenPort });
+ const reader = conn.readable.getReader();
+ const writer = conn.writable.getWriter();
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+ const data = encoder.encode("Hello World");
+
+ await writer.write(data);
+ const { value, done } = await reader.read();
+ assert(!done);
+ assertEquals(decoder.decode(value), "Hello World");
+ await reader.cancel();
+ await server;
+});
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readableStreamTextEncoderPipe() {
+ const filename = "tests/testdata/assets/hello.txt";
+ const file = await Deno.open(filename);
+ const readable = file.readable.pipeThrough(new TextDecoderStream());
+ const chunks = [];
+ for await (const chunk of readable) {
+ chunks.push(chunk);
+ }
+ assertEquals(chunks.length, 1);
+ assertEquals(chunks[0].length, 12);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writableStream() {
+ const path = await Deno.makeTempFile();
+ const file = await Deno.open(path, { write: true });
+ assert(file.writable instanceof WritableStream);
+ const readable = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new TextEncoder().encode("hello "));
+ controller.enqueue(new TextEncoder().encode("world!"));
+ controller.close();
+ },
+ });
+ await readable.pipeTo(file.writable);
+ const res = await Deno.readTextFile(path);
+ assertEquals(res, "hello world!");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: true } },
+ async function netListenUnref() {
+ const [statusCode, _output] = await execCode(`
+ async function main() {
+ const listener = Deno.listen({ port: ${listenPort} });
+ listener.unref();
+ await listener.accept(); // This doesn't block the program from exiting
+ }
+ main();
+ `);
+ assertEquals(statusCode, 0);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: true } },
+ async function netListenUnref2() {
+ const [statusCode, _output] = await execCode(`
+ async function main() {
+ const listener = Deno.listen({ port: ${listenPort} });
+ await listener.accept();
+ listener.unref();
+ await listener.accept(); // The program exits here
+ throw new Error(); // The program doesn't reach here
+ }
+ main();
+ const conn = await Deno.connect({ port: ${listenPort} });
+ conn.close();
+ `);
+ assertEquals(statusCode, 0);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: true, net: true } },
+ async function netListenUnrefAndRef() {
+ const p = execCode2(`
+ async function main() {
+ const listener = Deno.listen({ port: ${listenPort} });
+ listener.unref();
+ listener.ref(); // This restores 'ref' state of listener
+ console.log("started");
+ await listener.accept();
+ console.log("accepted")
+ }
+ main();
+ `);
+ await p.waitStdoutText("started");
+ const conn = await Deno.connect({ port: listenPort });
+ conn.close();
+ const [statusCode, output] = await p.finished();
+ assertEquals(statusCode, 0);
+ assertEquals(output.trim(), "started\naccepted");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function netListenUnrefConcurrentAccept() {
+ const timer = setTimeout(() => {}, 1000);
+ const listener = Deno.listen({ port: listenPort });
+ listener.accept().catch(() => {});
+ listener.unref();
+ // Unref'd listener still causes Busy error
+ // on concurrent accept calls.
+ await assertRejects(async () => {
+ await listener.accept(); // The program exits here
+ }, Deno.errors.Busy);
+ listener.close();
+ clearTimeout(timer);
+ },
+);
+
+Deno.test({
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+}, function netUnixListenAddrAlreadyInUse() {
+ const filePath = tmpUnixSocketPath();
+ const listener = Deno.listen({ path: filePath, transport: "unix" });
+ assertThrows(
+ () => {
+ Deno.listen({ path: filePath, transport: "unix" });
+ },
+ Deno.errors.AddrInUse,
+ );
+ listener.close();
+});
+
+Deno.test(
+ { permissions: { net: true, read: true, run: true } },
+ async function netConnUnref() {
+ const listener = Deno.listen({ port: listenPort });
+ const intervalId = setInterval(() => {}); // This keeps event loop alive.
+
+ const program = execCode(`
+ async function main() {
+ const conn = await Deno.connect({ port: ${listenPort} });
+ conn.unref();
+ await conn.read(new Uint8Array(10)); // The program exits here
+ throw new Error(); // The program doesn't reach here
+ }
+ main();
+ `);
+ const conn = await listener.accept();
+ const [statusCode, _output] = await program;
+ conn.close();
+ listener.close();
+ clearInterval(intervalId);
+ assertEquals(statusCode, 0);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true, run: true } },
+ async function netConnUnrefReadable() {
+ const listener = Deno.listen({ port: listenPort });
+ const intervalId = setInterval(() => {}); // This keeps event loop alive.
+
+ const program = execCode(`
+ async function main() {
+ const conn = await Deno.connect({ port: ${listenPort} });
+ conn.unref();
+ const reader = conn.readable.getReader();
+ await reader.read(); // The program exits here
+ throw new Error(); // The program doesn't reach here
+ }
+ main();
+ `);
+ const conn = await listener.accept();
+ const [statusCode, _output] = await program;
+ conn.close();
+ listener.close();
+ clearInterval(intervalId);
+ assertEquals(statusCode, 0);
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function netTcpReuseAddr() {
+ const listener1 = Deno.listen({
+ hostname: "127.0.0.1",
+ port: listenPort,
+ });
+ listener1.accept().then(
+ (conn) => {
+ conn.close();
+ },
+ );
+
+ const conn1 = await Deno.connect({ hostname: "127.0.0.1", port: listenPort });
+ const buf1 = new Uint8Array(1024);
+ await conn1.read(buf1);
+ listener1.close();
+ conn1.close();
+
+ const listener2 = Deno.listen({
+ hostname: "127.0.0.1",
+ port: listenPort,
+ });
+
+ listener2.accept().then(
+ (conn) => {
+ conn.close();
+ },
+ );
+
+ const conn2 = await Deno.connect({ hostname: "127.0.0.1", port: listenPort });
+ const buf2 = new Uint8Array(1024);
+ await conn2.read(buf2);
+
+ listener2.close();
+ conn2.close();
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function netUdpReuseAddr() {
+ const sender = Deno.listenDatagram({
+ port: 4002,
+ transport: "udp",
+ });
+ const listener1 = Deno.listenDatagram({
+ port: 4000,
+ transport: "udp",
+ reuseAddress: true,
+ });
+ const listener2 = Deno.listenDatagram({
+ port: 4000,
+ transport: "udp",
+ reuseAddress: true,
+ });
+
+ const sent = new Uint8Array([1, 2, 3]);
+ await sender.send(sent, listener1.addr);
+ await Promise.any([listener1.receive(), listener2.receive()]).then(
+ ([recvd, remote]) => {
+ assert(remote.transport === "udp");
+ assertEquals(recvd.length, 3);
+ assertEquals(1, recvd[0]);
+ assertEquals(2, recvd[1]);
+ assertEquals(3, recvd[2]);
+ },
+ );
+ sender.close();
+ listener1.close();
+ listener2.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ function netUdpNoReuseAddr() {
+ let listener1;
+ try {
+ listener1 = Deno.listenDatagram({
+ port: 4001,
+ transport: "udp",
+ reuseAddress: false,
+ });
+ } catch (err) {
+ assert(err);
+ assert(err instanceof Deno.errors.AddrInUse); // AddrInUse from previous test
+ }
+
+ assertThrows(() => {
+ Deno.listenDatagram({
+ port: 4001,
+ transport: "udp",
+ reuseAddress: false,
+ });
+ }, Deno.errors.AddrInUse);
+ if (typeof listener1 !== "undefined") {
+ listener1.close();
+ }
+ },
+);
+
+Deno.test({
+ ignore: Deno.build.os !== "linux",
+ permissions: { net: true },
+}, async function netTcpListenReusePort() {
+ const port = 4003;
+ const listener1 = Deno.listen({ port, reusePort: true });
+ const listener2 = Deno.listen({ port, reusePort: true });
+ let p1;
+ let p2;
+ let listener1Recv = false;
+ let listener2Recv = false;
+ while (!listener1Recv || !listener2Recv) {
+ if (!p1) {
+ p1 = listener1.accept().then((conn) => {
+ conn.close();
+ listener1Recv = true;
+ p1 = undefined;
+ }).catch(() => {});
+ }
+ if (!p2) {
+ p2 = listener2.accept().then((conn) => {
+ conn.close();
+ listener2Recv = true;
+ p2 = undefined;
+ }).catch(() => {});
+ }
+ const conn = await Deno.connect({ port });
+ conn.close();
+ await Promise.race([p1, p2]);
+ }
+ listener1.close();
+ listener2.close();
+});
+
+Deno.test({
+ ignore: Deno.build.os === "linux",
+ permissions: { net: true },
+}, function netTcpListenReusePortDoesNothing() {
+ const listener1 = Deno.listen({ port: 4003, reusePort: true });
+ assertThrows(() => {
+ Deno.listen({ port: 4003, reusePort: true });
+ }, Deno.errors.AddrInUse);
+ listener1.close();
+});
+
+Deno.test({
+ permissions: { net: true },
+}, function netTcpListenDoesNotThrowOnStringPort() {
+ // @ts-ignore String port is not allowed by typing, but it shouldn't throw
+ // for backwards compatibility.
+ const listener = Deno.listen({ hostname: "localhost", port: "0" });
+ listener.close();
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function listenerExplicitResourceManagement() {
+ let done: Promise<Deno.errors.BadResource>;
+
+ {
+ using listener = Deno.listen({ port: listenPort });
+
+ done = assertRejects(
+ () => listener.accept(),
+ Deno.errors.BadResource,
+ );
+ }
+
+ await done;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function listenerExplicitResourceManagementManualClose() {
+ using listener = Deno.listen({ port: listenPort });
+ listener.close();
+ await assertRejects( // definitely closed
+ () => listener.accept(),
+ Deno.errors.BadResource,
+ );
+ // calling [Symbol.dispose] after manual close is a no-op
+ },
+);
diff --git a/tests/unit/network_interfaces_test.ts b/tests/unit/network_interfaces_test.ts
new file mode 100644
index 000000000..160efbfe6
--- /dev/null
+++ b/tests/unit/network_interfaces_test.ts
@@ -0,0 +1,30 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assert } from "./test_util.ts";
+
+Deno.test(
+ {
+ name: "Deno.networkInterfaces",
+ permissions: { sys: ["networkInterfaces"] },
+ },
+ () => {
+ const networkInterfaces = Deno.networkInterfaces();
+ assert(Array.isArray(networkInterfaces));
+ assert(networkInterfaces.length > 0);
+ for (
+ const { name, family, address, netmask, scopeid, cidr, mac }
+ of networkInterfaces
+ ) {
+ assert(typeof name === "string");
+ assert(family === "IPv4" || family === "IPv6");
+ assert(typeof address === "string");
+ assert(typeof netmask === "string");
+ assert(
+ (family === "IPv6" && typeof scopeid === "number") ||
+ (family === "IPv4" && scopeid === null),
+ );
+ assert(typeof cidr === "string");
+ assert(typeof mac === "string");
+ }
+ },
+);
diff --git a/tests/unit/ops_test.ts b/tests/unit/ops_test.ts
new file mode 100644
index 000000000..4a0daa0a5
--- /dev/null
+++ b/tests/unit/ops_test.ts
@@ -0,0 +1,17 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+const EXPECTED_OP_COUNT = 15;
+
+Deno.test(function checkExposedOps() {
+ // @ts-ignore TS doesn't allow to index with symbol
+ const core = Deno[Deno.internal].core;
+ const opNames = Object.keys(core.ops);
+
+ if (opNames.length !== EXPECTED_OP_COUNT) {
+ throw new Error(
+ `Expected ${EXPECTED_OP_COUNT} ops, but got ${opNames.length}:\n${
+ opNames.join("\n")
+ }`,
+ );
+ }
+});
diff --git a/tests/unit/os_test.ts b/tests/unit/os_test.ts
new file mode 100644
index 000000000..e24494854
--- /dev/null
+++ b/tests/unit/os_test.ts
@@ -0,0 +1,304 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertNotEquals,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test({ permissions: { env: true } }, function envSuccess() {
+ Deno.env.set("TEST_VAR", "A");
+ const env = Deno.env.toObject();
+ Deno.env.set("TEST_VAR", "B");
+ assertEquals(env["TEST_VAR"], "A");
+ assertNotEquals(Deno.env.get("TEST_VAR"), env["TEST_VAR"]);
+});
+
+Deno.test({ permissions: { env: true } }, function envNotFound() {
+ const r = Deno.env.get("env_var_does_not_exist!");
+ assertEquals(r, undefined);
+});
+
+Deno.test({ permissions: { env: true } }, function deleteEnv() {
+ Deno.env.set("TEST_VAR", "A");
+ assertEquals(Deno.env.get("TEST_VAR"), "A");
+ assertEquals(Deno.env.delete("TEST_VAR"), undefined);
+ assertEquals(Deno.env.get("TEST_VAR"), undefined);
+});
+
+Deno.test({ permissions: { env: true } }, function hasEnv() {
+ Deno.env.set("TEST_VAR", "A");
+ assert(Deno.env.has("TEST_VAR"));
+ Deno.env.delete("TEST_VAR");
+ assert(!Deno.env.has("TEST_VAR"));
+});
+
+Deno.test({ permissions: { env: true } }, function avoidEmptyNamedEnv() {
+ assertThrows(() => Deno.env.set("", "v"), TypeError);
+ assertThrows(() => Deno.env.set("a=a", "v"), TypeError);
+ assertThrows(() => Deno.env.set("a\0a", "v"), TypeError);
+ assertThrows(() => Deno.env.set("TEST_VAR", "v\0v"), TypeError);
+
+ assertThrows(() => Deno.env.get(""), TypeError);
+ assertThrows(() => Deno.env.get("a=a"), TypeError);
+ assertThrows(() => Deno.env.get("a\0a"), TypeError);
+
+ assertThrows(() => Deno.env.delete(""), TypeError);
+ assertThrows(() => Deno.env.delete("a=a"), TypeError);
+ assertThrows(() => Deno.env.delete("a\0a"), TypeError);
+});
+
+Deno.test({ permissions: { env: false } }, function envPermissionDenied1() {
+ assertThrows(() => {
+ Deno.env.toObject();
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { env: false } }, function envPermissionDenied2() {
+ assertThrows(() => {
+ Deno.env.get("PATH");
+ }, Deno.errors.PermissionDenied);
+});
+
+// This test verifies that on Windows, environment variables are
+// case-insensitive. Case normalization needs be done using the collation
+// that Windows uses, rather than naively using String.toLowerCase().
+Deno.test(
+ {
+ ignore: Deno.build.os !== "windows",
+ permissions: { read: true, env: true, run: true },
+ },
+ async function envCaseInsensitive() {
+ // Utility function that runs a Deno subprocess with the environment
+ // specified in `inputEnv`. The subprocess reads the environment variables
+ // which are in the keys of `expectedEnv` and writes them to stdout as JSON.
+ // It is then verified that these match with the values of `expectedEnv`.
+ const checkChildEnv = async (
+ inputEnv: Record<string, string>,
+ expectedEnv: Record<string, string>,
+ ) => {
+ const src = `
+ console.log(
+ ${JSON.stringify(Object.keys(expectedEnv))}.map(k => Deno.env.get(k))
+ )`;
+ const { success, stdout } = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", src],
+ env: { ...inputEnv, NO_COLOR: "1" },
+ }).output();
+ assertEquals(success, true);
+ const expectedValues = Object.values(expectedEnv);
+ const actualValues = JSON.parse(new TextDecoder().decode(stdout));
+ assertEquals(actualValues, expectedValues);
+ };
+
+ assertEquals(Deno.env.get("path"), Deno.env.get("PATH"));
+ assertEquals(Deno.env.get("Path"), Deno.env.get("PATH"));
+
+ // Check 'foo', 'Foo' and 'Foo' are case folded.
+ await checkChildEnv({ foo: "X" }, { foo: "X", Foo: "X", FOO: "X" });
+
+ // Check that 'µ' and 'Μ' are not case folded.
+ const lc1 = "µ";
+ const uc1 = lc1.toUpperCase();
+ assertNotEquals(lc1, uc1);
+ await checkChildEnv(
+ { [lc1]: "mu", [uc1]: "MU" },
+ { [lc1]: "mu", [uc1]: "MU" },
+ );
+
+ // Check that 'dž' and 'DŽ' are folded, but 'Dž' is preserved.
+ const c2 = "Dž";
+ const lc2 = c2.toLowerCase();
+ const uc2 = c2.toUpperCase();
+ assertNotEquals(c2, lc2);
+ assertNotEquals(c2, uc2);
+ await checkChildEnv(
+ { [c2]: "Dz", [lc2]: "dz" },
+ { [c2]: "Dz", [lc2]: "dz", [uc2]: "dz" },
+ );
+ await checkChildEnv(
+ { [c2]: "Dz", [uc2]: "DZ" },
+ { [c2]: "Dz", [uc2]: "DZ", [lc2]: "DZ" },
+ );
+ },
+);
+
+Deno.test({ permissions: { env: true } }, function envInvalidChars() {
+ assertThrows(() => Deno.env.get(""), TypeError, "Key is an empty string");
+ assertThrows(
+ () => Deno.env.get("\0"),
+ TypeError,
+ 'Key contains invalid characters: "\\0"',
+ );
+ assertThrows(
+ () => Deno.env.get("="),
+ TypeError,
+ 'Key contains invalid characters: "="',
+ );
+ assertThrows(
+ () => Deno.env.set("", "foo"),
+ TypeError,
+ "Key is an empty string",
+ );
+ assertThrows(
+ () => Deno.env.set("\0", "foo"),
+ TypeError,
+ 'Key contains invalid characters: "\\0"',
+ );
+ assertThrows(
+ () => Deno.env.set("=", "foo"),
+ TypeError,
+ 'Key contains invalid characters: "="',
+ );
+ assertThrows(
+ () => Deno.env.set("foo", "\0"),
+ TypeError,
+ 'Value contains invalid characters: "\\0"',
+ );
+});
+
+Deno.test(function osPid() {
+ assertEquals(typeof Deno.pid, "number");
+ assert(Deno.pid > 0);
+});
+
+Deno.test(function osPpid() {
+ assertEquals(typeof Deno.ppid, "number");
+ assert(Deno.ppid > 0);
+});
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function osPpidIsEqualToPidOfParentProcess() {
+ const decoder = new TextDecoder();
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "-p", "--unstable", "Deno.ppid"],
+ env: { NO_COLOR: "true" },
+ }).output();
+
+ const expected = Deno.pid;
+ const actual = parseInt(decoder.decode(stdout));
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test({ permissions: { read: true } }, function execPath() {
+ assertNotEquals(Deno.execPath(), "");
+});
+
+Deno.test({ permissions: { read: false } }, function execPathPerm() {
+ assertThrows(
+ () => {
+ Deno.execPath();
+ },
+ Deno.errors.PermissionDenied,
+ "Requires read access to <exec_path>, run again with the --allow-read flag",
+ );
+});
+
+Deno.test(
+ { permissions: { sys: ["loadavg"] } },
+ function loadavgSuccess() {
+ const load = Deno.loadavg();
+ assertEquals(load.length, 3);
+ },
+);
+
+Deno.test({ permissions: { sys: false } }, function loadavgPerm() {
+ assertThrows(() => {
+ Deno.loadavg();
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { sys: ["hostname"] } },
+ function hostnameDir() {
+ assertNotEquals(Deno.hostname(), "");
+ },
+);
+
+Deno.test(
+ { permissions: { run: [Deno.execPath()], read: true } },
+ // See https://github.com/denoland/deno/issues/16527
+ async function hostnameWithoutOtherNetworkUsages() {
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "-p", "Deno.hostname()"],
+ }).output();
+ const hostname = new TextDecoder().decode(stdout).trim();
+ assert(hostname.length > 0);
+ },
+);
+
+Deno.test({ permissions: { sys: false } }, function hostnamePerm() {
+ assertThrows(() => {
+ Deno.hostname();
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { sys: ["osRelease"] } },
+ function releaseDir() {
+ assertNotEquals(Deno.osRelease(), "");
+ },
+);
+
+Deno.test({ permissions: { sys: false } }, function releasePerm() {
+ assertThrows(() => {
+ Deno.osRelease();
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { sys: ["osUptime"] } }, function osUptime() {
+ const uptime = Deno.osUptime();
+ assert(typeof uptime === "number");
+ assert(uptime > 0);
+});
+
+Deno.test({ permissions: { sys: false } }, function osUptimePerm() {
+ assertThrows(() => {
+ Deno.osUptime();
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { sys: ["systemMemoryInfo"] } },
+ function systemMemoryInfo() {
+ const info = Deno.systemMemoryInfo();
+ assert(info.total >= 0);
+ assert(info.free >= 0);
+ assert(info.available >= 0);
+ assert(info.buffers >= 0);
+ assert(info.cached >= 0);
+ assert(info.swapTotal >= 0);
+ assert(info.swapFree >= 0);
+ },
+);
+
+Deno.test({ permissions: { sys: ["uid"] } }, function getUid() {
+ if (Deno.build.os === "windows") {
+ assertEquals(Deno.uid(), null);
+ } else {
+ const uid = Deno.uid();
+ assert(typeof uid === "number");
+ assert(uid > 0);
+ }
+});
+
+Deno.test({ permissions: { sys: ["gid"] } }, function getGid() {
+ if (Deno.build.os === "windows") {
+ assertEquals(Deno.gid(), null);
+ } else {
+ const gid = Deno.gid();
+ assert(typeof gid === "number");
+ assert(gid > 0);
+ }
+});
+
+Deno.test(function memoryUsage() {
+ const mem = Deno.memoryUsage();
+ assert(typeof mem.rss === "number");
+ assert(typeof mem.heapTotal === "number");
+ assert(typeof mem.heapUsed === "number");
+ assert(typeof mem.external === "number");
+ assert(mem.rss >= mem.heapTotal);
+});
diff --git a/tests/unit/path_from_url_test.ts b/tests/unit/path_from_url_test.ts
new file mode 100644
index 000000000..b3a6406bc
--- /dev/null
+++ b/tests/unit/path_from_url_test.ts
@@ -0,0 +1,41 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assertEquals, assertThrows } from "./test_util.ts";
+
+// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
+const { pathFromURL } = Deno[Deno.internal];
+
+Deno.test(
+ { ignore: Deno.build.os === "windows" },
+ function pathFromURLPosix() {
+ assertEquals(
+ pathFromURL(new URL("file:///test/directory")),
+ "/test/directory",
+ );
+ assertEquals(pathFromURL(new URL("file:///space_ .txt")), "/space_ .txt");
+ assertThrows(() => pathFromURL(new URL("https://deno.land/welcome.ts")));
+ },
+);
+
+Deno.test(
+ { ignore: Deno.build.os !== "windows" },
+ function pathFromURLWin32() {
+ assertEquals(
+ pathFromURL(new URL("file:///c:/windows/test")),
+ "c:\\windows\\test",
+ );
+ assertEquals(
+ pathFromURL(new URL("file:///c:/space_ .txt")),
+ "c:\\space_ .txt",
+ );
+ assertThrows(() => pathFromURL(new URL("https://deno.land/welcome.ts")));
+ /* TODO(ry) Add tests for these situations
+ * ampersand_&.tx file:///D:/weird_names/ampersand_&.txt
+ * at_@.txt file:///D:/weird_names/at_@.txt
+ * emoji_🙃.txt file:///D:/weird_names/emoji_%F0%9F%99%83.txt
+ * percent_%.txt file:///D:/weird_names/percent_%25.txt
+ * pound_#.txt file:///D:/weird_names/pound_%23.txt
+ * swapped_surrogate_pair_��.txt file:///D:/weird_names/swapped_surrogate_pair_%EF%BF%BD%EF%BF%BD.txt
+ */
+ },
+);
diff --git a/tests/unit/performance_test.ts b/tests/unit/performance_test.ts
new file mode 100644
index 000000000..0c9ed21df
--- /dev/null
+++ b/tests/unit/performance_test.ts
@@ -0,0 +1,185 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertNotStrictEquals,
+ assertStringIncludes,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test({ permissions: { hrtime: false } }, async function performanceNow() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const start = performance.now();
+ let totalTime = 0;
+ setTimeout(() => {
+ const end = performance.now();
+ totalTime = end - start;
+ resolve();
+ }, 10);
+ await promise;
+ assert(totalTime >= 10);
+});
+
+Deno.test(function timeOrigin() {
+ const origin = performance.timeOrigin;
+
+ assert(origin > 0);
+ assert(Date.now() >= origin);
+});
+
+Deno.test(function performanceToJSON() {
+ const json = performance.toJSON();
+
+ assert("timeOrigin" in json);
+ assert(json.timeOrigin === performance.timeOrigin);
+ // check there are no other keys
+ assertEquals(Object.keys(json).length, 1);
+});
+
+Deno.test(function performanceMark() {
+ const mark = performance.mark("test");
+ assert(mark instanceof PerformanceMark);
+ assertEquals(mark.detail, null);
+ assertEquals(mark.name, "test");
+ assertEquals(mark.entryType, "mark");
+ assert(mark.startTime > 0);
+ assertEquals(mark.duration, 0);
+ const entries = performance.getEntries();
+ assert(entries[entries.length - 1] === mark);
+ const markEntries = performance.getEntriesByName("test", "mark");
+ assert(markEntries[markEntries.length - 1] === mark);
+});
+
+Deno.test(function performanceMarkDetail() {
+ const detail = { foo: "foo" };
+ const mark = performance.mark("test", { detail });
+ assert(mark instanceof PerformanceMark);
+ assertEquals(mark.detail, { foo: "foo" });
+ assertNotStrictEquals(mark.detail, detail);
+});
+
+Deno.test(function performanceMarkDetailArrayBuffer() {
+ const detail = new ArrayBuffer(10);
+ const mark = performance.mark("test", { detail });
+ assert(mark instanceof PerformanceMark);
+ assertEquals(mark.detail, new ArrayBuffer(10));
+ assertNotStrictEquals(mark.detail, detail);
+});
+
+Deno.test(function performanceMarkDetailSubTypedArray() {
+ class SubUint8Array extends Uint8Array {}
+ const detail = new SubUint8Array([1, 2]);
+ const mark = performance.mark("test", { detail });
+ assert(mark instanceof PerformanceMark);
+ assertEquals(mark.detail, new Uint8Array([1, 2]));
+ assertNotStrictEquals(mark.detail, detail);
+});
+
+Deno.test(function performanceMeasure() {
+ const markName1 = "mark1";
+ const measureName1 = "measure1";
+ const measureName2 = "measure2";
+ const mark1 = performance.mark(markName1);
+ // Measure against the inaccurate-but-known-good wall clock
+ const now = new Date().valueOf();
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ try {
+ const later = new Date().valueOf();
+ const measure1 = performance.measure(measureName1, markName1);
+ const measure2 = performance.measure(
+ measureName2,
+ undefined,
+ markName1,
+ );
+ assert(measure1 instanceof PerformanceMeasure);
+ assertEquals(measure1.detail, null);
+ assertEquals(measure1.name, measureName1);
+ assertEquals(measure1.entryType, "measure");
+ assert(measure1.startTime > 0);
+ assertEquals(measure2.startTime, 0);
+ assertEquals(mark1.startTime, measure1.startTime);
+ assertEquals(mark1.startTime, measure2.duration);
+ assert(
+ measure1.duration >= 100,
+ `duration below 100ms: ${measure1.duration}`,
+ );
+ assert(
+ measure1.duration < (later - now) * 1.50,
+ `duration exceeds 150% of wallclock time: ${measure1.duration}ms vs ${
+ later - now
+ }ms`,
+ );
+ const entries = performance.getEntries();
+ assert(entries[entries.length - 1] === measure2);
+ const entriesByName = performance.getEntriesByName(
+ measureName1,
+ "measure",
+ );
+ assert(entriesByName[entriesByName.length - 1] === measure1);
+ const measureEntries = performance.getEntriesByType("measure");
+ assert(measureEntries[measureEntries.length - 1] === measure2);
+ } catch (e) {
+ return reject(e);
+ }
+ resolve();
+ }, 100);
+ });
+});
+
+Deno.test(function performanceCustomInspectFunction() {
+ assertStringIncludes(Deno.inspect(performance), "Performance");
+ assertStringIncludes(
+ Deno.inspect(Performance.prototype),
+ "Performance",
+ );
+});
+
+Deno.test(function performanceMarkCustomInspectFunction() {
+ const mark1 = performance.mark("mark1");
+ assertStringIncludes(Deno.inspect(mark1), "PerformanceMark");
+ assertStringIncludes(
+ Deno.inspect(PerformanceMark.prototype),
+ "PerformanceMark",
+ );
+});
+
+Deno.test(function performanceMeasureCustomInspectFunction() {
+ const measure1 = performance.measure("measure1");
+ assertStringIncludes(Deno.inspect(measure1), "PerformanceMeasure");
+ assertStringIncludes(
+ Deno.inspect(PerformanceMeasure.prototype),
+ "PerformanceMeasure",
+ );
+});
+
+Deno.test(function performanceIllegalConstructor() {
+ assertThrows(() => new Performance(), TypeError, "Illegal constructor");
+ assertEquals(Performance.length, 0);
+});
+
+Deno.test(function performanceEntryIllegalConstructor() {
+ assertThrows(() => new PerformanceEntry(), TypeError, "Illegal constructor");
+ assertEquals(PerformanceEntry.length, 0);
+});
+
+Deno.test(function performanceMeasureIllegalConstructor() {
+ assertThrows(
+ () => new PerformanceMeasure(),
+ TypeError,
+ "Illegal constructor",
+ );
+});
+
+Deno.test(function performanceIsEventTarget() {
+ assert(performance instanceof EventTarget);
+
+ return new Promise((resolve) => {
+ const handler = () => {
+ resolve();
+ };
+
+ performance.addEventListener("test", handler, { once: true });
+ performance.dispatchEvent(new Event("test"));
+ });
+});
diff --git a/tests/unit/permissions_test.ts b/tests/unit/permissions_test.ts
new file mode 100644
index 000000000..4dab0696a
--- /dev/null
+++ b/tests/unit/permissions_test.ts
@@ -0,0 +1,202 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test(async function permissionInvalidName() {
+ await assertRejects(async () => {
+ // deno-lint-ignore no-explicit-any
+ await Deno.permissions.query({ name: "foo" as any });
+ }, TypeError);
+});
+
+Deno.test(function permissionInvalidNameSync() {
+ assertThrows(() => {
+ // deno-lint-ignore no-explicit-any
+ Deno.permissions.querySync({ name: "foo" as any });
+ }, TypeError);
+});
+
+Deno.test(async function permissionNetInvalidHost() {
+ await assertRejects(async () => {
+ await Deno.permissions.query({ name: "net", host: ":" });
+ }, URIError);
+});
+
+Deno.test(function permissionNetInvalidHostSync() {
+ assertThrows(() => {
+ Deno.permissions.querySync({ name: "net", host: ":" });
+ }, URIError);
+});
+
+Deno.test(async function permissionSysValidKind() {
+ await Deno.permissions.query({ name: "sys", kind: "loadavg" });
+ await Deno.permissions.query({ name: "sys", kind: "osRelease" });
+ await Deno.permissions.query({ name: "sys", kind: "osUptime" });
+ await Deno.permissions.query({ name: "sys", kind: "networkInterfaces" });
+ await Deno.permissions.query({ name: "sys", kind: "systemMemoryInfo" });
+ await Deno.permissions.query({ name: "sys", kind: "hostname" });
+ await Deno.permissions.query({ name: "sys", kind: "uid" });
+ await Deno.permissions.query({ name: "sys", kind: "gid" });
+ await Deno.permissions.query({ name: "sys", kind: "cpus" });
+});
+
+Deno.test(function permissionSysValidKindSync() {
+ Deno.permissions.querySync({ name: "sys", kind: "loadavg" });
+ Deno.permissions.querySync({ name: "sys", kind: "osRelease" });
+ Deno.permissions.querySync({ name: "sys", kind: "networkInterfaces" });
+ Deno.permissions.querySync({ name: "sys", kind: "systemMemoryInfo" });
+ Deno.permissions.querySync({ name: "sys", kind: "hostname" });
+ Deno.permissions.querySync({ name: "sys", kind: "uid" });
+ Deno.permissions.querySync({ name: "sys", kind: "gid" });
+ Deno.permissions.querySync({ name: "sys", kind: "cpus" });
+});
+
+Deno.test(async function permissionSysInvalidKind() {
+ await assertRejects(async () => {
+ // deno-lint-ignore no-explicit-any
+ await Deno.permissions.query({ name: "sys", kind: "abc" as any });
+ }, TypeError);
+});
+
+Deno.test(function permissionSysInvalidKindSync() {
+ assertThrows(() => {
+ // deno-lint-ignore no-explicit-any
+ Deno.permissions.querySync({ name: "sys", kind: "abc" as any });
+ }, TypeError);
+});
+
+Deno.test(async function permissionQueryReturnsEventTarget() {
+ const status = await Deno.permissions.query({ name: "hrtime" });
+ assert(["granted", "denied", "prompt"].includes(status.state));
+ let called = false;
+ status.addEventListener("change", () => {
+ called = true;
+ });
+ status.dispatchEvent(new Event("change"));
+ assert(called);
+ assert(status === (await Deno.permissions.query({ name: "hrtime" })));
+});
+
+Deno.test(function permissionQueryReturnsEventTargetSync() {
+ const status = Deno.permissions.querySync({ name: "hrtime" });
+ assert(["granted", "denied", "prompt"].includes(status.state));
+ let called = false;
+ status.addEventListener("change", () => {
+ called = true;
+ });
+ status.dispatchEvent(new Event("change"));
+ assert(called);
+ assert(status === Deno.permissions.querySync({ name: "hrtime" }));
+});
+
+Deno.test(async function permissionQueryForReadReturnsSameStatus() {
+ const status1 = await Deno.permissions.query({
+ name: "read",
+ path: ".",
+ });
+ const status2 = await Deno.permissions.query({
+ name: "read",
+ path: ".",
+ });
+ assert(status1 === status2);
+});
+
+Deno.test(function permissionQueryForReadReturnsSameStatusSync() {
+ const status1 = Deno.permissions.querySync({
+ name: "read",
+ path: ".",
+ });
+ const status2 = Deno.permissions.querySync({
+ name: "read",
+ path: ".",
+ });
+ assert(status1 === status2);
+});
+
+Deno.test(function permissionsIllegalConstructor() {
+ assertThrows(() => new Deno.Permissions(), TypeError, "Illegal constructor.");
+ assertEquals(Deno.Permissions.length, 0);
+});
+
+Deno.test(function permissionStatusIllegalConstructor() {
+ assertThrows(
+ () => new Deno.PermissionStatus(),
+ TypeError,
+ "Illegal constructor.",
+ );
+ assertEquals(Deno.PermissionStatus.length, 0);
+});
+
+// Regression test for https://github.com/denoland/deno/issues/17020
+Deno.test(async function permissionURL() {
+ const path = new URL(".", import.meta.url);
+
+ await Deno.permissions.query({ name: "read", path });
+ await Deno.permissions.query({ name: "write", path });
+ await Deno.permissions.query({ name: "ffi", path });
+ await Deno.permissions.query({ name: "run", command: path });
+});
+
+Deno.test(function permissionURLSync() {
+ Deno.permissions.querySync({
+ name: "read",
+ path: new URL(".", import.meta.url),
+ });
+ Deno.permissions.querySync({
+ name: "write",
+ path: new URL(".", import.meta.url),
+ });
+ Deno.permissions.querySync({
+ name: "run",
+ command: new URL(".", import.meta.url),
+ });
+});
+
+Deno.test(async function permissionDescriptorValidation() {
+ for (const value of [undefined, null, {}]) {
+ for (const method of ["query", "request", "revoke"]) {
+ await assertRejects(
+ async () => {
+ // deno-lint-ignore no-explicit-any
+ await (Deno.permissions as any)[method](value as any);
+ },
+ TypeError,
+ '"undefined" is not a valid permission name',
+ );
+ }
+ }
+});
+
+Deno.test(function permissionDescriptorValidationSync() {
+ for (const value of [undefined, null, {}]) {
+ for (const method of ["querySync", "revokeSync", "requestSync"]) {
+ assertThrows(
+ () => {
+ // deno-lint-ignore no-explicit-any
+ (Deno.permissions as any)[method](value as any);
+ },
+ TypeError,
+ '"undefined" is not a valid permission name',
+ );
+ }
+ }
+});
+
+// Regression test for https://github.com/denoland/deno/issues/15894.
+Deno.test(async function permissionStatusObjectsNotEqual() {
+ assert(
+ await Deno.permissions.query({ name: "env", variable: "A" }) !=
+ await Deno.permissions.query({ name: "env", variable: "B" }),
+ );
+});
+
+Deno.test(function permissionStatusObjectsNotEqualSync() {
+ assert(
+ Deno.permissions.querySync({ name: "env", variable: "A" }) !=
+ Deno.permissions.querySync({ name: "env", variable: "B" }),
+ );
+});
diff --git a/tests/unit/process_test.ts b/tests/unit/process_test.ts
new file mode 100644
index 000000000..0cc4e99aa
--- /dev/null
+++ b/tests/unit/process_test.ts
@@ -0,0 +1,689 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertStrictEquals,
+ assertStringIncludes,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test(
+ { permissions: { read: true, run: false } },
+ function runPermissions() {
+ assertThrows(() => {
+ // deno-lint-ignore no-deprecated-deno-api
+ Deno.run({
+ cmd: [Deno.execPath(), "eval", "console.log('hello world')"],
+ });
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runSuccess() {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ // freeze the array to ensure it's not modified
+ cmd: Object.freeze([
+ Deno.execPath(),
+ "eval",
+ "console.log('hello world')",
+ ]),
+ stdout: "piped",
+ stderr: "null",
+ });
+ const status = await p.status();
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, undefined);
+ p.stdout.close();
+ p.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runUrl() {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ new URL(`file:///${Deno.execPath()}`),
+ "eval",
+ "console.log('hello world')",
+ ],
+ stdout: "piped",
+ stderr: "null",
+ });
+ const status = await p.status();
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, undefined);
+ p.stdout.close();
+ p.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runStdinRid0(): Promise<
+ void
+ > {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [Deno.execPath(), "eval", "console.log('hello world')"],
+ stdin: 0,
+ stdout: "piped",
+ stderr: "null",
+ });
+ const status = await p.status();
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, undefined);
+ p.stdout.close();
+ p.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function runInvalidStdio() {
+ assertThrows(() =>
+ // deno-lint-ignore no-deprecated-deno-api
+ Deno.run({
+ cmd: [Deno.execPath(), "eval", "console.log('hello world')"],
+ // @ts-expect-error because Deno.run should throw on invalid stdin.
+ stdin: "a",
+ })
+ );
+ assertThrows(() =>
+ // deno-lint-ignore no-deprecated-deno-api
+ Deno.run({
+ cmd: [Deno.execPath(), "eval", "console.log('hello world')"],
+ // @ts-expect-error because Deno.run should throw on invalid stdout.
+ stdout: "b",
+ })
+ );
+ assertThrows(() =>
+ // deno-lint-ignore no-deprecated-deno-api
+ Deno.run({
+ cmd: [Deno.execPath(), "eval", "console.log('hello world')"],
+ // @ts-expect-error because Deno.run should throw on invalid stderr.
+ stderr: "c",
+ })
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runCommandFailedWithCode() {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [Deno.execPath(), "eval", "Deno.exit(41 + 1)"],
+ });
+ const status = await p.status();
+ assertEquals(status.success, false);
+ assertEquals(status.code, 42);
+ assertEquals(status.signal, undefined);
+ p.close();
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ },
+ async function runCommandFailedWithSignal() {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ "Deno.kill(Deno.pid, 'SIGKILL')",
+ ],
+ });
+ const status = await p.status();
+ assertEquals(status.success, false);
+ if (Deno.build.os === "windows") {
+ assertEquals(status.code, 1);
+ assertEquals(status.signal, undefined);
+ } else {
+ assertEquals(status.code, 128 + 9);
+ assertEquals(status.signal, 9);
+ }
+ p.close();
+ },
+);
+
+Deno.test({ permissions: { run: true } }, function runNotFound() {
+ let error;
+ try {
+ // deno-lint-ignore no-deprecated-deno-api
+ Deno.run({ cmd: ["this file hopefully doesn't exist"] });
+ } catch (e) {
+ error = e;
+ }
+ assert(error !== undefined);
+ assert(error instanceof Deno.errors.NotFound);
+});
+
+Deno.test(
+ { permissions: { write: true, run: true, read: true } },
+ async function runWithCwdIsAsync() {
+ const enc = new TextEncoder();
+ const cwd = await Deno.makeTempDir({ prefix: "deno_command_test" });
+
+ const exitCodeFile = "deno_was_here";
+ const programFile = "poll_exit.ts";
+ const program = `
+async function tryExit() {
+ try {
+ const code = parseInt(await Deno.readTextFile("${exitCodeFile}"));
+ Deno.exit(code);
+ } catch {
+ // Retry if we got here before deno wrote the file.
+ setTimeout(tryExit, 0.01);
+ }
+}
+
+tryExit();
+`;
+
+ Deno.writeFileSync(`${cwd}/${programFile}`, enc.encode(program));
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cwd,
+ cmd: [Deno.execPath(), "run", "--allow-read", programFile],
+ });
+
+ // Write the expected exit code *after* starting deno.
+ // This is how we verify that `run()` is actually asynchronous.
+ const code = 84;
+ Deno.writeFileSync(`${cwd}/${exitCodeFile}`, enc.encode(`${code}`));
+
+ const status = await p.status();
+ assertEquals(status.success, false);
+ assertEquals(status.code, code);
+ assertEquals(status.signal, undefined);
+ p.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runStdinPiped(): Promise<
+ void
+ > {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ "if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')",
+ ],
+ stdin: "piped",
+ });
+ assert(p.stdin);
+ assert(!p.stdout);
+ assert(!p.stderr);
+
+ const msg = new TextEncoder().encode("hello");
+ const n = await p.stdin.write(msg);
+ assertEquals(n, msg.byteLength);
+
+ p.stdin.close();
+
+ const status = await p.status();
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, undefined);
+ p.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runStdoutPiped(): Promise<
+ void
+ > {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ "await Deno.stdout.write(new TextEncoder().encode('hello'))",
+ ],
+ stdout: "piped",
+ });
+ assert(!p.stdin);
+ assert(!p.stderr);
+
+ const data = new Uint8Array(10);
+ let r = await p.stdout.read(data);
+ if (r === null) {
+ throw new Error("p.stdout.read(...) should not be null");
+ }
+ assertEquals(r, 5);
+ const s = new TextDecoder().decode(data.subarray(0, r));
+ assertEquals(s, "hello");
+ r = await p.stdout.read(data);
+ assertEquals(r, null);
+ p.stdout.close();
+
+ const status = await p.status();
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, undefined);
+ p.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runStderrPiped(): Promise<
+ void
+ > {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ "await Deno.stderr.write(new TextEncoder().encode('hello'))",
+ ],
+ stderr: "piped",
+ });
+ assert(!p.stdin);
+ assert(!p.stdout);
+
+ const data = new Uint8Array(10);
+ let r = await p.stderr.read(data);
+ if (r === null) {
+ throw new Error("p.stderr.read should not return null here");
+ }
+ assertEquals(r, 5);
+ const s = new TextDecoder().decode(data.subarray(0, r));
+ assertEquals(s, "hello");
+ r = await p.stderr.read(data);
+ assertEquals(r, null);
+ p.stderr!.close();
+
+ const status = await p.status();
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, undefined);
+ p.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runOutput() {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ "await Deno.stdout.write(new TextEncoder().encode('hello'))",
+ ],
+ stdout: "piped",
+ });
+ const output = await p.output();
+ const s = new TextDecoder().decode(output);
+ assertEquals(s, "hello");
+ p.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runStderrOutput(): Promise<
+ void
+ > {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ "await Deno.stderr.write(new TextEncoder().encode('error'))",
+ ],
+ stderr: "piped",
+ });
+ const error = await p.stderrOutput();
+ const s = new TextDecoder().decode(error);
+ assertEquals(s, "error");
+ p.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, write: true, read: true } },
+ async function runRedirectStdoutStderr() {
+ const tempDir = await Deno.makeTempDir();
+ const fileName = tempDir + "/redirected_stdio.txt";
+ using file = await Deno.open(fileName, {
+ create: true,
+ write: true,
+ });
+
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ "Deno.stderr.write(new TextEncoder().encode('error\\n')); Deno.stdout.write(new TextEncoder().encode('output\\n'));",
+ ],
+ stdout: file.rid,
+ stderr: file.rid,
+ });
+
+ await p.status();
+ p.close();
+
+ const fileContents = await Deno.readFile(fileName);
+ const decoder = new TextDecoder();
+ const text = decoder.decode(fileContents);
+
+ assertStringIncludes(text, "error");
+ assertStringIncludes(text, "output");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, write: true, read: true } },
+ async function runRedirectStdin() {
+ const tempDir = await Deno.makeTempDir();
+ const fileName = tempDir + "/redirected_stdio.txt";
+ await Deno.writeTextFile(fileName, "hello");
+ using file = await Deno.open(fileName);
+
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ "if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')",
+ ],
+ stdin: file.rid,
+ });
+
+ const status = await p.status();
+ assertEquals(status.code, 0);
+ p.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runEnv() {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ "Deno.stdout.write(new TextEncoder().encode(Deno.env.get('FOO') + Deno.env.get('BAR')))",
+ ],
+ env: {
+ FOO: "0123",
+ BAR: "4567",
+ },
+ stdout: "piped",
+ });
+ const output = await p.output();
+ const s = new TextDecoder().decode(output);
+ assertEquals(s, "01234567");
+ p.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runClose() {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ "setTimeout(() => Deno.stdout.write(new TextEncoder().encode('error')), 10000)",
+ ],
+ stderr: "piped",
+ });
+ assert(!p.stdin);
+ assert(!p.stdout);
+
+ p.close();
+
+ const data = new Uint8Array(10);
+ const r = await p.stderr.read(data);
+ assertEquals(r, null);
+ p.stderr.close();
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function runKillAfterStatus() {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [Deno.execPath(), "eval", 'console.log("hello")'],
+ });
+ await p.status();
+
+ let error = null;
+ try {
+ p.kill("SIGTERM");
+ } catch (e) {
+ error = e;
+ }
+
+ assert(
+ error instanceof Deno.errors.NotFound ||
+ // On Windows, the underlying Windows API may return
+ // `ERROR_ACCESS_DENIED` when the process has exited, but hasn't been
+ // completely cleaned up yet and its `pid` is still valid.
+ (Deno.build.os === "windows" &&
+ error instanceof Deno.errors.PermissionDenied),
+ );
+
+ p.close();
+ },
+);
+
+Deno.test({ permissions: { run: false } }, function killPermissions() {
+ assertThrows(() => {
+ // Unlike the other test cases, we don't have permission to spawn a
+ // subprocess we can safely kill. Instead we send SIGCONT to the current
+ // process - assuming that Deno does not have a special handler set for it
+ // and will just continue even if a signal is erroneously sent.
+ Deno.kill(Deno.pid, "SIGCONT");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { ignore: Deno.build.os !== "windows", permissions: { run: true } },
+ function negativePidInvalidWindows() {
+ assertThrows(() => {
+ Deno.kill(-1, "SIGTERM");
+ }, TypeError);
+ },
+);
+
+Deno.test(
+ { ignore: Deno.build.os !== "windows", permissions: { run: true } },
+ function invalidSignalNameWindows() {
+ assertThrows(() => {
+ Deno.kill(Deno.pid, "SIGUSR1");
+ }, TypeError);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function killSuccess() {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [Deno.execPath(), "eval", "setTimeout(() => {}, 10000)"],
+ });
+
+ try {
+ Deno.kill(p.pid, "SIGKILL");
+ const status = await p.status();
+
+ assertEquals(status.success, false);
+ if (Deno.build.os === "windows") {
+ assertEquals(status.code, 1);
+ assertEquals(status.signal, undefined);
+ } else {
+ assertEquals(status.code, 137);
+ assertEquals(status.signal, 9);
+ }
+ } finally {
+ p.close();
+ }
+ },
+);
+
+Deno.test({ permissions: { run: true, read: true } }, function killFailed() {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [Deno.execPath(), "eval", "setTimeout(() => {}, 10000)"],
+ });
+ assert(!p.stdin);
+ assert(!p.stdout);
+
+ assertThrows(() => {
+ // @ts-expect-error testing runtime error of bad signal
+ Deno.kill(p.pid, "foobar");
+ }, TypeError);
+
+ p.close();
+});
+
+Deno.test(
+ { permissions: { run: true, read: true, env: true } },
+ async function clearEnv(): Promise<void> {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ "-p",
+ "JSON.stringify(Deno.env.toObject())",
+ ],
+ stdout: "piped",
+ clearEnv: true,
+ env: {
+ FOO: "23147",
+ },
+ });
+
+ const obj = JSON.parse(new TextDecoder().decode(await p.output()));
+
+ // can't check for object equality because the OS may set additional env
+ // vars for processes, so we check if PATH isn't present as that is a common
+ // env var across OS's and isn't set for processes.
+ assertEquals(obj.FOO, "23147");
+ assert(!("PATH" in obj));
+
+ p.close();
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ ignore: Deno.build.os === "windows",
+ },
+ async function uid(): Promise<void> {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ "id",
+ "-u",
+ ],
+ stdout: "piped",
+ });
+
+ const currentUid = new TextDecoder().decode(await p.output());
+ p.close();
+
+ if (currentUid !== "0") {
+ assertThrows(() => {
+ // deno-lint-ignore no-deprecated-deno-api
+ Deno.run({
+ cmd: [
+ "echo",
+ "fhqwhgads",
+ ],
+ uid: 0,
+ });
+ }, Deno.errors.PermissionDenied);
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ ignore: Deno.build.os === "windows",
+ },
+ async function gid(): Promise<void> {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ "id",
+ "-g",
+ ],
+ stdout: "piped",
+ });
+
+ const currentGid = new TextDecoder().decode(await p.output());
+ p.close();
+
+ if (currentGid !== "0") {
+ assertThrows(() => {
+ // deno-lint-ignore no-deprecated-deno-api
+ Deno.run({
+ cmd: [
+ "echo",
+ "fhqwhgads",
+ ],
+ gid: 0,
+ });
+ }, Deno.errors.PermissionDenied);
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true, write: true },
+ ignore: Deno.build.os === "windows",
+ },
+ async function non_existent_cwd(): Promise<void> {
+ // deno-lint-ignore no-deprecated-deno-api
+ const p = Deno.run({
+ cmd: [
+ Deno.execPath(),
+ "eval",
+ `const dir = Deno.makeTempDirSync();
+ Deno.chdir(dir);
+ Deno.removeSync(dir);
+ const p = Deno.run({cmd:[Deno.execPath(), "eval", "console.log(1);"]});
+ const { code } = await p.status();
+ p.close();
+ Deno.exit(code);
+ `,
+ ],
+ stdout: "piped",
+ stderr: "piped",
+ });
+
+ const { code } = await p.status();
+ const stderr = new TextDecoder().decode(await p.stderrOutput());
+ p.close();
+ p.stdout.close();
+ assertStrictEquals(code, 1);
+ assertStringIncludes(stderr, "Failed getting cwd.");
+ },
+);
diff --git a/tests/unit/progressevent_test.ts b/tests/unit/progressevent_test.ts
new file mode 100644
index 000000000..809c2ad39
--- /dev/null
+++ b/tests/unit/progressevent_test.ts
@@ -0,0 +1,18 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+Deno.test(function progressEventConstruct() {
+ const progressEventDefs = new ProgressEvent("progressEventType", {});
+ assertEquals(progressEventDefs.lengthComputable, false);
+ assertEquals(progressEventDefs.loaded, 0);
+ assertEquals(progressEventDefs.total, 0);
+
+ const progressEvent = new ProgressEvent("progressEventType", {
+ lengthComputable: true,
+ loaded: 123,
+ total: 456,
+ });
+ assertEquals(progressEvent.lengthComputable, true);
+ assertEquals(progressEvent.loaded, 123);
+ assertEquals(progressEvent.total, 456);
+});
diff --git a/tests/unit/promise_hooks_test.ts b/tests/unit/promise_hooks_test.ts
new file mode 100644
index 000000000..f7c44155d
--- /dev/null
+++ b/tests/unit/promise_hooks_test.ts
@@ -0,0 +1,109 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assertEquals } from "./test_util.ts";
+
+function monitorPromises(outputArray: string[]) {
+ const promiseIds = new Map<Promise<unknown>, string>();
+
+ function identify(promise: Promise<unknown>) {
+ if (!promiseIds.has(promise)) {
+ promiseIds.set(promise, "p" + (promiseIds.size + 1));
+ }
+ return promiseIds.get(promise);
+ }
+
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ Deno[Deno.internal].core.setPromiseHooks(
+ (promise: Promise<unknown>, parentPromise?: Promise<unknown>) => {
+ outputArray.push(
+ `init ${identify(promise)}` +
+ (parentPromise ? ` from ${identify(parentPromise)}` : ``),
+ );
+ },
+ (promise: Promise<unknown>) => {
+ outputArray.push(`before ${identify(promise)}`);
+ },
+ (promise: Promise<unknown>) => {
+ outputArray.push(`after ${identify(promise)}`);
+ },
+ (promise: Promise<unknown>) => {
+ outputArray.push(`resolve ${identify(promise)}`);
+ },
+ );
+}
+
+Deno.test(async function promiseHookBasic() {
+ // Bogus await here to ensure any pending promise resolution from the
+ // test runtime has a chance to run and avoid contaminating our results.
+ await Promise.resolve(null);
+
+ const hookResults: string[] = [];
+ monitorPromises(hookResults);
+
+ async function asyncFn() {
+ await Promise.resolve(15);
+ await Promise.resolve(20);
+ Promise.reject(new Error()).catch(() => {});
+ }
+
+ // The function above is equivalent to:
+ // function asyncFn() {
+ // return new Promise(resolve => {
+ // Promise.resolve(15).then(() => {
+ // Promise.resolve(20).then(() => {
+ // Promise.reject(new Error()).catch(() => {});
+ // resolve();
+ // });
+ // });
+ // });
+ // }
+
+ await asyncFn();
+
+ assertEquals(hookResults, [
+ "init p1", // Creates the promise representing the return of `asyncFn()`.
+ "init p2", // Creates the promise representing `Promise.resolve(15)`.
+ "resolve p2", // The previous promise resolves to `15` immediately.
+ "init p3 from p2", // Creates the promise that is resolved after the first `await` of the function. Equivalent to `p2.then(...)`.
+ "init p4 from p1", // The resolution above gives time for other pending code to run. Creates the promise that is resolved
+ // from the `await` at `await asyncFn()`, the last code to run. Equivalent to `asyncFn().then(...)`.
+ "before p3", // Begins executing the code after `await Promise.resolve(15)`.
+ "init p5", // Creates the promise representing `Promise.resolve(20)`.
+ "resolve p5", // The previous promise resolves to `20` immediately.
+ "init p6 from p5", // Creates the promise that is resolved after the second `await` of the function. Equivalent to `p5.then(...)`.
+ "resolve p3", // The promise representing the code right after the first await is marked as resolved.
+ "after p3", // We are now after the resolution code of the promise above.
+ "before p6", // Begins executing the code after `await Promise.resolve(20)`.
+ "init p7", // Creates a new promise representing `Promise.reject(new Error())`.
+ "resolve p7", // This promise is "resolved" immediately to a rejection with an error instance.
+ "init p8 from p7", // Creates a new promise for the `.catch` of the previous promise.
+ "resolve p1", // At this point the promise of the function is resolved.
+ "resolve p6", // This concludes the resolution of the code after `await Promise.resolve(20)`.
+ "after p6", // We are now after the resolution code of the promise above.
+ "before p8", // The `.catch` block is pending execution, it begins to execute.
+ "resolve p8", // It does nothing and resolves to `undefined`.
+ "after p8", // We are after the resolution of the `.catch` block.
+ "before p4", // Now we begin the execution of the code that happens after `await asyncFn();`.
+ ]);
+});
+
+Deno.test(async function promiseHookMultipleConsumers() {
+ const hookResultsFirstConsumer: string[] = [];
+ const hookResultsSecondConsumer: string[] = [];
+
+ monitorPromises(hookResultsFirstConsumer);
+ monitorPromises(hookResultsSecondConsumer);
+
+ async function asyncFn() {
+ await Promise.resolve(15);
+ await Promise.resolve(20);
+ Promise.reject(new Error()).catch(() => {});
+ }
+ await asyncFn();
+
+ // Two invocations of `setPromiseHooks` should yield the exact same results, in the same order.
+ assertEquals(
+ hookResultsFirstConsumer,
+ hookResultsSecondConsumer,
+ );
+});
diff --git a/tests/unit/read_dir_test.ts b/tests/unit/read_dir_test.ts
new file mode 100644
index 000000000..cba9647e5
--- /dev/null
+++ b/tests/unit/read_dir_test.ts
@@ -0,0 +1,113 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+ pathToAbsoluteFileUrl,
+} from "./test_util.ts";
+
+function assertSameContent(files: Deno.DirEntry[]) {
+ let counter = 0;
+
+ for (const entry of files) {
+ if (entry.name === "subdir") {
+ assert(entry.isDirectory);
+ counter++;
+ }
+ }
+
+ assertEquals(counter, 1);
+}
+
+Deno.test({ permissions: { read: true } }, function readDirSyncSuccess() {
+ const files = [...Deno.readDirSync("tests/testdata")];
+ assertSameContent(files);
+});
+
+Deno.test({ permissions: { read: true } }, function readDirSyncWithUrl() {
+ const files = [
+ ...Deno.readDirSync(pathToAbsoluteFileUrl("tests/testdata")),
+ ];
+ assertSameContent(files);
+});
+
+Deno.test({ permissions: { read: false } }, function readDirSyncPerm() {
+ assertThrows(() => {
+ Deno.readDirSync("tests/");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, function readDirSyncNotDir() {
+ assertThrows(
+ () => {
+ Deno.readDirSync("tests/testdata/assets/fixture.json");
+ },
+ Error,
+ `readdir 'tests/testdata/assets/fixture.json'`,
+ );
+});
+
+Deno.test({ permissions: { read: true } }, function readDirSyncNotFound() {
+ assertThrows(
+ () => {
+ Deno.readDirSync("bad_dir_name");
+ },
+ Deno.errors.NotFound,
+ `readdir 'bad_dir_name'`,
+ );
+});
+
+Deno.test({ permissions: { read: true } }, async function readDirSuccess() {
+ const files = [];
+ for await (const dirEntry of Deno.readDir("tests/testdata")) {
+ files.push(dirEntry);
+ }
+ assertSameContent(files);
+});
+
+Deno.test({ permissions: { read: true } }, async function readDirWithUrl() {
+ const files = [];
+ for await (
+ const dirEntry of Deno.readDir(pathToAbsoluteFileUrl("tests/testdata"))
+ ) {
+ files.push(dirEntry);
+ }
+ assertSameContent(files);
+});
+
+Deno.test({ permissions: { read: false } }, async function readDirPerm() {
+ await assertRejects(async () => {
+ await Deno.readDir("tests/")[Symbol.asyncIterator]().next();
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { read: true }, ignore: Deno.build.os == "windows" },
+ async function readDirDevFd(): Promise<
+ void
+ > {
+ for await (const _ of Deno.readDir("/dev/fd")) {
+ // We don't actually care whats in here; just that we don't panic on non regular entries
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true }, ignore: Deno.build.os == "windows" },
+ function readDirDevFdSync() {
+ for (const _ of Deno.readDirSync("/dev/fd")) {
+ // We don't actually care whats in here; just that we don't panic on non regular file entries
+ }
+ },
+);
+
+Deno.test({ permissions: { read: true } }, async function readDirNotFound() {
+ await assertRejects(
+ async () => {
+ await Deno.readDir("bad_dir_name")[Symbol.asyncIterator]().next();
+ },
+ Deno.errors.NotFound,
+ `readdir 'bad_dir_name'`,
+ );
+});
diff --git a/tests/unit/read_file_test.ts b/tests/unit/read_file_test.ts
new file mode 100644
index 000000000..bfb3b5085
--- /dev/null
+++ b/tests/unit/read_file_test.ts
@@ -0,0 +1,182 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+ pathToAbsoluteFileUrl,
+ unreachable,
+} from "./test_util.ts";
+
+Deno.test({ permissions: { read: true } }, function readFileSyncSuccess() {
+ const data = Deno.readFileSync("tests/testdata/assets/fixture.json");
+ assert(data.byteLength > 0);
+ const decoder = new TextDecoder("utf-8");
+ const json = decoder.decode(data);
+ const pkg = JSON.parse(json);
+ assertEquals(pkg.name, "deno");
+});
+
+Deno.test({ permissions: { read: true } }, function readFileSyncUrl() {
+ const data = Deno.readFileSync(
+ pathToAbsoluteFileUrl("tests/testdata/assets/fixture.json"),
+ );
+ assert(data.byteLength > 0);
+ const decoder = new TextDecoder("utf-8");
+ const json = decoder.decode(data);
+ const pkg = JSON.parse(json);
+ assertEquals(pkg.name, "deno");
+});
+
+Deno.test({ permissions: { read: false } }, function readFileSyncPerm() {
+ assertThrows(() => {
+ Deno.readFileSync("tests/testdata/assets/fixture.json");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, function readFileSyncNotFound() {
+ assertThrows(() => {
+ Deno.readFileSync("bad_filename");
+ }, Deno.errors.NotFound);
+});
+
+Deno.test({ permissions: { read: true } }, async function readFileUrl() {
+ const data = await Deno.readFile(
+ pathToAbsoluteFileUrl("tests/testdata/assets/fixture.json"),
+ );
+ assert(data.byteLength > 0);
+ const decoder = new TextDecoder("utf-8");
+ const json = decoder.decode(data);
+ const pkg = JSON.parse(json);
+ assertEquals(pkg.name, "deno");
+});
+
+Deno.test({ permissions: { read: true } }, async function readFileSuccess() {
+ const data = await Deno.readFile("tests/testdata/assets/fixture.json");
+ assert(data.byteLength > 0);
+ const decoder = new TextDecoder("utf-8");
+ const json = decoder.decode(data);
+ const pkg = JSON.parse(json);
+ assertEquals(pkg.name, "deno");
+});
+
+Deno.test({ permissions: { read: false } }, async function readFilePerm() {
+ await assertRejects(async () => {
+ await Deno.readFile("tests/testdata/assets/fixture.json");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, function readFileSyncLoop() {
+ for (let i = 0; i < 256; i++) {
+ Deno.readFileSync("tests/testdata/assets/fixture.json");
+ }
+});
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readFileDoesNotLeakResources() {
+ await assertRejects(async () => await Deno.readFile("cli"));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ function readFileSyncDoesNotLeakResources() {
+ assertThrows(() => Deno.readFileSync("cli"));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readFileWithAbortSignal() {
+ const ac = new AbortController();
+ queueMicrotask(() => ac.abort());
+ const error = await assertRejects(
+ async () => {
+ await Deno.readFile("tests/testdata/assets/fixture.json", {
+ signal: ac.signal,
+ });
+ },
+ );
+ assert(error instanceof DOMException);
+ assertEquals(error.name, "AbortError");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readFileWithAbortSignalReason() {
+ const ac = new AbortController();
+ const abortReason = new Error();
+ queueMicrotask(() => ac.abort(abortReason));
+ const error = await assertRejects(
+ async () => {
+ await Deno.readFile("tests/testdata/assets/fixture.json", {
+ signal: ac.signal,
+ });
+ },
+ );
+ assertEquals(error, abortReason);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readFileWithAbortSignalPrimitiveReason() {
+ const ac = new AbortController();
+ queueMicrotask(() => ac.abort("Some string"));
+ try {
+ await Deno.readFile("tests/testdata/assets/fixture.json", {
+ signal: ac.signal,
+ });
+ unreachable();
+ } catch (e) {
+ assertEquals(e, "Some string");
+ }
+ },
+);
+
+// Test that AbortController's cancel handle is cleaned-up correctly, and do not leak resources.
+Deno.test(
+ { permissions: { read: true } },
+ async function readFileWithAbortSignalNotCalled() {
+ const ac = new AbortController();
+ await Deno.readFile("tests/testdata/assets/fixture.json", {
+ signal: ac.signal,
+ });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true }, ignore: Deno.build.os !== "linux" },
+ async function readFileProcFs() {
+ const data = await Deno.readFile("/proc/self/stat");
+ assert(data.byteLength > 0);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readFileNotFoundErrorCode() {
+ try {
+ await Deno.readFile("definitely-not-found.json");
+ } catch (e) {
+ assertEquals(e.code, "ENOENT");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readFileIsDirectoryErrorCode() {
+ try {
+ await Deno.readFile("tests/testdata/assets/");
+ } catch (e) {
+ if (Deno.build.os === "windows") {
+ assertEquals(e.code, "ENOENT");
+ } else {
+ assertEquals(e.code, "EISDIR");
+ }
+ }
+ },
+);
diff --git a/tests/unit/read_link_test.ts b/tests/unit/read_link_test.ts
new file mode 100644
index 000000000..3ed1817bb
--- /dev/null
+++ b/tests/unit/read_link_test.ts
@@ -0,0 +1,99 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assertEquals,
+ assertRejects,
+ assertThrows,
+ pathToAbsoluteFileUrl,
+} from "./test_util.ts";
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ function readLinkSyncSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const target = testDir +
+ (Deno.build.os == "windows" ? "\\target" : "/target");
+ const symlink = testDir +
+ (Deno.build.os == "windows" ? "\\symlink" : "/symlink");
+ Deno.mkdirSync(target);
+ Deno.symlinkSync(target, symlink);
+ const targetPath = Deno.readLinkSync(symlink);
+ assertEquals(targetPath, target);
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ function readLinkSyncUrlSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const target = testDir +
+ (Deno.build.os == "windows" ? "\\target" : "/target");
+ const symlink = testDir +
+ (Deno.build.os == "windows" ? "\\symlink" : "/symlink");
+ Deno.mkdirSync(target);
+ Deno.symlinkSync(target, symlink);
+ const targetPath = Deno.readLinkSync(pathToAbsoluteFileUrl(symlink));
+ assertEquals(targetPath, target);
+ },
+);
+
+Deno.test({ permissions: { read: false } }, function readLinkSyncPerm() {
+ assertThrows(() => {
+ Deno.readLinkSync("/symlink");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, function readLinkSyncNotFound() {
+ assertThrows(
+ () => {
+ Deno.readLinkSync("bad_filename");
+ },
+ Deno.errors.NotFound,
+ `readlink 'bad_filename'`,
+ );
+});
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function readLinkSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const target = testDir +
+ (Deno.build.os == "windows" ? "\\target" : "/target");
+ const symlink = testDir +
+ (Deno.build.os == "windows" ? "\\symlink" : "/symlink");
+ Deno.mkdirSync(target);
+ Deno.symlinkSync(target, symlink);
+ const targetPath = await Deno.readLink(symlink);
+ assertEquals(targetPath, target);
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function readLinkUrlSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const target = testDir +
+ (Deno.build.os == "windows" ? "\\target" : "/target");
+ const symlink = testDir +
+ (Deno.build.os == "windows" ? "\\symlink" : "/symlink");
+ Deno.mkdirSync(target);
+ Deno.symlinkSync(target, symlink);
+ const targetPath = await Deno.readLink(pathToAbsoluteFileUrl(symlink));
+ assertEquals(targetPath, target);
+ },
+);
+
+Deno.test({ permissions: { read: false } }, async function readLinkPerm() {
+ await assertRejects(async () => {
+ await Deno.readLink("/symlink");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, async function readLinkNotFound() {
+ await assertRejects(
+ async () => {
+ await Deno.readLink("bad_filename");
+ },
+ Deno.errors.NotFound,
+ `readlink 'bad_filename'`,
+ );
+});
diff --git a/tests/unit/read_text_file_test.ts b/tests/unit/read_text_file_test.ts
new file mode 100644
index 000000000..94aa5f0a8
--- /dev/null
+++ b/tests/unit/read_text_file_test.ts
@@ -0,0 +1,208 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+ pathToAbsoluteFileUrl,
+ unreachable,
+} from "./test_util.ts";
+
+Deno.test({ permissions: { read: true } }, function readTextFileSyncSuccess() {
+ const data = Deno.readTextFileSync("tests/testdata/assets/fixture.json");
+ assert(data.length > 0);
+ const pkg = JSON.parse(data);
+ assertEquals(pkg.name, "deno");
+});
+
+Deno.test({ permissions: { read: true } }, function readTextFileSyncByUrl() {
+ const data = Deno.readTextFileSync(
+ pathToAbsoluteFileUrl("tests/testdata/assets/fixture.json"),
+ );
+ assert(data.length > 0);
+ const pkg = JSON.parse(data);
+ assertEquals(pkg.name, "deno");
+});
+
+Deno.test({ permissions: { read: false } }, function readTextFileSyncPerm() {
+ assertThrows(() => {
+ Deno.readTextFileSync("tests/testdata/assets/fixture.json");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, function readTextFileSyncNotFound() {
+ assertThrows(() => {
+ Deno.readTextFileSync("bad_filename");
+ }, Deno.errors.NotFound);
+});
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readTextFileSuccess() {
+ const data = await Deno.readTextFile(
+ "tests/testdata/assets/fixture.json",
+ );
+ assert(data.length > 0);
+ const pkg = JSON.parse(data);
+ assertEquals(pkg.name, "deno");
+ },
+);
+
+Deno.test({ permissions: { read: true } }, async function readTextFileByUrl() {
+ const data = await Deno.readTextFile(
+ pathToAbsoluteFileUrl("tests/testdata/assets/fixture.json"),
+ );
+ assert(data.length > 0);
+ const pkg = JSON.parse(data);
+ assertEquals(pkg.name, "deno");
+});
+
+Deno.test({ permissions: { read: false } }, async function readTextFilePerm() {
+ await assertRejects(async () => {
+ await Deno.readTextFile("tests/testdata/assets/fixture.json");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, function readTextFileSyncLoop() {
+ for (let i = 0; i < 256; i++) {
+ Deno.readTextFileSync("tests/testdata/assets/fixture.json");
+ }
+});
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readTextFileDoesNotLeakResources() {
+ await assertRejects(async () => await Deno.readTextFile("cli"));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ function readTextFileSyncDoesNotLeakResources() {
+ assertThrows(() => Deno.readTextFileSync("cli"));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readTextFileWithAbortSignal() {
+ const ac = new AbortController();
+ queueMicrotask(() => ac.abort());
+ const error = await assertRejects(
+ async () => {
+ await Deno.readTextFile("tests/testdata/assets/fixture.json", {
+ signal: ac.signal,
+ });
+ },
+ );
+ assert(error instanceof DOMException);
+ assertEquals(error.name, "AbortError");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readTextFileWithAbortSignalReason() {
+ const ac = new AbortController();
+ const abortReason = new Error();
+ queueMicrotask(() => ac.abort(abortReason));
+ const error = await assertRejects(
+ async () => {
+ await Deno.readTextFile("tests/testdata/assets/fixture.json", {
+ signal: ac.signal,
+ });
+ },
+ );
+ assertEquals(error, abortReason);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function readTextFileWithAbortSignalPrimitiveReason() {
+ const ac = new AbortController();
+ queueMicrotask(() => ac.abort("Some string"));
+ try {
+ await Deno.readTextFile("tests/testdata/assets/fixture.json", {
+ signal: ac.signal,
+ });
+ unreachable();
+ } catch (e) {
+ assertEquals(e, "Some string");
+ }
+ },
+);
+
+// Test that AbortController's cancel handle is cleaned-up correctly, and do not leak resources.
+Deno.test(
+ { permissions: { read: true } },
+ async function readTextFileWithAbortSignalNotCalled() {
+ const ac = new AbortController();
+ await Deno.readTextFile("tests/testdata/assets/fixture.json", {
+ signal: ac.signal,
+ });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true }, ignore: Deno.build.os !== "linux" },
+ async function readTextFileProcFs() {
+ const data = await Deno.readTextFile("/proc/self/stat");
+ assert(data.length > 0);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function readTextFileSyncV8LimitError() {
+ const kStringMaxLengthPlusOne = 536870888 + 1;
+ const bytes = new Uint8Array(kStringMaxLengthPlusOne);
+ const filePath = "tests/testdata/too_big_a_file.txt";
+
+ try {
+ Deno.writeFileSync(filePath, bytes);
+ } catch {
+ // NOTE(bartlomieju): writing a 0.5Gb file might be too much for CI,
+ // so skip running if writing fails.
+ return;
+ }
+
+ assertThrows(
+ () => {
+ Deno.readTextFileSync(filePath);
+ },
+ TypeError,
+ "buffer exceeds maximum length",
+ );
+
+ Deno.removeSync(filePath);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function readTextFileV8LimitError() {
+ const kStringMaxLengthPlusOne = 536870888 + 1;
+ const bytes = new Uint8Array(kStringMaxLengthPlusOne);
+ const filePath = "tests/testdata/too_big_a_file_2.txt";
+
+ try {
+ await Deno.writeFile(filePath, bytes);
+ } catch {
+ // NOTE(bartlomieju): writing a 0.5Gb file might be too much for CI,
+ // so skip running if writing fails.
+ return;
+ }
+
+ await assertRejects(
+ async () => {
+ await Deno.readTextFile(filePath);
+ },
+ TypeError,
+ "buffer exceeds maximum length",
+ );
+
+ await Deno.remove(filePath);
+ },
+);
diff --git a/tests/unit/real_path_test.ts b/tests/unit/real_path_test.ts
new file mode 100644
index 000000000..b3656a927
--- /dev/null
+++ b/tests/unit/real_path_test.ts
@@ -0,0 +1,114 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertMatch,
+ assertRejects,
+ assertThrows,
+ pathToAbsoluteFileUrl,
+} from "./test_util.ts";
+
+Deno.test({ permissions: { read: true } }, function realPathSyncSuccess() {
+ const relative = "tests/testdata/assets/fixture.json";
+ const realPath = Deno.realPathSync(relative);
+ if (Deno.build.os !== "windows") {
+ assert(realPath.startsWith("/"));
+ assert(realPath.endsWith(relative));
+ } else {
+ assertMatch(realPath, /^[A-Z]:\\/);
+ assert(realPath.endsWith(relative.replace(/\//g, "\\")));
+ }
+});
+
+Deno.test({ permissions: { read: true } }, function realPathSyncUrl() {
+ const relative = "tests/testdata/assets/fixture.json";
+ const url = pathToAbsoluteFileUrl(relative);
+ assertEquals(Deno.realPathSync(relative), Deno.realPathSync(url));
+});
+
+Deno.test(
+ {
+ permissions: { read: true, write: true },
+ },
+ function realPathSyncSymlink() {
+ const testDir = Deno.makeTempDirSync();
+ const target = testDir + "/target";
+ const symlink = testDir + "/symln";
+ Deno.mkdirSync(target);
+ Deno.symlinkSync(target, symlink);
+ const realPath = Deno.realPathSync(symlink);
+ if (Deno.build.os !== "windows") {
+ assert(realPath.startsWith("/"));
+ assert(realPath.endsWith("/target"));
+ } else {
+ assertMatch(realPath, /^[A-Z]:\\/);
+ assert(realPath.endsWith("\\target"));
+ }
+ },
+);
+
+Deno.test({ permissions: { read: false } }, function realPathSyncPerm() {
+ assertThrows(() => {
+ Deno.realPathSync("some_file");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, function realPathSyncNotFound() {
+ assertThrows(() => {
+ Deno.realPathSync("bad_filename");
+ }, Deno.errors.NotFound);
+});
+
+Deno.test({ permissions: { read: true } }, async function realPathSuccess() {
+ const relativePath = "tests/testdata/assets/fixture.json";
+ const realPath = await Deno.realPath(relativePath);
+ if (Deno.build.os !== "windows") {
+ assert(realPath.startsWith("/"));
+ assert(realPath.endsWith(relativePath));
+ } else {
+ assertMatch(realPath, /^[A-Z]:\\/);
+ assert(realPath.endsWith(relativePath.replace(/\//g, "\\")));
+ }
+});
+
+Deno.test(
+ { permissions: { read: true } },
+ async function realPathUrl() {
+ const relative = "tests/testdata/assets/fixture.json";
+ const url = pathToAbsoluteFileUrl(relative);
+ assertEquals(await Deno.realPath(relative), await Deno.realPath(url));
+ },
+);
+
+Deno.test(
+ {
+ permissions: { read: true, write: true },
+ },
+ async function realPathSymlink() {
+ const testDir = Deno.makeTempDirSync();
+ const target = testDir + "/target";
+ const symlink = testDir + "/symln";
+ Deno.mkdirSync(target);
+ Deno.symlinkSync(target, symlink);
+ const realPath = await Deno.realPath(symlink);
+ if (Deno.build.os !== "windows") {
+ assert(realPath.startsWith("/"));
+ assert(realPath.endsWith("/target"));
+ } else {
+ assertMatch(realPath, /^[A-Z]:\\/);
+ assert(realPath.endsWith("\\target"));
+ }
+ },
+);
+
+Deno.test({ permissions: { read: false } }, async function realPathPerm() {
+ await assertRejects(async () => {
+ await Deno.realPath("some_file");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, async function realPathNotFound() {
+ await assertRejects(async () => {
+ await Deno.realPath("bad_filename");
+ }, Deno.errors.NotFound);
+});
diff --git a/tests/unit/ref_unref_test.ts b/tests/unit/ref_unref_test.ts
new file mode 100644
index 000000000..6f5bcf0a7
--- /dev/null
+++ b/tests/unit/ref_unref_test.ts
@@ -0,0 +1,12 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assertNotEquals, execCode } from "./test_util.ts";
+
+Deno.test("[unrefOpPromise] unref'ing invalid ops does not have effects", async () => {
+ const [statusCode, _] = await execCode(`
+ Deno[Deno.internal].core.unrefOpPromise(new Promise(r => null));
+ setTimeout(() => { throw new Error() }, 10)
+ `);
+ // Invalid unrefOpPromise call doesn't affect exit condition of event loop
+ assertNotEquals(statusCode, 0);
+});
diff --git a/tests/unit/remove_test.ts b/tests/unit/remove_test.ts
new file mode 100644
index 000000000..f4e54dc52
--- /dev/null
+++ b/tests/unit/remove_test.ts
@@ -0,0 +1,291 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertRejects, assertThrows } from "./test_util.ts";
+
+const REMOVE_METHODS = ["remove", "removeSync"] as const;
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function removeDirSuccess() {
+ for (const method of REMOVE_METHODS) {
+ // REMOVE EMPTY DIRECTORY
+ const path = Deno.makeTempDirSync() + "/subdir";
+ Deno.mkdirSync(path);
+ const pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory); // check exist first
+ await Deno[method](path); // remove
+ // We then check again after remove
+ assertThrows(() => {
+ Deno.statSync(path);
+ }, Deno.errors.NotFound);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function removeFileSuccess() {
+ for (const method of REMOVE_METHODS) {
+ // REMOVE FILE
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data, { mode: 0o666 });
+ const fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile); // check exist first
+ await Deno[method](filename); // remove
+ // We then check again after remove
+ assertThrows(() => {
+ Deno.statSync(filename);
+ }, Deno.errors.NotFound);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function removeFileByUrl() {
+ for (const method of REMOVE_METHODS) {
+ // REMOVE FILE
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+
+ const tempDir = Deno.makeTempDirSync();
+ const fileUrl = new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/test.txt`,
+ );
+
+ Deno.writeFileSync(fileUrl, data, { mode: 0o666 });
+ const fileInfo = Deno.statSync(fileUrl);
+ assert(fileInfo.isFile); // check exist first
+ await Deno[method](fileUrl); // remove
+ // We then check again after remove
+ assertThrows(() => {
+ Deno.statSync(fileUrl);
+ }, Deno.errors.NotFound);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function removeFail() {
+ for (const method of REMOVE_METHODS) {
+ // NON-EMPTY DIRECTORY
+ const path = Deno.makeTempDirSync() + "/dir/subdir";
+ const subPath = path + "/subsubdir";
+ Deno.mkdirSync(path, { recursive: true });
+ Deno.mkdirSync(subPath);
+ const pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory); // check exist first
+ const subPathInfo = Deno.statSync(subPath);
+ assert(subPathInfo.isDirectory); // check exist first
+
+ await assertRejects(
+ async () => {
+ await Deno[method](path);
+ },
+ Error,
+ `remove '${path}'`,
+ );
+ // TODO(ry) Is Other really the error we should get here? What would Go do?
+
+ // NON-EXISTENT DIRECTORY/FILE
+ await assertRejects(
+ async () => {
+ await Deno[method]("/baddir");
+ },
+ Deno.errors.NotFound,
+ `remove '/baddir'`,
+ );
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function removeDanglingSymlinkSuccess() {
+ for (const method of REMOVE_METHODS) {
+ const danglingSymlinkPath = Deno.makeTempDirSync() + "/dangling_symlink";
+ if (Deno.build.os === "windows") {
+ Deno.symlinkSync("unexistent_file", danglingSymlinkPath, {
+ type: "file",
+ });
+ } else {
+ Deno.symlinkSync("unexistent_file", danglingSymlinkPath);
+ }
+ const pathInfo = Deno.lstatSync(danglingSymlinkPath);
+ assert(pathInfo.isSymlink);
+ await Deno[method](danglingSymlinkPath);
+ assertThrows(() => {
+ Deno.lstatSync(danglingSymlinkPath);
+ }, Deno.errors.NotFound);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function removeValidSymlinkSuccess() {
+ for (const method of REMOVE_METHODS) {
+ const encoder = new TextEncoder();
+ const data = encoder.encode("Test");
+ const tempDir = Deno.makeTempDirSync();
+ const filePath = tempDir + "/test.txt";
+ const validSymlinkPath = tempDir + "/valid_symlink";
+ Deno.writeFileSync(filePath, data, { mode: 0o666 });
+ if (Deno.build.os === "windows") {
+ Deno.symlinkSync(filePath, validSymlinkPath, { type: "file" });
+ } else {
+ Deno.symlinkSync(filePath, validSymlinkPath);
+ }
+ const symlinkPathInfo = Deno.statSync(validSymlinkPath);
+ assert(symlinkPathInfo.isFile);
+ await Deno[method](validSymlinkPath);
+ assertThrows(() => {
+ Deno.statSync(validSymlinkPath);
+ }, Deno.errors.NotFound);
+ await Deno[method](filePath);
+ }
+ },
+);
+
+Deno.test({ permissions: { write: false } }, async function removePerm() {
+ for (const method of REMOVE_METHODS) {
+ await assertRejects(async () => {
+ await Deno[method]("/baddir");
+ }, Deno.errors.PermissionDenied);
+ }
+});
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function removeAllDirSuccess() {
+ for (const method of REMOVE_METHODS) {
+ // REMOVE EMPTY DIRECTORY
+ let path = Deno.makeTempDirSync() + "/dir/subdir";
+ Deno.mkdirSync(path, { recursive: true });
+ let pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory); // check exist first
+ await Deno[method](path, { recursive: true }); // remove
+ // We then check again after remove
+ assertThrows(
+ () => {
+ Deno.statSync(path);
+ }, // Directory is gone
+ Deno.errors.NotFound,
+ );
+
+ // REMOVE NON-EMPTY DIRECTORY
+ path = Deno.makeTempDirSync() + "/dir/subdir";
+ const subPath = path + "/subsubdir";
+ Deno.mkdirSync(path, { recursive: true });
+ Deno.mkdirSync(subPath);
+ pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory); // check exist first
+ const subPathInfo = Deno.statSync(subPath);
+ assert(subPathInfo.isDirectory); // check exist first
+ await Deno[method](path, { recursive: true }); // remove
+ // We then check parent directory again after remove
+ assertThrows(() => {
+ Deno.statSync(path);
+ }, Deno.errors.NotFound);
+ // Directory is gone
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { write: true, read: true } },
+ async function removeAllFileSuccess() {
+ for (const method of REMOVE_METHODS) {
+ // REMOVE FILE
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data, { mode: 0o666 });
+ const fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile); // check exist first
+ await Deno[method](filename, { recursive: true }); // remove
+ // We then check again after remove
+ assertThrows(() => {
+ Deno.statSync(filename);
+ }, Deno.errors.NotFound);
+ // File is gone
+ }
+ },
+);
+
+Deno.test({ permissions: { write: true } }, async function removeAllFail() {
+ for (const method of REMOVE_METHODS) {
+ // NON-EXISTENT DIRECTORY/FILE
+ await assertRejects(
+ async () => {
+ // Non-existent
+ await Deno[method]("/baddir", { recursive: true });
+ },
+ Deno.errors.NotFound,
+ `remove '/baddir'`,
+ );
+ }
+});
+
+Deno.test({ permissions: { write: false } }, async function removeAllPerm() {
+ for (const method of REMOVE_METHODS) {
+ await assertRejects(async () => {
+ await Deno[method]("/baddir", { recursive: true });
+ }, Deno.errors.PermissionDenied);
+ }
+});
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { write: true, read: true },
+ },
+ async function removeUnixSocketSuccess() {
+ for (const method of REMOVE_METHODS) {
+ // MAKE TEMPORARY UNIX SOCKET
+ const path = Deno.makeTempDirSync() + "/test.sock";
+ const listener = Deno.listen({ transport: "unix", path });
+ listener.close();
+ Deno.statSync(path); // check if unix socket exists
+
+ await Deno[method](path);
+ assertThrows(() => Deno.statSync(path), Deno.errors.NotFound);
+ }
+ },
+);
+
+if (Deno.build.os === "windows") {
+ Deno.test(
+ { permissions: { run: true, write: true, read: true } },
+ async function removeFileSymlink() {
+ const { success } = await new Deno.Command("cmd", {
+ args: ["/c", "mklink", "file_link", "bar"],
+ stdout: "null",
+ }).output();
+
+ assert(success);
+ await Deno.remove("file_link");
+ await assertRejects(async () => {
+ await Deno.lstat("file_link");
+ }, Deno.errors.NotFound);
+ },
+ );
+
+ Deno.test(
+ { permissions: { run: true, write: true, read: true } },
+ async function removeDirSymlink() {
+ const { success } = await new Deno.Command("cmd", {
+ args: ["/c", "mklink", "/d", "dir_link", "bar"],
+ stdout: "null",
+ }).output();
+
+ assert(success);
+ await Deno.remove("dir_link");
+ await assertRejects(async () => {
+ await Deno.lstat("dir_link");
+ }, Deno.errors.NotFound);
+ },
+ );
+}
diff --git a/tests/unit/rename_test.ts b/tests/unit/rename_test.ts
new file mode 100644
index 000000000..4f6bb09cf
--- /dev/null
+++ b/tests/unit/rename_test.ts
@@ -0,0 +1,274 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ AssertionError,
+ assertIsError,
+ assertThrows,
+ pathToAbsoluteFileUrl,
+} from "./test_util.ts";
+
+function assertMissing(path: string) {
+ let caughtErr = false;
+ let info;
+ try {
+ info = Deno.lstatSync(path);
+ } catch (e) {
+ caughtErr = true;
+ assert(e instanceof Deno.errors.NotFound);
+ }
+ assert(caughtErr);
+ assertEquals(info, undefined);
+}
+
+function assertFile(path: string) {
+ const info = Deno.lstatSync(path);
+ assert(info.isFile);
+}
+
+function assertDirectory(path: string, mode?: number) {
+ const info = Deno.lstatSync(path);
+ assert(info.isDirectory);
+ if (Deno.build.os !== "windows" && mode !== undefined) {
+ assertEquals(info.mode! & 0o777, mode & ~Deno.umask());
+ }
+}
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function renameSyncSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const oldpath = testDir + "/oldpath";
+ const newpath = testDir + "/newpath";
+ Deno.mkdirSync(oldpath);
+ Deno.renameSync(oldpath, newpath);
+ assertDirectory(newpath);
+ assertMissing(oldpath);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function renameSyncWithURL() {
+ const testDir = Deno.makeTempDirSync();
+ const oldpath = testDir + "/oldpath";
+ const newpath = testDir + "/newpath";
+ Deno.mkdirSync(oldpath);
+ Deno.renameSync(
+ pathToAbsoluteFileUrl(oldpath),
+ pathToAbsoluteFileUrl(newpath),
+ );
+ assertDirectory(newpath);
+ assertMissing(oldpath);
+ },
+);
+
+Deno.test(
+ { permissions: { read: false, write: true } },
+ function renameSyncReadPerm() {
+ assertThrows(() => {
+ const oldpath = "/oldbaddir";
+ const newpath = "/newbaddir";
+ Deno.renameSync(oldpath, newpath);
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: false } },
+ function renameSyncWritePerm() {
+ assertThrows(() => {
+ const oldpath = "/oldbaddir";
+ const newpath = "/newbaddir";
+ Deno.renameSync(oldpath, newpath);
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function renameSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const oldpath = testDir + "/oldpath";
+ const newpath = testDir + "/newpath";
+ Deno.mkdirSync(oldpath);
+ await Deno.rename(oldpath, newpath);
+ assertDirectory(newpath);
+ assertMissing(oldpath);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function renameWithURL() {
+ const testDir = Deno.makeTempDirSync();
+ const oldpath = testDir + "/oldpath";
+ const newpath = testDir + "/newpath";
+ Deno.mkdirSync(oldpath);
+ await Deno.rename(
+ pathToAbsoluteFileUrl(oldpath),
+ pathToAbsoluteFileUrl(newpath),
+ );
+ assertDirectory(newpath);
+ assertMissing(oldpath);
+ },
+);
+
+function readFileString(filename: string): string {
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ return dec.decode(dataRead);
+}
+
+function writeFileString(filename: string, s: string) {
+ const enc = new TextEncoder();
+ const data = enc.encode(s);
+ Deno.writeFileSync(filename, data, { mode: 0o666 });
+}
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ function renameSyncErrorsUnix() {
+ const testDir = Deno.makeTempDirSync();
+ const oldfile = testDir + "/oldfile";
+ const olddir = testDir + "/olddir";
+ const emptydir = testDir + "/empty";
+ const fulldir = testDir + "/dir";
+ const file = fulldir + "/file";
+ writeFileString(oldfile, "Hello");
+ Deno.mkdirSync(olddir);
+ Deno.mkdirSync(emptydir);
+ Deno.mkdirSync(fulldir);
+ writeFileString(file, "world");
+
+ assertThrows(
+ () => {
+ Deno.renameSync(oldfile, emptydir);
+ },
+ Error,
+ "Is a directory",
+ );
+ try {
+ assertThrows(
+ () => {
+ Deno.renameSync(olddir, fulldir);
+ },
+ Error,
+ "Directory not empty",
+ );
+ } catch (e) {
+ // rename syscall may also return EEXIST, e.g. with XFS
+ assertIsError(
+ e,
+ AssertionError,
+ `Expected error message to include "Directory not empty", but got "File exists`,
+ );
+ }
+ assertThrows(
+ () => {
+ Deno.renameSync(olddir, file);
+ },
+ Error,
+ "Not a directory",
+ );
+ assertThrows(
+ () => {
+ Deno.renameSync(olddir, file);
+ },
+ Error,
+ `rename '${olddir}' -> '${file}'`,
+ );
+
+ const fileLink = testDir + "/fileLink";
+ const dirLink = testDir + "/dirLink";
+ const danglingLink = testDir + "/danglingLink";
+ Deno.symlinkSync(file, fileLink);
+ Deno.symlinkSync(emptydir, dirLink);
+ Deno.symlinkSync(testDir + "/nonexistent", danglingLink);
+
+ assertThrows(
+ () => {
+ Deno.renameSync(olddir, fileLink);
+ },
+ Error,
+ "Not a directory",
+ );
+ assertThrows(
+ () => {
+ Deno.renameSync(olddir, dirLink);
+ },
+ Error,
+ "Not a directory",
+ );
+ assertThrows(
+ () => {
+ Deno.renameSync(olddir, danglingLink);
+ },
+ Error,
+ "Not a directory",
+ );
+
+ // should succeed on Unix
+ Deno.renameSync(olddir, emptydir);
+ Deno.renameSync(oldfile, dirLink);
+ Deno.renameSync(dirLink, danglingLink);
+ assertFile(danglingLink);
+ assertEquals("Hello", readFileString(danglingLink));
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os !== "windows",
+ permissions: { read: true, write: true },
+ },
+ function renameSyncErrorsWin() {
+ const testDir = Deno.makeTempDirSync();
+ const oldfile = testDir + "/oldfile";
+ const olddir = testDir + "/olddir";
+ const emptydir = testDir + "/empty";
+ const fulldir = testDir + "/dir";
+ const file = fulldir + "/file";
+ writeFileString(oldfile, "Hello");
+ Deno.mkdirSync(olddir);
+ Deno.mkdirSync(emptydir);
+ Deno.mkdirSync(fulldir);
+ writeFileString(file, "world");
+
+ assertThrows(
+ () => {
+ Deno.renameSync(oldfile, emptydir);
+ },
+ Deno.errors.PermissionDenied,
+ "Access is denied",
+ );
+ assertThrows(
+ () => {
+ Deno.renameSync(olddir, fulldir);
+ },
+ Deno.errors.PermissionDenied,
+ "Access is denied",
+ );
+ assertThrows(
+ () => {
+ Deno.renameSync(olddir, emptydir);
+ },
+ Deno.errors.PermissionDenied,
+ "Access is denied",
+ );
+ assertThrows(
+ () => {
+ Deno.renameSync(olddir, emptydir);
+ },
+ Error,
+ `rename '${olddir}' -> '${emptydir}'`,
+ );
+
+ // should succeed on Windows
+ Deno.renameSync(olddir, file);
+ assertDirectory(file);
+ },
+);
diff --git a/tests/unit/request_test.ts b/tests/unit/request_test.ts
new file mode 100644
index 000000000..fe34c20a5
--- /dev/null
+++ b/tests/unit/request_test.ts
@@ -0,0 +1,77 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals, assertStringIncludes } from "./test_util.ts";
+
+Deno.test(async function fromInit() {
+ const req = new Request("http://foo/", {
+ body: "ahoyhoy",
+ method: "POST",
+ headers: {
+ "test-header": "value",
+ },
+ });
+
+ assertEquals("ahoyhoy", await req.text());
+ assertEquals(req.url, "http://foo/");
+ assertEquals(req.headers.get("test-header"), "value");
+});
+
+Deno.test(function requestNonString() {
+ const nonString = {
+ toString() {
+ return "http://foo/";
+ },
+ };
+ // deno-lint-ignore ban-ts-comment
+ // @ts-expect-error
+ assertEquals(new Request(nonString).url, "http://foo/");
+});
+
+Deno.test(function methodNonString() {
+ assertEquals(new Request("http://foo/", { method: undefined }).method, "GET");
+});
+
+Deno.test(function requestRelativeUrl() {
+ assertEquals(
+ new Request("relative-url").url,
+ "http://127.0.0.1:4545/relative-url",
+ );
+});
+
+Deno.test(async function cloneRequestBodyStream() {
+ // hack to get a stream
+ const stream =
+ new Request("http://foo/", { body: "a test body", method: "POST" }).body;
+ const r1 = new Request("http://foo/", {
+ body: stream,
+ method: "POST",
+ });
+
+ const r2 = r1.clone();
+
+ const b1 = await r1.text();
+ const b2 = await r2.text();
+
+ assertEquals(b1, b2);
+});
+
+Deno.test(function customInspectFunction() {
+ const request = new Request("https://example.com");
+ assertEquals(
+ Deno.inspect(request),
+ `Request {
+ bodyUsed: false,
+ headers: Headers {},
+ method: "GET",
+ redirect: "follow",
+ url: "https://example.com/"
+}`,
+ );
+ assertStringIncludes(Deno.inspect(Request.prototype), "Request");
+});
+
+Deno.test(function requestConstructorTakeURLObjectAsParameter() {
+ assertEquals(
+ new Request(new URL("http://foo/")).url,
+ "http://foo/",
+ );
+});
diff --git a/tests/unit/resources_test.ts b/tests/unit/resources_test.ts
new file mode 100644
index 000000000..bb0b9f2f8
--- /dev/null
+++ b/tests/unit/resources_test.ts
@@ -0,0 +1,55 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals, assertThrows } from "./test_util.ts";
+
+const listenPort = 4505;
+
+Deno.test(function resourcesCloseBadArgs() {
+ assertThrows(() => {
+ Deno.close((null as unknown) as number);
+ }, TypeError);
+});
+
+Deno.test(function resourcesStdio() {
+ const res = Deno.resources();
+
+ assertEquals(res[0], "stdin");
+ assertEquals(res[1], "stdout");
+ assertEquals(res[2], "stderr");
+});
+
+Deno.test({ permissions: { net: true } }, async function resourcesNet() {
+ const listener = Deno.listen({ port: listenPort });
+ const dialerConn = await Deno.connect({ port: listenPort });
+ const listenerConn = await listener.accept();
+
+ const res = Deno.resources();
+ assertEquals(
+ Object.values(res).filter((r): boolean => r === "tcpListener").length,
+ 1,
+ );
+ const tcpStreams = Object.values(res).filter(
+ (r): boolean => r === "tcpStream",
+ );
+ assert(tcpStreams.length >= 2);
+
+ listenerConn.close();
+ dialerConn.close();
+ listener.close();
+});
+
+Deno.test({ permissions: { read: true } }, async function resourcesFile() {
+ const resourcesBefore = Deno.resources();
+ const f = await Deno.open("tests/testdata/assets/hello.txt");
+ const resourcesAfter = Deno.resources();
+ f.close();
+
+ // check that exactly one new resource (file) was added
+ assertEquals(
+ Object.keys(resourcesAfter).length,
+ Object.keys(resourcesBefore).length + 1,
+ );
+ const newRid = +Object.keys(resourcesAfter).find((rid): boolean => {
+ return !Object.prototype.hasOwnProperty.call(resourcesBefore, rid);
+ })!;
+ assertEquals(resourcesAfter[newRid], "fsFile");
+});
diff --git a/tests/unit/response_test.ts b/tests/unit/response_test.ts
new file mode 100644
index 000000000..bbdd5f481
--- /dev/null
+++ b/tests/unit/response_test.ts
@@ -0,0 +1,102 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertStringIncludes,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test(async function responseText() {
+ const response = new Response("hello world");
+ const textPromise = response.text();
+ assert(textPromise instanceof Promise);
+ const text = await textPromise;
+ assert(typeof text === "string");
+ assertEquals(text, "hello world");
+});
+
+Deno.test(async function responseArrayBuffer() {
+ const response = new Response(new Uint8Array([1, 2, 3]));
+ const arrayBufferPromise = response.arrayBuffer();
+ assert(arrayBufferPromise instanceof Promise);
+ const arrayBuffer = await arrayBufferPromise;
+ assert(arrayBuffer instanceof ArrayBuffer);
+ assertEquals(new Uint8Array(arrayBuffer), new Uint8Array([1, 2, 3]));
+});
+
+Deno.test(async function responseJson() {
+ const response = new Response('{"hello": "world"}');
+ const jsonPromise = response.json();
+ assert(jsonPromise instanceof Promise);
+ const json = await jsonPromise;
+ assert(json instanceof Object);
+ assertEquals(json, { hello: "world" });
+});
+
+Deno.test(async function responseBlob() {
+ const response = new Response(new Uint8Array([1, 2, 3]));
+ const blobPromise = response.blob();
+ assert(blobPromise instanceof Promise);
+ const blob = await blobPromise;
+ assert(blob instanceof Blob);
+ assertEquals(blob.size, 3);
+ assertEquals(await blob.arrayBuffer(), new Uint8Array([1, 2, 3]).buffer);
+});
+
+Deno.test(async function responseFormData() {
+ const input = new FormData();
+ input.append("hello", "world");
+ const response = new Response(input);
+ const contentType = response.headers.get("content-type")!;
+ assert(contentType.startsWith("multipart/form-data"));
+ const formDataPromise = response.formData();
+ assert(formDataPromise instanceof Promise);
+ const formData = await formDataPromise;
+ assert(formData instanceof FormData);
+ assertEquals([...formData], [...input]);
+});
+
+Deno.test(function responseInvalidInit() {
+ // deno-lint-ignore ban-ts-comment
+ // @ts-expect-error
+ assertThrows(() => new Response("", 0));
+ assertThrows(() => new Response("", { status: 0 }));
+ // deno-lint-ignore ban-ts-comment
+ // @ts-expect-error
+ assertThrows(() => new Response("", { status: null }));
+});
+
+Deno.test(function responseNullInit() {
+ // deno-lint-ignore ban-ts-comment
+ // @ts-expect-error
+ const response = new Response("", null);
+ assertEquals(response.status, 200);
+});
+
+Deno.test(function customInspectFunction() {
+ const response = new Response();
+ assertEquals(
+ Deno.inspect(response),
+ `Response {
+ body: null,
+ bodyUsed: false,
+ headers: Headers {},
+ ok: true,
+ redirected: false,
+ status: 200,
+ statusText: "",
+ url: ""
+}`,
+ );
+ assertStringIncludes(Deno.inspect(Response.prototype), "Response");
+});
+
+Deno.test(async function responseBodyUsed() {
+ const response = new Response("body");
+ assert(!response.bodyUsed);
+ await response.text();
+ assert(response.bodyUsed);
+ // .body getter is needed so we can test the faulty code path
+ response.body;
+ assert(response.bodyUsed);
+});
diff --git a/tests/unit/serve_test.ts b/tests/unit/serve_test.ts
new file mode 100644
index 000000000..e972b36cd
--- /dev/null
+++ b/tests/unit/serve_test.ts
@@ -0,0 +1,3932 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assertMatch, assertRejects } from "@test_util/std/assert/mod.ts";
+import { Buffer, BufReader, BufWriter } from "@test_util/std/io/mod.ts";
+import { TextProtoReader } from "../testdata/run/textproto.ts";
+import {
+ assert,
+ assertEquals,
+ assertStringIncludes,
+ assertThrows,
+ execCode,
+ fail,
+ tmpUnixSocketPath,
+} from "./test_util.ts";
+
+// Since these tests may run in parallel, ensure this port is unique to this file
+const servePort = 4502;
+
+const {
+ upgradeHttpRaw,
+ addTrailers,
+ serveHttpOnListener,
+ serveHttpOnConnection,
+ // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
+} = Deno[Deno.internal];
+
+function createOnErrorCb(ac: AbortController): (err: unknown) => Response {
+ return (err) => {
+ console.error(err);
+ ac.abort();
+ return new Response("Internal server error", { status: 500 });
+ };
+}
+
+function onListen(
+ resolve: (value: void | PromiseLike<void>) => void,
+): ({ hostname, port }: { hostname: string; port: number }) => void {
+ return () => {
+ resolve();
+ };
+}
+
+async function makeServer(
+ handler: (req: Request) => Response | Promise<Response>,
+): Promise<
+ {
+ finished: Promise<void>;
+ abort: () => void;
+ shutdown: () => Promise<void>;
+ [Symbol.asyncDispose](): PromiseLike<void>;
+ }
+> {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler,
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ });
+
+ await promise;
+ return {
+ finished: server.finished,
+ abort() {
+ ac.abort();
+ },
+ async shutdown() {
+ await server.shutdown();
+ },
+ [Symbol.asyncDispose]() {
+ return server[Symbol.asyncDispose]();
+ },
+ };
+}
+
+Deno.test(async function httpServerShutsDownPortBeforeResolving() {
+ const { finished, abort } = await makeServer((_req) => new Response("ok"));
+ assertThrows(() => Deno.listen({ port: servePort }));
+ abort();
+ await finished;
+
+ const listener = Deno.listen({ port: servePort });
+ listener!.close();
+});
+
+// When shutting down abruptly, we require that all in-progress connections are aborted,
+// no new connections are allowed, and no new transactions are allowed on existing connections.
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerShutdownAbruptGuaranteeHttp11() {
+ const deferredQueue: {
+ input: ReturnType<typeof Promise.withResolvers<string>>;
+ out: ReturnType<typeof Promise.withResolvers<void>>;
+ }[] = [];
+ const { finished, abort } = await makeServer((_req) => {
+ const { input, out } = deferredQueue.shift()!;
+ return new Response(
+ new ReadableStream({
+ async start(controller) {
+ controller.enqueue(new Uint8Array([46]));
+ out.resolve();
+ controller.enqueue(encoder.encode(await input.promise));
+ controller.close();
+ },
+ }),
+ );
+ });
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+ const conn = await Deno.connect({ port: servePort });
+ const w = conn.writable.getWriter();
+ const r = conn.readable.getReader();
+
+ const deferred1 = {
+ input: Promise.withResolvers<string>(),
+ out: Promise.withResolvers<void>(),
+ };
+ deferredQueue.push(deferred1);
+ const deferred2 = {
+ input: Promise.withResolvers<string>(),
+ out: Promise.withResolvers<void>(),
+ };
+ deferredQueue.push(deferred2);
+ const deferred3 = {
+ input: Promise.withResolvers<string>(),
+ out: Promise.withResolvers<void>(),
+ };
+ deferredQueue.push(deferred3);
+ deferred1.input.resolve("#");
+ deferred2.input.resolve("$");
+ await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`));
+ await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`));
+
+ // Fully read two responses
+ let text = "";
+ while (!text.includes("$\r\n")) {
+ text += decoder.decode((await r.read()).value);
+ }
+
+ await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`));
+ await deferred3.out.promise;
+
+ // This is half served, so wait for the chunk that has the first '.'
+ text = "";
+ while (!text.includes("1\r\n.\r\n")) {
+ text += decoder.decode((await r.read()).value);
+ }
+
+ abort();
+
+ // This doesn't actually write anything, but we release it after aborting
+ deferred3.input.resolve("!");
+
+ // Guarantee: can't connect to an aborted server (though this may not happen immediately)
+ let failed = false;
+ for (let i = 0; i < 10; i++) {
+ try {
+ const conn = await Deno.connect({ port: servePort });
+ conn.close();
+ // Give the runtime a few ticks to settle (required for Windows)
+ await new Promise((r) => setTimeout(r, 2 ** i));
+ continue;
+ } catch (_) {
+ failed = true;
+ break;
+ }
+ }
+ assert(failed, "The Deno.serve listener was not disabled promptly");
+
+ // Guarantee: the pipeline is closed abruptly
+ assert((await r.read()).done);
+
+ try {
+ conn.close();
+ } catch (_) {
+ // Ignore
+ }
+ await finished;
+ },
+);
+
+// When shutting down abruptly, we require that all in-progress connections are aborted,
+// no new connections are allowed, and no new transactions are allowed on existing connections.
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerShutdownGracefulGuaranteeHttp11() {
+ const deferredQueue: {
+ input: ReturnType<typeof Promise.withResolvers<string>>;
+ out: ReturnType<typeof Promise.withResolvers<void>>;
+ }[] = [];
+ const { finished, shutdown } = await makeServer((_req) => {
+ const { input, out } = deferredQueue.shift()!;
+ return new Response(
+ new ReadableStream({
+ async start(controller) {
+ controller.enqueue(new Uint8Array([46]));
+ out.resolve();
+ controller.enqueue(encoder.encode(await input.promise));
+ controller.close();
+ },
+ }),
+ );
+ });
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+ const conn = await Deno.connect({ port: servePort });
+ const w = conn.writable.getWriter();
+ const r = conn.readable.getReader();
+
+ const deferred1 = {
+ input: Promise.withResolvers<string>(),
+ out: Promise.withResolvers<void>(),
+ };
+ deferredQueue.push(deferred1);
+ const deferred2 = {
+ input: Promise.withResolvers<string>(),
+ out: Promise.withResolvers<void>(),
+ };
+ deferredQueue.push(deferred2);
+ const deferred3 = {
+ input: Promise.withResolvers<string>(),
+ out: Promise.withResolvers<void>(),
+ };
+ deferredQueue.push(deferred3);
+ deferred1.input.resolve("#");
+ deferred2.input.resolve("$");
+ await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`));
+ await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`));
+
+ // Fully read two responses
+ let text = "";
+ while (!text.includes("$\r\n")) {
+ text += decoder.decode((await r.read()).value);
+ }
+
+ await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`));
+ await deferred3.out.promise;
+
+ // This is half served, so wait for the chunk that has the first '.'
+ text = "";
+ while (!text.includes("1\r\n.\r\n")) {
+ text += decoder.decode((await r.read()).value);
+ }
+
+ const shutdownPromise = shutdown();
+
+ // Release the final response _after_ we shut down
+ deferred3.input.resolve("!");
+
+ // Guarantee: can't connect to an aborted server (though this may not happen immediately)
+ let failed = false;
+ for (let i = 0; i < 10; i++) {
+ try {
+ const conn = await Deno.connect({ port: servePort });
+ conn.close();
+ // Give the runtime a few ticks to settle (required for Windows)
+ await new Promise((r) => setTimeout(r, 2 ** i));
+ continue;
+ } catch (_) {
+ failed = true;
+ break;
+ }
+ }
+ assert(failed, "The Deno.serve listener was not disabled promptly");
+
+ // Guarantee: existing connections fully drain
+ while (!text.includes("!\r\n")) {
+ text += decoder.decode((await r.read()).value);
+ }
+
+ await shutdownPromise;
+
+ try {
+ conn.close();
+ } catch (_) {
+ // Ignore
+ }
+ await finished;
+ },
+);
+
+// Ensure that resources don't leak during a graceful shutdown
+Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ async function httpServerShutdownGracefulResources() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const { finished, shutdown } = await makeServer(async (_req) => {
+ resolve();
+ await new Promise((r) => setTimeout(r, 10));
+ return new Response((await makeTempFile(1024 * 1024)).readable);
+ });
+
+ const f = fetch(`http://localhost:${servePort}`);
+ await promise;
+ assertEquals((await (await f).text()).length, 1048576);
+ await shutdown();
+ await finished;
+ },
+);
+
+// Ensure that resources don't leak during a graceful shutdown
+Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ async function httpServerShutdownGracefulResources2() {
+ const waitForAbort = Promise.withResolvers<void>();
+ const waitForRequest = Promise.withResolvers<void>();
+ const { finished, shutdown } = await makeServer(async (_req) => {
+ waitForRequest.resolve();
+ await waitForAbort.promise;
+ await new Promise((r) => setTimeout(r, 10));
+ return new Response((await makeTempFile(1024 * 1024)).readable);
+ });
+
+ const f = fetch(`http://localhost:${servePort}`);
+ await waitForRequest.promise;
+ const s = shutdown();
+ waitForAbort.resolve();
+ assertEquals((await (await f).text()).length, 1048576);
+ await s;
+ await finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ async function httpServerExplicitResourceManagement() {
+ let dataPromise;
+
+ {
+ await using _server = await makeServer(async (_req) => {
+ return new Response((await makeTempFile(1024 * 1024)).readable);
+ });
+
+ const resp = await fetch(`http://localhost:${servePort}`);
+ dataPromise = resp.arrayBuffer();
+ }
+
+ assertEquals((await dataPromise).byteLength, 1048576);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ async function httpServerExplicitResourceManagementManualClose() {
+ await using server = await makeServer(async (_req) => {
+ return new Response((await makeTempFile(1024 * 1024)).readable);
+ });
+
+ const resp = await fetch(`http://localhost:${servePort}`);
+
+ const [_, data] = await Promise.all([
+ server.shutdown(),
+ resp.arrayBuffer(),
+ ]);
+
+ assertEquals(data.byteLength, 1048576);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: true } },
+ async function httpServerUnref() {
+ const [statusCode, _output] = await execCode(`
+ async function main() {
+ const server = Deno.serve({ port: ${servePort}, handler: () => null });
+ server.unref();
+ await server.finished; // This doesn't block the program from exiting
+ }
+ main();
+ `);
+ assertEquals(statusCode, 0);
+ },
+);
+
+Deno.test(async function httpServerCanResolveHostnames() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: (_req) => new Response("ok"),
+ hostname: "localhost",
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await promise;
+ const resp = await fetch(`http://localhost:${servePort}/`, {
+ headers: { "connection": "close" },
+ });
+ const text = await resp.text();
+ assertEquals(text, "ok");
+ ac.abort();
+ await server.finished;
+});
+
+Deno.test(async function httpServerRejectsOnAddrInUse() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: (_req) => new Response("ok"),
+ hostname: "localhost",
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+ await promise;
+
+ assertThrows(
+ () =>
+ Deno.serve({
+ handler: (_req) => new Response("ok"),
+ hostname: "localhost",
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ }),
+ Deno.errors.AddrInUse,
+ );
+ ac.abort();
+ await server.finished;
+});
+
+Deno.test({ permissions: { net: true } }, async function httpServerBasic() {
+ const ac = new AbortController();
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: async (request, { remoteAddr }) => {
+ // FIXME(bartlomieju):
+ // make sure that request can be inspected
+ console.log(request);
+ assertEquals(new URL(request.url).href, `http://127.0.0.1:${servePort}/`);
+ assertEquals(await request.text(), "");
+ assertEquals(remoteAddr.hostname, "127.0.0.1");
+ deferred.resolve();
+ return new Response("Hello World", { headers: { "foo": "bar" } });
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ headers: { "connection": "close" },
+ });
+ await deferred.promise;
+ const clone = resp.clone();
+ const text = await resp.text();
+ assertEquals(text, "Hello World");
+ assertEquals(resp.headers.get("foo"), "bar");
+ const cloneText = await clone.text();
+ assertEquals(cloneText, "Hello World");
+ ac.abort();
+ await server.finished;
+});
+
+// Test serving of HTTP on an arbitrary listener.
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerOnListener() {
+ const ac = new AbortController();
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const listener = Deno.listen({ port: servePort });
+ const server = serveHttpOnListener(
+ listener,
+ ac.signal,
+ async (
+ request: Request,
+ { remoteAddr }: { remoteAddr: { hostname: string } },
+ ) => {
+ assertEquals(
+ new URL(request.url).href,
+ `http://127.0.0.1:${servePort}/`,
+ );
+ assertEquals(await request.text(), "");
+ assertEquals(remoteAddr.hostname, "127.0.0.1");
+ deferred.resolve();
+ return new Response("Hello World", { headers: { "foo": "bar" } });
+ },
+ createOnErrorCb(ac),
+ onListen(listeningDeferred.resolve),
+ );
+
+ await listeningDeferred.promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ headers: { "connection": "close" },
+ });
+ await listeningDeferred.promise;
+ const clone = resp.clone();
+ const text = await resp.text();
+ assertEquals(text, "Hello World");
+ assertEquals(resp.headers.get("foo"), "bar");
+ const cloneText = await clone.text();
+ assertEquals(cloneText, "Hello World");
+ ac.abort();
+ await server.finished;
+ },
+);
+
+// Test serving of HTTP on an arbitrary connection.
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerOnConnection() {
+ const ac = new AbortController();
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const listener = Deno.listen({ port: servePort });
+ const acceptPromise = listener.accept();
+ const fetchPromise = fetch(`http://127.0.0.1:${servePort}/`, {
+ headers: { "connection": "close" },
+ });
+
+ const server = serveHttpOnConnection(
+ await acceptPromise,
+ ac.signal,
+ async (
+ request: Request,
+ { remoteAddr }: { remoteAddr: { hostname: string } },
+ ) => {
+ assertEquals(
+ new URL(request.url).href,
+ `http://127.0.0.1:${servePort}/`,
+ );
+ assertEquals(await request.text(), "");
+ assertEquals(remoteAddr.hostname, "127.0.0.1");
+ deferred.resolve();
+ return new Response("Hello World", { headers: { "foo": "bar" } });
+ },
+ createOnErrorCb(ac),
+ onListen(listeningDeferred.resolve),
+ );
+
+ const resp = await fetchPromise;
+ await deferred.promise;
+ const clone = resp.clone();
+ const text = await resp.text();
+ assertEquals(text, "Hello World");
+ assertEquals(resp.headers.get("foo"), "bar");
+ const cloneText = await clone.text();
+ assertEquals(cloneText, "Hello World");
+ // Note that we don't need to abort this server -- it closes when the connection does
+ // ac.abort();
+ await server.finished;
+ listener.close();
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function httpServerOnError() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let requestStash: Request | null;
+
+ const server = Deno.serve({
+ handler: async (request: Request) => {
+ requestStash = request;
+ await new Promise((r) => setTimeout(r, 100));
+ throw "fail";
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: () => {
+ return new Response("failed: " + requestStash!.url, { status: 500 });
+ },
+ });
+
+ await promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ headers: { "connection": "close" },
+ });
+ const text = await resp.text();
+ ac.abort();
+ await server.finished;
+
+ assertEquals(text, `failed: http://127.0.0.1:${servePort}/`);
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerOnErrorFails() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+ // NOTE(bartlomieju): deno lint doesn't know that it's actually used later,
+ // but TypeScript can't see that either ¯\_(ツ)_/¯
+ // deno-lint-ignore no-unused-vars
+ let requestStash: Request | null;
+
+ const server = Deno.serve({
+ handler: async (request: Request) => {
+ requestStash = request;
+ await new Promise((r) => setTimeout(r, 100));
+ throw "fail";
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: () => {
+ throw "again";
+ },
+ });
+
+ await promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ headers: { "connection": "close" },
+ });
+ const text = await resp.text();
+ ac.abort();
+ await server.finished;
+
+ assertEquals(text, "Internal Server Error");
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function httpServerOverload1() {
+ const ac = new AbortController();
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ }, async (request) => {
+ // FIXME(bartlomieju):
+ // make sure that request can be inspected
+ console.log(request);
+ assertEquals(new URL(request.url).href, `http://127.0.0.1:${servePort}/`);
+ assertEquals(await request.text(), "");
+ deferred.resolve();
+ return new Response("Hello World", { headers: { "foo": "bar" } });
+ });
+
+ await listeningDeferred.promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ headers: { "connection": "close" },
+ });
+ await deferred.promise;
+ const clone = resp.clone();
+ const text = await resp.text();
+ assertEquals(text, "Hello World");
+ assertEquals(resp.headers.get("foo"), "bar");
+ const cloneText = await clone.text();
+ assertEquals(cloneText, "Hello World");
+ ac.abort();
+ await server.finished;
+});
+
+Deno.test({ permissions: { net: true } }, async function httpServerOverload2() {
+ const ac = new AbortController();
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ }, async (request) => {
+ // FIXME(bartlomieju):
+ // make sure that request can be inspected
+ console.log(request);
+ assertEquals(new URL(request.url).href, `http://127.0.0.1:${servePort}/`);
+ assertEquals(await request.text(), "");
+ deferred.resolve();
+ return new Response("Hello World", { headers: { "foo": "bar" } });
+ });
+
+ await listeningDeferred.promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ headers: { "connection": "close" },
+ });
+ await deferred.promise;
+ const clone = resp.clone();
+ const text = await resp.text();
+ assertEquals(text, "Hello World");
+ assertEquals(resp.headers.get("foo"), "bar");
+ const cloneText = await clone.text();
+ assertEquals(cloneText, "Hello World");
+ ac.abort();
+ await server.finished;
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ function httpServerErrorOverloadMissingHandler() {
+ // @ts-ignore - testing invalid overload
+ assertThrows(() => Deno.serve(), TypeError, "handler");
+ // @ts-ignore - testing invalid overload
+ assertThrows(() => Deno.serve({}), TypeError, "handler");
+ assertThrows(
+ // @ts-ignore - testing invalid overload
+ () => Deno.serve({ handler: undefined }),
+ TypeError,
+ "handler",
+ );
+ assertThrows(
+ // @ts-ignore - testing invalid overload
+ () => Deno.serve(undefined, { handler: () => {} }),
+ TypeError,
+ "handler",
+ );
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function httpServerPort0() {
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler() {
+ return new Response("Hello World");
+ },
+ port: 0,
+ signal: ac.signal,
+ onListen({ port }) {
+ assert(port > 0 && port < 65536);
+ ac.abort();
+ },
+ });
+ await server.finished;
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerDefaultOnListenCallback() {
+ const ac = new AbortController();
+
+ const consoleLog = console.log;
+ console.log = (msg) => {
+ try {
+ const match = msg.match(/Listening on http:\/\/localhost:(\d+)\//);
+ assert(!!match, `Didn't match ${msg}`);
+ const port = +match[1];
+ assert(port > 0 && port < 65536);
+ } finally {
+ ac.abort();
+ }
+ };
+
+ try {
+ const server = Deno.serve({
+ handler() {
+ return new Response("Hello World");
+ },
+ hostname: "0.0.0.0",
+ port: 0,
+ signal: ac.signal,
+ });
+
+ await server.finished;
+ } finally {
+ console.log = consoleLog;
+ }
+ },
+);
+
+// https://github.com/denoland/deno/issues/15107
+Deno.test(
+ { permissions: { net: true } },
+ async function httpLazyHeadersIssue15107() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ let headers: Headers;
+ const server = Deno.serve({
+ handler: async (request) => {
+ await request.text();
+ headers = request.headers;
+ deferred.resolve();
+ return new Response("");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ // Send GET request with a body + content-length.
+ const encoder = new TextEncoder();
+ const body =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\n\r\n12345`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await deferred.promise;
+ conn.close();
+ assertEquals(headers!.get("content-length"), "5");
+ ac.abort();
+ await server.finished;
+ },
+);
+
+function createUrlTest(
+ name: string,
+ methodAndPath: string,
+ host: string | null,
+ expected: string,
+) {
+ Deno.test(`httpServerUrl${name}`, async () => {
+ const listeningDeferred = Promise.withResolvers<number>();
+ const urlDeferred = Promise.withResolvers<string>();
+ const ac = new AbortController();
+ const server = Deno.serve({
+ handler: (request: Request) => {
+ urlDeferred.resolve(request.url);
+ return new Response("");
+ },
+ port: 0,
+ signal: ac.signal,
+ onListen: ({ port }: { port: number }) => {
+ listeningDeferred.resolve(port);
+ },
+ onError: createOnErrorCb(ac),
+ });
+
+ const port = await listeningDeferred.promise;
+ const conn = await Deno.connect({ port });
+
+ const encoder = new TextEncoder();
+ const body = `${methodAndPath} HTTP/1.1\r\n${
+ host ? ("Host: " + host + "\r\n") : ""
+ }Content-Length: 5\r\n\r\n12345`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ try {
+ const expectedResult = expected.replace("HOST", "localhost").replace(
+ "PORT",
+ `${port}`,
+ );
+ assertEquals(await urlDeferred.promise, expectedResult);
+ } finally {
+ ac.abort();
+ await server.finished;
+ conn.close();
+ }
+ });
+}
+
+createUrlTest("WithPath", "GET /path", null, "http://HOST:PORT/path");
+createUrlTest(
+ "WithPathAndHost",
+ "GET /path",
+ "deno.land",
+ "http://deno.land/path",
+);
+createUrlTest(
+ "WithAbsolutePath",
+ "GET http://localhost/path",
+ null,
+ "http://localhost/path",
+);
+createUrlTest(
+ "WithAbsolutePathAndHost",
+ "GET http://localhost/path",
+ "deno.land",
+ "http://localhost/path",
+);
+createUrlTest(
+ "WithPortAbsolutePath",
+ "GET http://localhost:1234/path",
+ null,
+ "http://localhost:1234/path",
+);
+createUrlTest(
+ "WithPortAbsolutePathAndHost",
+ "GET http://localhost:1234/path",
+ "deno.land",
+ "http://localhost:1234/path",
+);
+createUrlTest(
+ "WithPortAbsolutePathAndHostWithPort",
+ "GET http://localhost:1234/path",
+ "deno.land:9999",
+ "http://localhost:1234/path",
+);
+
+createUrlTest("WithAsterisk", "OPTIONS *", null, "*");
+createUrlTest(
+ "WithAuthorityForm",
+ "CONNECT deno.land:80",
+ null,
+ "deno.land:80",
+);
+
+// TODO(mmastrac): These should probably be 400 errors
+createUrlTest("WithInvalidAsterisk", "GET *", null, "*");
+createUrlTest("WithInvalidNakedPath", "GET path", null, "path");
+createUrlTest(
+ "WithInvalidNakedAuthority",
+ "GET deno.land:1234",
+ null,
+ "deno.land:1234",
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerGetRequestBody() {
+ const deferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: (request) => {
+ assertEquals(request.body, null);
+ deferred.resolve();
+ return new Response("", { headers: {} });
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ // Send GET request with a body + content-length.
+ const encoder = new TextEncoder();
+ const body =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:${servePort}\r\nContent-Length: 5\r\n\r\n12345`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ const resp = new Uint8Array(200);
+ const readResult = await conn.read(resp);
+ assert(readResult);
+ assert(readResult > 0);
+
+ conn.close();
+ await deferred.promise;
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerAbortedRequestBody() {
+ const deferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: async (request) => {
+ await assertRejects(async () => {
+ await request.text();
+ });
+ deferred.resolve();
+ // Not actually used
+ return new Response();
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ // Send POST request with a body + content-length, but don't send it all
+ const encoder = new TextEncoder();
+ const body =
+ `POST / HTTP/1.1\r\nHost: 127.0.0.1:${servePort}\r\nContent-Length: 10\r\n\r\n12345`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ conn.close();
+ await deferred.promise;
+ ac.abort();
+ await server.finished;
+ },
+);
+
+function createStreamTest(count: number, delay: number, action: string) {
+ function doAction(controller: ReadableStreamDefaultController, i: number) {
+ if (i == count) {
+ if (action == "Throw") {
+ controller.error(new Error("Expected error!"));
+ } else {
+ controller.close();
+ }
+ } else {
+ controller.enqueue(`a${i}`);
+
+ if (delay == 0) {
+ doAction(controller, i + 1);
+ } else {
+ setTimeout(() => doAction(controller, i + 1), delay);
+ }
+ }
+ }
+
+ function makeStream(_count: number, delay: number): ReadableStream {
+ return new ReadableStream({
+ start(controller) {
+ if (delay == 0) {
+ doAction(controller, 0);
+ } else {
+ setTimeout(() => doAction(controller, 0), delay);
+ }
+ },
+ }).pipeThrough(new TextEncoderStream());
+ }
+
+ Deno.test(`httpServerStreamCount${count}Delay${delay}${action}`, async () => {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: (_request) => {
+ return new Response(makeStream(count, delay));
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ try {
+ await promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`);
+ if (action == "Throw") {
+ await assertRejects(async () => {
+ await resp.text();
+ });
+ } else {
+ const text = await resp.text();
+
+ let expected = "";
+ for (let i = 0; i < count; i++) {
+ expected += `a${i}`;
+ }
+
+ assertEquals(text, expected);
+ }
+ } finally {
+ ac.abort();
+ await server.shutdown();
+ }
+ });
+}
+
+for (const count of [0, 1, 2, 3]) {
+ for (const delay of [0, 1, 25]) {
+ // Creating a stream that errors in start will throw
+ if (delay > 0) {
+ createStreamTest(count, delay, "Throw");
+ }
+ createStreamTest(count, delay, "Close");
+ }
+}
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerStreamRequest() {
+ const stream = new TransformStream();
+ const writer = stream.writable.getWriter();
+ writer.write(new TextEncoder().encode("hello "));
+ writer.write(new TextEncoder().encode("world"));
+ writer.close();
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const ac = new AbortController();
+ const server = Deno.serve({
+ handler: async (request) => {
+ const reqBody = await request.text();
+ assertEquals("hello world", reqBody);
+ return new Response("yo");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ body: stream.readable,
+ method: "POST",
+ headers: { "connection": "close" },
+ });
+
+ assertEquals(await resp.text(), "yo");
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function httpServerClose() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: () => new Response("ok"),
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+ await promise;
+ const client = await Deno.connect({ port: servePort });
+ client.close();
+ ac.abort();
+ await server.finished;
+});
+
+// https://github.com/denoland/deno/issues/15427
+Deno.test({ permissions: { net: true } }, async function httpServerCloseGet() {
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const requestDeferred = Promise.withResolvers<void>();
+ const responseDeferred = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: async () => {
+ requestDeferred.resolve();
+ await new Promise((r) => setTimeout(r, 500));
+ responseDeferred.resolve();
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const body =
+ `GET / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await requestDeferred.promise;
+ conn.close();
+ await responseDeferred.promise;
+ ac.abort();
+ await server.finished;
+});
+
+// FIXME:
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerEmptyBlobResponse() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: () => new Response(new Blob([])),
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`);
+ const respBody = await resp.text();
+
+ assertEquals("", respBody);
+ ac.abort();
+ await server.finished;
+ },
+);
+
+// https://github.com/denoland/deno/issues/17291
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerIncorrectChunkedResponse() {
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const errorDeferred = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: () => {
+ const body = new ReadableStream({
+ start(controller) {
+ // Non-encoded string is not a valid readable chunk.
+ // @ts-ignore we're testing that input is invalid
+ controller.enqueue("wat");
+ },
+ type: "bytes",
+ });
+ return new Response(body);
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: (err) => {
+ const errResp = new Response(
+ `Internal server error: ${(err as Error).message}`,
+ { status: 500 },
+ );
+ errorDeferred.resolve();
+ return errResp;
+ },
+ });
+
+ await listeningDeferred.promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`);
+ // Incorrectly implemented reader ReadableStream should reject.
+ assertStringIncludes(await resp.text(), "Failed to execute 'enqueue'");
+ await errorDeferred.promise;
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerCorrectLengthForUnicodeString() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: () => new Response("韓國".repeat(10)),
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ const body =
+ `GET / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assert(readResult);
+ const msg = decoder.decode(buf.subarray(0, readResult));
+
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ assert(msg.includes("content-length: 60"));
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function httpServerWebSocket() {
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const doneDeferred = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: (request) => {
+ const {
+ response,
+ socket,
+ } = Deno.upgradeWebSocket(request);
+ socket.onerror = (e) => {
+ console.error(e);
+ fail();
+ };
+ socket.onmessage = (m) => {
+ socket.send(m.data);
+ socket.close(1001);
+ };
+ socket.onclose = () => doneDeferred.resolve();
+ return response;
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const def = Promise.withResolvers<void>();
+ const ws = new WebSocket(`ws://localhost:${servePort}`);
+ ws.onmessage = (m) => assertEquals(m.data, "foo");
+ ws.onerror = (e) => {
+ console.error(e);
+ fail();
+ };
+ ws.onclose = () => def.resolve();
+ ws.onopen = () => ws.send("foo");
+
+ await def.promise;
+ await doneDeferred.promise;
+ ac.abort();
+ await server.finished;
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerWebSocketRaw() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: async (request) => {
+ const { conn, response } = upgradeHttpRaw(request);
+ const buf = new Uint8Array(1024);
+ let read;
+
+ // Write our fake HTTP upgrade
+ await conn.write(
+ new TextEncoder().encode(
+ "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgraded\r\n\r\nExtra",
+ ),
+ );
+
+ // Upgrade data
+ read = await conn.read(buf);
+ assertEquals(
+ new TextDecoder().decode(buf.subarray(0, read!)),
+ "Upgrade data",
+ );
+ // Read the packet to echo
+ read = await conn.read(buf);
+ // Echo
+ await conn.write(buf.subarray(0, read!));
+
+ conn.close();
+ return response;
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await promise;
+
+ const conn = await Deno.connect({ port: servePort });
+ await conn.write(
+ new TextEncoder().encode(
+ "GET / HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\nUpgrade data",
+ ),
+ );
+ const buf = new Uint8Array(1024);
+ let len;
+
+ // Headers
+ let headers = "";
+ for (let i = 0; i < 2; i++) {
+ len = await conn.read(buf);
+ headers += new TextDecoder().decode(buf.subarray(0, len!));
+ if (headers.endsWith("Extra")) {
+ break;
+ }
+ }
+ assertMatch(
+ headers,
+ /HTTP\/1\.1 101 Switching Protocols[ ,.A-Za-z:0-9\r\n]*Extra/im,
+ );
+
+ // Data to echo
+ await conn.write(new TextEncoder().encode("buffer data"));
+
+ // Echo
+ len = await conn.read(buf);
+ assertEquals(
+ new TextDecoder().decode(buf.subarray(0, len!)),
+ "buffer data",
+ );
+
+ conn.close();
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerWebSocketUpgradeTwice() {
+ const ac = new AbortController();
+ const done = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: (request) => {
+ const {
+ response,
+ socket,
+ } = Deno.upgradeWebSocket(request);
+ assertThrows(
+ () => {
+ Deno.upgradeWebSocket(request);
+ },
+ Deno.errors.Http,
+ "already upgraded",
+ );
+ socket.onerror = (e) => {
+ console.error(e);
+ fail();
+ };
+ socket.onmessage = (m) => {
+ socket.send(m.data);
+ socket.close(1001);
+ };
+ socket.onclose = () => done.resolve();
+ return response;
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const def = Promise.withResolvers<void>();
+ const ws = new WebSocket(`ws://localhost:${servePort}`);
+ ws.onmessage = (m) => assertEquals(m.data, "foo");
+ ws.onerror = (e) => {
+ console.error(e);
+ fail();
+ };
+ ws.onclose = () => def.resolve();
+ ws.onopen = () => ws.send("foo");
+
+ await def.promise;
+ await done.promise;
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerWebSocketCloseFast() {
+ const ac = new AbortController();
+ const done = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: (request) => {
+ const {
+ response,
+ socket,
+ } = Deno.upgradeWebSocket(request);
+ socket.onopen = () => socket.close();
+ socket.onclose = () => done.resolve();
+ return response;
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const def = Promise.withResolvers<void>();
+ const ws = new WebSocket(`ws://localhost:${servePort}`);
+ ws.onerror = (e) => {
+ console.error(e);
+ fail();
+ };
+ ws.onclose = () => def.resolve();
+
+ await def.promise;
+ await done.promise;
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerWebSocketCanAccessRequest() {
+ const ac = new AbortController();
+ const done = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: (request) => {
+ const {
+ response,
+ socket,
+ } = Deno.upgradeWebSocket(request);
+ socket.onerror = (e) => {
+ console.error(e);
+ fail();
+ };
+ socket.onmessage = (_m) => {
+ socket.send(request.url.toString());
+ socket.close(1001);
+ };
+ socket.onclose = () => done.resolve();
+ return response;
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const def = Promise.withResolvers<void>();
+ const ws = new WebSocket(`ws://localhost:${servePort}`);
+ ws.onmessage = (m) =>
+ assertEquals(m.data, `http://localhost:${servePort}/`);
+ ws.onerror = (e) => {
+ console.error(e);
+ fail();
+ };
+ ws.onclose = () => def.resolve();
+ ws.onopen = () => ws.send("foo");
+
+ await def.promise;
+ await done.promise;
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpVeryLargeRequest() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ let headers: Headers;
+ const server = Deno.serve({
+ handler: (request) => {
+ headers = request.headers;
+ deferred.resolve();
+ return new Response("");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ // Send GET request with a body + content-length.
+ const encoder = new TextEncoder();
+ const smthElse = "x".repeat(16 * 1024 + 256);
+ const body =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\nSomething-Else: ${smthElse}\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await deferred.promise;
+ conn.close();
+ assertEquals(headers!.get("content-length"), "5");
+ assertEquals(headers!.get("something-else"), smthElse);
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpVeryLargeRequestAndBody() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ let headers: Headers;
+ let text: string;
+ const server = Deno.serve({
+ handler: async (request) => {
+ headers = request.headers;
+ text = await request.text();
+ deferred.resolve();
+ return new Response("");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ // Send GET request with a body + content-length.
+ const encoder = new TextEncoder();
+ const smthElse = "x".repeat(16 * 1024 + 256);
+ const reqBody = "hello world".repeat(1024);
+ let body =
+ `PUT / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: ${reqBody.length}\r\nSomething-Else: ${smthElse}\r\n\r\n${reqBody}`;
+
+ while (body.length > 0) {
+ const writeResult = await conn.write(encoder.encode(body));
+ body = body.slice(writeResult);
+ }
+
+ await deferred.promise;
+ conn.close();
+
+ assertEquals(headers!.get("content-length"), `${reqBody.length}`);
+ assertEquals(headers!.get("something-else"), smthElse);
+ assertEquals(text!, reqBody);
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpConnectionClose() {
+ const deferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: () => {
+ deferred.resolve();
+ return new Response("");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ // Send GET request with a body + connection: close.
+ const encoder = new TextEncoder();
+ const body =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nConnection: Close\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ await deferred.promise;
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+async function testDuplex(
+ reader: ReadableStreamDefaultReader<Uint8Array>,
+ writable: WritableStreamDefaultWriter<Uint8Array>,
+) {
+ await writable.write(new Uint8Array([1]));
+ const chunk1 = await reader.read();
+ assert(!chunk1.done);
+ assertEquals(chunk1.value, new Uint8Array([1]));
+ await writable.write(new Uint8Array([2]));
+ const chunk2 = await reader.read();
+ assert(!chunk2.done);
+ assertEquals(chunk2.value, new Uint8Array([2]));
+ await writable.close();
+ const chunk3 = await reader.read();
+ assert(chunk3.done);
+}
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerStreamDuplexDirect() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve(
+ { port: servePort, signal: ac.signal },
+ (request: Request) => {
+ assert(request.body);
+ resolve();
+ return new Response(request.body);
+ },
+ );
+
+ const { readable, writable } = new TransformStream();
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ method: "POST",
+ body: readable,
+ });
+
+ await promise;
+ assert(resp.body);
+ await testDuplex(resp.body.getReader(), writable.getWriter());
+ ac.abort();
+ await server.finished;
+ },
+);
+
+// Test that a duplex stream passing through JavaScript also works (ie: that the request body resource
+// is still alive). https://github.com/denoland/deno/pull/20206
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerStreamDuplexJavascript() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve(
+ { port: servePort, signal: ac.signal },
+ (request: Request) => {
+ assert(request.body);
+ resolve();
+ const reader = request.body.getReader();
+ return new Response(
+ new ReadableStream({
+ async pull(controller) {
+ await new Promise((r) => setTimeout(r, 100));
+ const { done, value } = await reader.read();
+ if (done) {
+ controller.close();
+ } else {
+ controller.enqueue(value);
+ }
+ },
+ }),
+ );
+ },
+ );
+
+ const { readable, writable } = new TransformStream();
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ method: "POST",
+ body: readable,
+ });
+
+ await promise;
+ assert(resp.body);
+ await testDuplex(resp.body.getReader(), writable.getWriter());
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ // Issue: https://github.com/denoland/deno/issues/10930
+ async function httpServerStreamingResponse() {
+ // This test enqueues a single chunk for readable
+ // stream and waits for client to read that chunk and signal
+ // it before enqueueing subsequent chunk. Issue linked above
+ // presented a situation where enqueued chunks were not
+ // written to the HTTP connection until the next chunk was enqueued.
+ const listeningDeferred = Promise.withResolvers<void>();
+ const deferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ let counter = 0;
+
+ const deferreds = [
+ Promise.withResolvers<void>(),
+ Promise.withResolvers<void>(),
+ Promise.withResolvers<void>(),
+ ];
+
+ async function writeRequest(conn: Deno.Conn) {
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ const w = new BufWriter(conn);
+ const r = new BufReader(conn);
+ const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${servePort}\r\n\r\n`;
+ const writeResult = await w.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await w.flush();
+ const tpr = new TextProtoReader(r);
+ const statusLine = await tpr.readLine();
+ assert(statusLine !== null);
+ const headers = await tpr.readMimeHeader();
+ assert(headers !== null);
+
+ const chunkedReader = chunkedBodyReader(headers, r);
+
+ const buf = new Uint8Array(5);
+ const dest = new Buffer();
+
+ let result: number | null;
+
+ try {
+ while ((result = await chunkedReader.read(buf)) !== null) {
+ const len = Math.min(buf.byteLength, result);
+
+ await dest.write(buf.subarray(0, len));
+
+ // Resolve a deferred - this will make response stream to
+ // enqueue next chunk.
+ deferreds[counter - 1].resolve();
+ }
+ return decoder.decode(dest.bytes());
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ function periodicStream() {
+ return new ReadableStream({
+ start(controller) {
+ controller.enqueue(`${counter}\n`);
+ counter++;
+ },
+
+ async pull(controller) {
+ if (counter >= 3) {
+ return controller.close();
+ }
+
+ await deferreds[counter - 1].promise;
+
+ controller.enqueue(`${counter}\n`);
+ counter++;
+ },
+ }).pipeThrough(new TextEncoderStream());
+ }
+
+ const server = Deno.serve({
+ handler: () => {
+ deferred.resolve();
+ return new Response(periodicStream());
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ // start a client
+ const clientConn = await Deno.connect({ port: servePort });
+
+ const r1 = await writeRequest(clientConn);
+ assertEquals(r1, "0\n1\n2\n");
+
+ ac.abort();
+ await deferred.promise;
+ await server.finished;
+ clientConn.close();
+ },
+);
+
+// Make sure that the chunks of a large response aren't repeated or corrupted in some other way by
+// scatterning sentinels throughout.
+// https://github.com/denoland/fresh/issues/1699
+Deno.test(
+ { permissions: { net: true } },
+ async function httpLargeReadableStreamChunk() {
+ const ac = new AbortController();
+ const server = Deno.serve({
+ handler() {
+ return new Response(
+ new ReadableStream({
+ start(controller) {
+ const buffer = new Uint8Array(1024 * 1024);
+ // Mark the buffer with sentinels
+ for (let i = 0; i < 256; i++) {
+ buffer[i * 4096] = i;
+ }
+ controller.enqueue(buffer);
+ controller.close();
+ },
+ }),
+ );
+ },
+ port: servePort,
+ signal: ac.signal,
+ });
+ const response = await fetch(`http://localhost:${servePort}/`);
+ const body = await response.arrayBuffer();
+ assertEquals(1024 * 1024, body.byteLength);
+ const buffer = new Uint8Array(body);
+ for (let i = 0; i < 256; i++) {
+ assertEquals(
+ i,
+ buffer[i * 4096],
+ `sentinel mismatch at index ${i * 4096}`,
+ );
+ }
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpRequestLatin1Headers() {
+ const listeningDeferred = Promise.withResolvers<void>();
+ const deferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+ const server = Deno.serve({
+ handler: (request) => {
+ assertEquals(request.headers.get("X-Header-Test"), "á");
+ deferred.resolve();
+ return new Response("hello", { headers: { "X-Header-Test": "Æ" } });
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const clientConn = await Deno.connect({ port: servePort });
+ const requestText =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:${servePort}\r\nX-Header-Test: á\r\n\r\n`;
+ const requestBytes = new Uint8Array(requestText.length);
+ for (let i = 0; i < requestText.length; i++) {
+ requestBytes[i] = requestText.charCodeAt(i);
+ }
+ let written = 0;
+ while (written < requestBytes.byteLength) {
+ written += await clientConn.write(requestBytes.slice(written));
+ }
+
+ const buf = new Uint8Array(1024);
+ await clientConn.read(buf);
+
+ await deferred.promise;
+ const responseText = new TextDecoder("iso-8859-1").decode(buf);
+ clientConn.close();
+
+ ac.abort();
+ await server.finished;
+
+ assertMatch(responseText, /\r\n[Xx]-[Hh]eader-[Tt]est: Æ\r\n/);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerRequestWithoutPath() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: async (request) => {
+ // FIXME:
+ // assertEquals(new URL(request.url).href, `http://127.0.0.1:${servePort}/`);
+ assertEquals(await request.text(), "");
+ deferred.resolve();
+ return new Response("11");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const clientConn = await Deno.connect({ port: servePort });
+
+ async function writeRequest(conn: Deno.Conn) {
+ const encoder = new TextEncoder();
+
+ const w = new BufWriter(conn);
+ const r = new BufReader(conn);
+ const body =
+ `CONNECT 127.0.0.1:${servePort} HTTP/1.1\r\nHost: 127.0.0.1:${servePort}\r\n\r\n`;
+ const writeResult = await w.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await w.flush();
+ const tpr = new TextProtoReader(r);
+ const statusLine = await tpr.readLine();
+ assert(statusLine !== null);
+ const m = statusLine.match(/^(.+?) (.+?) (.+?)$/);
+ assert(m !== null, "must be matched");
+ const [_, _proto, status, _ok] = m;
+ assertEquals(status, "200");
+ const headers = await tpr.readMimeHeader();
+ assert(headers !== null);
+ }
+
+ await writeRequest(clientConn);
+ clientConn.close();
+ await deferred.promise;
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpCookieConcatenation() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: async (request) => {
+ assertEquals(await request.text(), "");
+ assertEquals(request.headers.get("cookie"), "foo=bar; bar=foo");
+ deferred.resolve();
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ reusePort: true,
+ });
+
+ await listeningDeferred.promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ headers: [
+ ["connection", "close"],
+ ["cookie", "foo=bar"],
+ ["cookie", "bar=foo"],
+ ],
+ });
+ await deferred.promise;
+
+ const text = await resp.text();
+ assertEquals(text, "ok");
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+// https://github.com/denoland/deno/issues/12741
+// https://github.com/denoland/deno/pull/12746
+// https://github.com/denoland/deno/pull/12798
+Deno.test(
+ { permissions: { net: true, run: true } },
+ async function httpServerDeleteRequestHasBody() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const hostname = "localhost";
+
+ const server = Deno.serve({
+ handler: () => {
+ deferred.resolve();
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const url = `http://${hostname}:${servePort}/`;
+ const args = ["-X", "DELETE", url];
+ const { success } = await new Deno.Command("curl", {
+ args,
+ stdout: "null",
+ stderr: "null",
+ }).output();
+ assert(success);
+ await deferred.promise;
+ ac.abort();
+
+ await server.finished;
+ },
+);
+
+// FIXME:
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerRespondNonAsciiUint8Array() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: (request) => {
+ assertEquals(request.body, null);
+ deferred.resolve();
+ return new Response(new Uint8Array([128]));
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+ await listeningDeferred.resolve;
+ const resp = await fetch(`http://localhost:${servePort}/`);
+
+ await deferred.promise;
+
+ assertEquals(resp.status, 200);
+ const body = await resp.arrayBuffer();
+ assertEquals(new Uint8Array(body), new Uint8Array([128]));
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+// Some of these tests are ported from Hyper
+// https://github.com/hyperium/hyper/blob/889fa2d87252108eb7668b8bf034ffcc30985117/src/proto/h1/role.rs
+// https://github.com/hyperium/hyper/blob/889fa2d87252108eb7668b8bf034ffcc30985117/tests/server.rs
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerParseRequest() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: (request) => {
+ assertEquals(request.method, "GET");
+ assertEquals(request.headers.get("host"), "deno.land");
+ deferred.resolve();
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const body = `GET /echo HTTP/1.1\r\nHost: deno.land\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await deferred.promise;
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerParseHeaderHtabs() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: (request) => {
+ assertEquals(request.method, "GET");
+ assertEquals(request.headers.get("server"), "hello\tworld");
+ deferred.resolve();
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const body = `GET / HTTP/1.1\r\nserver: hello\tworld\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await deferred.promise;
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerGetShouldIgnoreBody() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: async (request) => {
+ assertEquals(request.method, "GET");
+ assertEquals(await request.text(), "");
+ deferred.resolve();
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ // Connection: close = don't try to parse the body as a new request
+ const body =
+ `GET / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\nI shouldn't be read.\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await deferred.promise;
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerPostWithBody() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: async (request) => {
+ assertEquals(request.method, "POST");
+ assertEquals(await request.text(), "I'm a good request.");
+ deferred.resolve();
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const body =
+ `POST / HTTP/1.1\r\nHost: example.domain\r\nContent-Length: 19\r\n\r\nI'm a good request.`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await deferred.promise;
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+type TestCase = {
+ headers?: Record<string, string>;
+ // deno-lint-ignore no-explicit-any
+ body: any;
+ expectsChunked?: boolean;
+ expectsConnLen?: boolean;
+};
+
+function hasHeader(msg: string, name: string): boolean {
+ const n = msg.indexOf("\r\n\r\n") || msg.length;
+ return msg.slice(0, n).includes(name);
+}
+
+function createServerLengthTest(name: string, testCase: TestCase) {
+ Deno.test(name, async function () {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: (request) => {
+ assertEquals(request.method, "GET");
+ deferred.resolve();
+ return new Response(testCase.body, testCase.headers ?? {});
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const body =
+ `GET / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await deferred.promise;
+
+ const decoder = new TextDecoder();
+ let msg = "";
+ while (true) {
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ if (!readResult) {
+ break;
+ }
+ msg += decoder.decode(buf.subarray(0, readResult));
+ try {
+ assert(
+ testCase.expectsChunked == hasHeader(msg, "Transfer-Encoding:"),
+ );
+ assert(testCase.expectsChunked == hasHeader(msg, "chunked"));
+ assert(testCase.expectsConnLen == hasHeader(msg, "Content-Length:"));
+
+ const n = msg.indexOf("\r\n\r\n") + 4;
+
+ if (testCase.expectsChunked) {
+ assertEquals(msg.slice(n + 1, n + 3), "\r\n");
+ assertEquals(msg.slice(msg.length - 7), "\r\n0\r\n\r\n");
+ }
+
+ if (testCase.expectsConnLen && typeof testCase.body === "string") {
+ assertEquals(msg.slice(n), testCase.body);
+ }
+ break;
+ } catch {
+ continue;
+ }
+ }
+
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ });
+}
+
+// Quick and dirty way to make a readable stream from a string. Alternatively,
+// `readableStreamFromReader(file)` could be used.
+function stream(s: string): ReadableStream<Uint8Array> {
+ return new Response(s).body!;
+}
+
+createServerLengthTest("fixedResponseKnown", {
+ headers: { "content-length": "11" },
+ body: "foo bar baz",
+ expectsChunked: false,
+ expectsConnLen: true,
+});
+
+createServerLengthTest("fixedResponseUnknown", {
+ headers: { "content-length": "11" },
+ body: stream("foo bar baz"),
+ expectsChunked: true,
+ expectsConnLen: false,
+});
+
+createServerLengthTest("fixedResponseKnownEmpty", {
+ headers: { "content-length": "0" },
+ body: "",
+ expectsChunked: false,
+ expectsConnLen: true,
+});
+
+createServerLengthTest("chunkedRespondKnown", {
+ headers: { "transfer-encoding": "chunked" },
+ body: "foo bar baz",
+ expectsChunked: false,
+ expectsConnLen: true,
+});
+
+createServerLengthTest("chunkedRespondUnknown", {
+ headers: { "transfer-encoding": "chunked" },
+ body: stream("foo bar baz"),
+ expectsChunked: true,
+ expectsConnLen: false,
+});
+
+createServerLengthTest("autoResponseWithKnownLength", {
+ body: "foo bar baz",
+ expectsChunked: false,
+ expectsConnLen: true,
+});
+
+createServerLengthTest("autoResponseWithUnknownLength", {
+ body: stream("foo bar baz"),
+ expectsChunked: true,
+ expectsConnLen: false,
+});
+
+createServerLengthTest("autoResponseWithKnownLengthEmpty", {
+ body: "",
+ expectsChunked: false,
+ expectsConnLen: true,
+});
+
+createServerLengthTest("autoResponseWithUnknownLengthEmpty", {
+ body: stream(""),
+ expectsChunked: true,
+ expectsConnLen: false,
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerPostWithContentLengthBody() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: async (request) => {
+ assertEquals(request.method, "POST");
+ assertEquals(request.headers.get("content-length"), "5");
+ assertEquals(await request.text(), "hello");
+ deferred.resolve();
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+
+ const body =
+ `POST / HTTP/1.1\r\nHost: example.domain\r\nContent-Length: 5\r\n\r\nhello`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await deferred.promise;
+
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerPostWithInvalidPrefixContentLength() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: () => {
+ throw new Error("unreachable");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ const body =
+ `POST / HTTP/1.1\r\nHost: example.domain\r\nContent-Length: +5\r\n\r\nhello`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assert(readResult);
+ const msg = decoder.decode(buf.subarray(0, readResult));
+ assert(msg.includes("HTTP/1.1 400 Bad Request"));
+
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerPostWithChunkedBody() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: async (request) => {
+ assertEquals(request.method, "POST");
+ assertEquals(await request.text(), "qwert");
+ deferred.resolve();
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+
+ const body =
+ `POST / HTTP/1.1\r\nHost: example.domain\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nq\r\n2\r\nwe\r\n2\r\nrt\r\n0\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await deferred.promise;
+
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerPostWithIncompleteBody() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: async (r) => {
+ deferred.resolve();
+ assertEquals(await r.text(), "12345");
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+
+ const body =
+ `POST / HTTP/1.1\r\nHost: example.domain\r\nContent-Length: 10\r\n\r\n12345`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ await deferred.promise;
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerHeadResponseDoesntSendBody() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: () => {
+ deferred.resolve();
+ return new Response("NaN".repeat(100));
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ const body =
+ `HEAD / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ await deferred.promise;
+
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assert(readResult);
+ const msg = decoder.decode(buf.subarray(0, readResult));
+
+ assert(msg.includes("content-length: 300\r\n"));
+
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+function makeTempData(size: number) {
+ return new Uint8Array(size).fill(1);
+}
+
+async function makeTempFile(size: number) {
+ const tmpFile = await Deno.makeTempFile();
+ using file = await Deno.open(tmpFile, { write: true, read: true });
+ const data = makeTempData(size);
+ await file.write(data);
+
+ return await Deno.open(tmpFile, { write: true, read: true });
+}
+
+const compressionTestCases = [
+ { name: "Empty", length: 0, in: {}, out: {}, expect: null },
+ {
+ name: "EmptyAcceptGzip",
+ length: 0,
+ in: { "Accept-Encoding": "gzip" },
+ out: {},
+ expect: null,
+ },
+ // This technically would be compressible if not for the size, however the size_hint is not implemented
+ // for FileResource and we don't currently peek ahead on resources.
+ // {
+ // name: "EmptyAcceptGzip2",
+ // length: 0,
+ // in: { "Accept-Encoding": "gzip" },
+ // out: { "Content-Type": "text/plain" },
+ // expect: null,
+ // },
+ { name: "Incompressible", length: 1024, in: {}, out: {}, expect: null },
+ {
+ name: "IncompressibleAcceptGzip",
+ length: 1024,
+ in: { "Accept-Encoding": "gzip" },
+ out: {},
+ expect: null,
+ },
+ {
+ name: "IncompressibleType",
+ length: 1024,
+ in: { "Accept-Encoding": "gzip" },
+ out: { "Content-Type": "text/fake" },
+ expect: null,
+ },
+ {
+ name: "CompressibleType",
+ length: 1024,
+ in: { "Accept-Encoding": "gzip" },
+ out: { "Content-Type": "text/plain" },
+ expect: "gzip",
+ },
+ {
+ name: "CompressibleType2",
+ length: 1024,
+ in: { "Accept-Encoding": "gzip, deflate, br" },
+ out: { "Content-Type": "text/plain" },
+ expect: "gzip",
+ },
+ {
+ name: "CompressibleType3",
+ length: 1024,
+ in: { "Accept-Encoding": "br" },
+ out: { "Content-Type": "text/plain" },
+ expect: "br",
+ },
+ {
+ name: "IncompressibleRange",
+ length: 1024,
+ in: { "Accept-Encoding": "gzip" },
+ out: { "Content-Type": "text/plain", "Content-Range": "1" },
+ expect: null,
+ },
+ {
+ name: "IncompressibleCE",
+ length: 1024,
+ in: { "Accept-Encoding": "gzip" },
+ out: { "Content-Type": "text/plain", "Content-Encoding": "random" },
+ expect: null,
+ },
+ {
+ name: "IncompressibleCC",
+ length: 1024,
+ in: { "Accept-Encoding": "gzip" },
+ out: { "Content-Type": "text/plain", "Cache-Control": "no-transform" },
+ expect: null,
+ },
+ {
+ name: "BadHeader",
+ length: 1024,
+ in: { "Accept-Encoding": "\x81" },
+ out: { "Content-Type": "text/plain", "Cache-Control": "no-transform" },
+ expect: null,
+ },
+];
+
+for (const testCase of compressionTestCases) {
+ const name = `httpServerCompression${testCase.name}`;
+ Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ {
+ [name]: async function () {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+ const server = Deno.serve({
+ handler: async (_request) => {
+ const f = await makeTempFile(testCase.length);
+ deferred.resolve();
+ // deno-lint-ignore no-explicit-any
+ const headers = testCase.out as any;
+ headers["Content-Length"] = testCase.length.toString();
+ return new Response(f.readable, {
+ headers: headers as HeadersInit,
+ });
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+ try {
+ await listeningDeferred.promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ headers: testCase.in as HeadersInit,
+ });
+ await deferred.promise;
+ const body = await resp.arrayBuffer();
+ if (testCase.expect == null) {
+ assertEquals(body.byteLength, testCase.length);
+ assertEquals(
+ resp.headers.get("content-length"),
+ testCase.length.toString(),
+ );
+ assertEquals(
+ resp.headers.get("content-encoding"),
+ testCase.out["Content-Encoding"] || null,
+ );
+ } else if (testCase.expect == "gzip") {
+ // Note the fetch will transparently decompress this response, BUT we can detect that a response
+ // was compressed by the lack of a content length.
+ assertEquals(body.byteLength, testCase.length);
+ assertEquals(resp.headers.get("content-encoding"), null);
+ assertEquals(resp.headers.get("content-length"), null);
+ }
+ } finally {
+ ac.abort();
+ await server.finished;
+ }
+ },
+ }[name],
+ );
+}
+
+Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ async function httpServerPostFile() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: async (request) => {
+ assertEquals(
+ new Uint8Array(await request.arrayBuffer()),
+ makeTempData(70 * 1024),
+ );
+ deferred.resolve();
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const f = await makeTempFile(70 * 1024);
+ const response = await fetch(`http://localhost:${servePort}/`, {
+ method: "POST",
+ body: f.readable,
+ });
+
+ await deferred.promise;
+
+ assertEquals(response.status, 200);
+ assertEquals(await response.text(), "ok");
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+for (const delay of ["delay", "nodelay"]) {
+ for (const url of ["text", "file", "stream"]) {
+ // Ensure that we don't panic when the incoming TCP request was dropped
+ // https://github.com/denoland/deno/issues/20315 and that we correctly
+ // close/cancel the response
+ Deno.test({
+ permissions: { read: true, write: true, net: true },
+ name: `httpServerTcpCancellation_${url}_${delay}`,
+ fn: async function () {
+ const ac = new AbortController();
+ const streamCancelled = url == "stream"
+ ? Promise.withResolvers<void>()
+ : undefined;
+ const listeningDeferred = Promise.withResolvers<void>();
+ const waitForAbort = Promise.withResolvers<void>();
+ const waitForRequest = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ handler: async (req: Request) => {
+ let respBody = null;
+ if (req.url.includes("/text")) {
+ respBody = "text";
+ } else if (req.url.includes("/file")) {
+ respBody = (await makeTempFile(1024)).readable;
+ } else if (req.url.includes("/stream")) {
+ respBody = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new Uint8Array([1]));
+ },
+ cancel(reason) {
+ streamCancelled!.resolve(reason);
+ },
+ });
+ } else {
+ fail();
+ }
+ waitForRequest.resolve();
+ await waitForAbort.promise;
+
+ if (delay == "delay") {
+ await new Promise((r) => setTimeout(r, 1000));
+ }
+ // Allocate the request body
+ req.body;
+ return new Response(respBody);
+ },
+ });
+
+ await listeningDeferred.promise;
+
+ // Create a POST request and drop it once the server has received it
+ const conn = await Deno.connect({ port: servePort });
+ const writer = conn.writable.getWriter();
+ await writer.write(
+ new TextEncoder().encode(`POST /${url} HTTP/1.0\n\n`),
+ );
+ await waitForRequest.promise;
+ await writer.close();
+
+ waitForAbort.resolve();
+
+ // Wait for cancellation before we shut the server down
+ if (streamCancelled !== undefined) {
+ await streamCancelled;
+ }
+
+ // Since the handler has a chance of creating resources or running async
+ // ops, we need to use a graceful shutdown here to ensure they have fully
+ // drained.
+ await server.shutdown();
+
+ await server.finished;
+ },
+ });
+ }
+}
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerCancelFetch() {
+ const request2 = Promise.withResolvers<void>();
+ const request2Aborted = Promise.withResolvers<string>();
+ const { finished, abort } = await makeServer(async (req) => {
+ if (req.url.endsWith("/1")) {
+ const fetchRecursive = await fetch(`http://localhost:${servePort}/2`);
+ return new Response(fetchRecursive.body);
+ } else if (req.url.endsWith("/2")) {
+ request2.resolve();
+ return new Response(
+ new ReadableStream({
+ start(_controller) {/* just hang */},
+ cancel(reason) {
+ request2Aborted.resolve(reason);
+ },
+ }),
+ );
+ }
+ fail();
+ });
+ const fetchAbort = new AbortController();
+ const fetchPromise = await fetch(`http://localhost:${servePort}/1`, {
+ signal: fetchAbort.signal,
+ });
+ await fetchPromise;
+ await request2.promise;
+ fetchAbort.abort();
+ assertEquals("resource closed", await request2Aborted.promise);
+
+ abort();
+ await finished;
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function httpServerWithTls() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const hostname = "127.0.0.1";
+
+ const server = Deno.serve({
+ handler: () => new Response("Hello World"),
+ hostname,
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
+ key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
+ });
+
+ await promise;
+ const caCert = Deno.readTextFileSync("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const resp = await fetch(`https://localhost:${servePort}/`, {
+ client,
+ headers: { "connection": "close" },
+ });
+
+ const respBody = await resp.text();
+ assertEquals("Hello World", respBody);
+
+ client.close();
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ async function httpServerRequestCLTE() {
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const deferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: async (req) => {
+ assertEquals(await req.text(), "");
+ deferred.resolve();
+ return new Response("ok");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+
+ const body =
+ `POST / HTTP/1.1\r\nHost: example.domain\r\nContent-Length: 13\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nEXTRA`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await deferred.promise;
+
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ async function httpServerRequestTETE() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: () => {
+ throw new Error("oops");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ const variations = [
+ "Transfer-Encoding : chunked",
+ "Transfer-Encoding: xchunked",
+ "Transfer-Encoding: chunkedx",
+ "Transfer-Encoding\n: chunked",
+ ];
+
+ await promise;
+ for (const teHeader of variations) {
+ const conn = await Deno.connect({ port: servePort });
+ const body =
+ `POST / HTTP/1.1\r\nHost: example.domain\r\n${teHeader}\r\n\r\n0\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assert(readResult);
+ const msg = decoder.decode(buf.subarray(0, readResult));
+ assert(msg.includes("HTTP/1.1 400 Bad Request\r\n"));
+
+ conn.close();
+ }
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServer204ResponseDoesntSendContentLength() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const ac = new AbortController();
+ const server = Deno.serve({
+ handler: (_request) => new Response(null, { status: 204 }),
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ try {
+ await promise;
+ const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
+ method: "GET",
+ headers: { "connection": "close" },
+ });
+ assertEquals(resp.status, 204);
+ assertEquals(resp.headers.get("Content-Length"), null);
+ } finally {
+ ac.abort();
+ await server.finished;
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServer304ResponseDoesntSendBody() {
+ const deferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: () => {
+ deferred.resolve();
+ return new Response(null, { status: 304 });
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ const body =
+ `GET / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ await deferred.promise;
+
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assert(readResult);
+ const msg = decoder.decode(buf.subarray(0, readResult));
+
+ assert(msg.startsWith("HTTP/1.1 304 Not Modified"));
+ assert(msg.endsWith("\r\n\r\n"));
+
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerExpectContinue() {
+ const deferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: async (req) => {
+ deferred.resolve();
+ assertEquals(await req.text(), "hello");
+ return new Response(null, { status: 304 });
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ {
+ const body =
+ `POST / HTTP/1.1\r\nHost: example.domain\r\nExpect: 100-continue\r\nContent-Length: 5\r\nConnection: close\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ }
+
+ await deferred.promise;
+
+ {
+ const msgExpected = "HTTP/1.1 100 Continue\r\n\r\n";
+ const buf = new Uint8Array(encoder.encode(msgExpected).byteLength);
+ const readResult = await conn.read(buf);
+ assert(readResult);
+ const msg = decoder.decode(buf.subarray(0, readResult));
+ assert(msg.startsWith(msgExpected));
+ }
+
+ {
+ const body = "hello";
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ }
+
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assert(readResult);
+ const msg = decoder.decode(buf.subarray(0, readResult));
+
+ assert(msg.startsWith("HTTP/1.1 304 Not Modified"));
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerExpectContinueButNoBodyLOL() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve({
+ handler: async (req) => {
+ deferred.resolve();
+ assertEquals(await req.text(), "");
+ return new Response(null, { status: 304 });
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(listeningDeferred.resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await listeningDeferred.promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ {
+ // // no content-length or transfer-encoding means no body!
+ const body =
+ `POST / HTTP/1.1\r\nHost: example.domain\r\nExpect: 100-continue\r\nConnection: close\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ }
+
+ await deferred.promise;
+
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assert(readResult);
+ const msg = decoder.decode(buf.subarray(0, readResult));
+
+ assert(msg.startsWith("HTTP/1.1 304 Not Modified"));
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+const badRequests = [
+ ["weirdMethodName", "GE T / HTTP/1.1\r\n\r\n"],
+ ["illegalRequestLength", "POST / HTTP/1.1\r\nContent-Length: foo\r\n\r\n"],
+ ["illegalRequestLength2", "POST / HTTP/1.1\r\nContent-Length: -1\r\n\r\n"],
+ ["illegalRequestLength3", "POST / HTTP/1.1\r\nContent-Length: 1.1\r\n\r\n"],
+ ["illegalRequestLength4", "POST / HTTP/1.1\r\nContent-Length: 1.\r\n\r\n"],
+];
+
+for (const [name, req] of badRequests) {
+ const testFn = {
+ [name]: async () => {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: () => {
+ throw new Error("oops");
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ await promise;
+ const conn = await Deno.connect({ port: servePort });
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ {
+ const writeResult = await conn.write(encoder.encode(req));
+ assertEquals(req.length, writeResult);
+ }
+
+ const buf = new Uint8Array(100);
+ const readResult = await conn.read(buf);
+ assert(readResult);
+ const msg = decoder.decode(buf.subarray(0, readResult));
+
+ assert(msg.startsWith("HTTP/1.1 400 "));
+ conn.close();
+
+ ac.abort();
+ await server.finished;
+ },
+ }[name];
+
+ Deno.test(
+ { permissions: { net: true } },
+ testFn,
+ );
+}
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerConcurrentRequests() {
+ const ac = new AbortController();
+ const { resolve } = Promise.withResolvers<void>();
+
+ let reqCount = -1;
+ let timerId: number | undefined;
+ const server = Deno.serve({
+ handler: (_req) => {
+ reqCount++;
+ if (reqCount === 0) {
+ const msg = new TextEncoder().encode("data: hello\r\n\r\n");
+ // SSE
+ const body = new ReadableStream({
+ start(controller) {
+ timerId = setInterval(() => {
+ controller.enqueue(msg);
+ }, 1000);
+ },
+ cancel() {
+ if (typeof timerId === "number") {
+ clearInterval(timerId);
+ }
+ },
+ });
+ return new Response(body, {
+ headers: {
+ "Content-Type": "text/event-stream",
+ },
+ });
+ }
+
+ return new Response(`hello ${reqCount}`);
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ const sseRequest = await fetch(`http://localhost:${servePort}/`);
+
+ const decoder = new TextDecoder();
+ const stream = sseRequest.body!.getReader();
+ {
+ const { done, value } = await stream.read();
+ assert(!done);
+ assertEquals(decoder.decode(value), "data: hello\r\n\r\n");
+ }
+
+ const helloRequest = await fetch(`http://localhost:${servePort}/`);
+ assertEquals(helloRequest.status, 200);
+ assertEquals(await helloRequest.text(), "hello 1");
+
+ {
+ const { done, value } = await stream.read();
+ assert(!done);
+ assertEquals(decoder.decode(value), "data: hello\r\n\r\n");
+ }
+
+ await stream.cancel();
+ clearInterval(timerId);
+ ac.abort();
+ await server.finished;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function serveWithPrototypePollution() {
+ const originalThen = Promise.prototype.then;
+ const originalSymbolIterator = Array.prototype[Symbol.iterator];
+ try {
+ Promise.prototype.then = Array.prototype[Symbol.iterator] = () => {
+ throw new Error();
+ };
+ const ac = new AbortController();
+ const { resolve } = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ handler: (_req) => new Response("ok"),
+ hostname: "localhost",
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+ ac.abort();
+ await server.finished;
+ } finally {
+ Promise.prototype.then = originalThen;
+ Array.prototype[Symbol.iterator] = originalSymbolIterator;
+ }
+ },
+);
+
+// https://github.com/denoland/deno/issues/15549
+Deno.test(
+ { permissions: { net: true } },
+ async function testIssue15549() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let count = 0;
+ const server = Deno.serve({
+ async onListen({ port }: { port: number }) {
+ const res1 = await fetch(`http://localhost:${port}/`);
+ assertEquals(await res1.text(), "hello world 1");
+
+ const res2 = await fetch(`http://localhost:${port}/`);
+ assertEquals(await res2.text(), "hello world 2");
+
+ resolve();
+ ac.abort();
+ },
+ signal: ac.signal,
+ }, () => {
+ count++;
+ return new Response(`hello world ${count}`);
+ });
+
+ await promise;
+ await server.finished;
+ },
+);
+
+// https://github.com/denoland/deno/issues/15858
+Deno.test(
+ "Clone should work",
+ { permissions: { net: true } },
+ async function httpServerCanCloneRequest() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<number>();
+
+ const server = Deno.serve({
+ handler: async (req) => {
+ const cloned = req.clone();
+ assertEquals(req.headers, cloned.headers);
+
+ assertEquals(cloned.url, req.url);
+ assertEquals(cloned.cache, req.cache);
+ assertEquals(cloned.destination, req.destination);
+ assertEquals(cloned.headers, req.headers);
+ assertEquals(cloned.integrity, req.integrity);
+ assertEquals(cloned.isHistoryNavigation, req.isHistoryNavigation);
+ assertEquals(cloned.isReloadNavigation, req.isReloadNavigation);
+ assertEquals(cloned.keepalive, req.keepalive);
+ assertEquals(cloned.method, req.method);
+ assertEquals(cloned.mode, req.mode);
+ assertEquals(cloned.redirect, req.redirect);
+ assertEquals(cloned.referrer, req.referrer);
+ assertEquals(cloned.referrerPolicy, req.referrerPolicy);
+
+ // both requests can read body
+ await req.text();
+ await cloned.json();
+
+ return new Response("ok");
+ },
+ signal: ac.signal,
+ onListen: ({ port }: { port: number }) => resolve(port),
+ onError: createOnErrorCb(ac),
+ });
+
+ try {
+ const port = await promise;
+ const resp = await fetch(`http://localhost:${port}/`, {
+ headers: { connection: "close" },
+ method: "POST",
+ body: '{"sus":true}',
+ });
+ const text = await resp.text();
+ assertEquals(text, "ok");
+ } finally {
+ ac.abort();
+ await server.finished;
+ }
+ },
+);
+
+// https://fetch.spec.whatwg.org/#dom-request-clone
+Deno.test(
+ "Throw if disturbed",
+ { permissions: { net: true } },
+ async function shouldThrowIfBodyIsUnusableDisturbed() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<number>();
+
+ const server = Deno.serve({
+ handler: async (req) => {
+ await req.text();
+
+ try {
+ req.clone();
+ fail();
+ } catch (cloneError) {
+ assert(cloneError instanceof TypeError);
+ assert(
+ cloneError.message.endsWith("Body is unusable."),
+ );
+
+ ac.abort();
+ await server.finished;
+ }
+
+ return new Response("ok");
+ },
+ signal: ac.signal,
+ onListen: ({ port }: { port: number }) => resolve(port),
+ });
+
+ try {
+ const port = await promise;
+ await fetch(`http://localhost:${port}/`, {
+ headers: { connection: "close" },
+ method: "POST",
+ body: '{"bar":true}',
+ });
+ fail();
+ } catch (clientError) {
+ assert(clientError instanceof TypeError);
+ assert(
+ clientError.message.endsWith(
+ "connection closed before message completed",
+ ),
+ );
+ } finally {
+ ac.abort();
+ await server.finished;
+ }
+ },
+);
+
+// https://fetch.spec.whatwg.org/#dom-request-clone
+Deno.test({
+ name: "Throw if locked",
+ permissions: { net: true },
+ fn: async function shouldThrowIfBodyIsUnusableLocked() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<number>();
+
+ const server = Deno.serve({
+ handler: async (req) => {
+ const _reader = req.body?.getReader();
+
+ try {
+ req.clone();
+ fail();
+ } catch (cloneError) {
+ assert(cloneError instanceof TypeError);
+ assert(
+ cloneError.message.endsWith("Body is unusable."),
+ );
+
+ ac.abort();
+ await server.finished;
+ }
+ return new Response("ok");
+ },
+ signal: ac.signal,
+ onListen: ({ port }: { port: number }) => resolve(port),
+ });
+
+ try {
+ const port = await promise;
+ await fetch(`http://localhost:${port}/`, {
+ headers: { connection: "close" },
+ method: "POST",
+ body: '{"bar":true}',
+ });
+ fail();
+ } catch (clientError) {
+ assert(clientError instanceof TypeError);
+ assert(
+ clientError.message.endsWith(
+ "connection closed before message completed",
+ ),
+ );
+ } finally {
+ ac.abort();
+ await server.finished;
+ }
+ },
+});
+
+// Checks large streaming response
+// https://github.com/denoland/deno/issues/16567
+Deno.test(
+ { permissions: { net: true } },
+ async function testIssue16567() {
+ const ac = new AbortController();
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const server = Deno.serve({
+ async onListen({ port }) {
+ const res1 = await fetch(`http://localhost:${port}/`);
+ assertEquals((await res1.text()).length, 40 * 50_000);
+
+ resolve();
+ ac.abort();
+ },
+ signal: ac.signal,
+ }, () =>
+ new Response(
+ new ReadableStream({
+ start(c) {
+ // 2MB "a...a" response with 40 chunks
+ for (const _ of Array(40)) {
+ c.enqueue(new Uint8Array(50_000).fill(97));
+ }
+ c.close();
+ },
+ }),
+ ));
+
+ await promise;
+ await server.finished;
+ },
+);
+
+function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
+ // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6
+ const tp = new TextProtoReader(r);
+ let finished = false;
+ const chunks: Array<{
+ offset: number;
+ data: Uint8Array;
+ }> = [];
+ async function read(buf: Uint8Array): Promise<number | null> {
+ if (finished) return null;
+ const [chunk] = chunks;
+ if (chunk) {
+ const chunkRemaining = chunk.data.byteLength - chunk.offset;
+ const readLength = Math.min(chunkRemaining, buf.byteLength);
+ for (let i = 0; i < readLength; i++) {
+ buf[i] = chunk.data[chunk.offset + i];
+ }
+ chunk.offset += readLength;
+ if (chunk.offset === chunk.data.byteLength) {
+ chunks.shift();
+ // Consume \r\n;
+ if ((await tp.readLine()) === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ }
+ return readLength;
+ }
+ const line = await tp.readLine();
+ if (line === null) throw new Deno.errors.UnexpectedEof();
+ // TODO(bartlomieju): handle chunk extension
+ const [chunkSizeString] = line.split(";");
+ const chunkSize = parseInt(chunkSizeString, 16);
+ if (Number.isNaN(chunkSize) || chunkSize < 0) {
+ throw new Deno.errors.InvalidData("Invalid chunk size");
+ }
+ if (chunkSize > 0) {
+ if (chunkSize > buf.byteLength) {
+ let eof = await r.readFull(buf);
+ if (eof === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ const restChunk = new Uint8Array(chunkSize - buf.byteLength);
+ eof = await r.readFull(restChunk);
+ if (eof === null) {
+ throw new Deno.errors.UnexpectedEof();
+ } else {
+ chunks.push({
+ offset: 0,
+ data: restChunk,
+ });
+ }
+ return buf.byteLength;
+ } else {
+ const bufToFill = buf.subarray(0, chunkSize);
+ const eof = await r.readFull(bufToFill);
+ if (eof === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ // Consume \r\n
+ if ((await tp.readLine()) === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ return chunkSize;
+ }
+ } else {
+ assert(chunkSize === 0);
+ // Consume \r\n
+ if ((await r.readLine()) === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ await readTrailers(h, r);
+ finished = true;
+ return null;
+ }
+ }
+ return { read };
+}
+
+async function readTrailers(
+ headers: Headers,
+ r: BufReader,
+) {
+ const trailers = parseTrailer(headers.get("trailer"));
+ if (trailers == null) return;
+ const trailerNames = [...trailers.keys()];
+ const tp = new TextProtoReader(r);
+ const result = await tp.readMimeHeader();
+ if (result == null) {
+ throw new Deno.errors.InvalidData("Missing trailer header.");
+ }
+ const undeclared = [...result.keys()].filter(
+ (k) => !trailerNames.includes(k),
+ );
+ if (undeclared.length > 0) {
+ throw new Deno.errors.InvalidData(
+ `Undeclared trailers: ${Deno.inspect(undeclared)}.`,
+ );
+ }
+ for (const [k, v] of result) {
+ headers.append(k, v);
+ }
+ const missingTrailers = trailerNames.filter((k) => !result.has(k));
+ if (missingTrailers.length > 0) {
+ throw new Deno.errors.InvalidData(
+ `Missing trailers: ${Deno.inspect(missingTrailers)}.`,
+ );
+ }
+ headers.delete("trailer");
+}
+
+function parseTrailer(field: string | null): Headers | undefined {
+ if (field == null) {
+ return undefined;
+ }
+ const trailerNames = field.split(",").map((v) => v.trim().toLowerCase());
+ if (trailerNames.length === 0) {
+ throw new Deno.errors.InvalidData("Empty trailer header.");
+ }
+ const prohibited = trailerNames.filter((k) => isProhibitedForTrailer(k));
+ if (prohibited.length > 0) {
+ throw new Deno.errors.InvalidData(
+ `Prohibited trailer names: ${Deno.inspect(prohibited)}.`,
+ );
+ }
+ return new Headers(trailerNames.map((key) => [key, ""]));
+}
+
+function isProhibitedForTrailer(key: string): boolean {
+ const s = new Set(["transfer-encoding", "content-length", "trailer"]);
+ return s.has(key.toLowerCase());
+}
+
+// TODO(mmastrac): curl on Windows CI stopped supporting --http2?
+Deno.test(
+ {
+ permissions: { net: true, run: true },
+ ignore: Deno.build.os === "windows",
+ },
+ async function httpServeCurlH2C() {
+ const ac = new AbortController();
+ const server = Deno.serve(
+ { port: servePort, signal: ac.signal },
+ () => new Response("hello world!"),
+ );
+
+ assertEquals(
+ "hello world!",
+ await curlRequest([`http://localhost:${servePort}/path`]),
+ );
+ assertEquals(
+ "hello world!",
+ await curlRequest([`http://localhost:${servePort}/path`, "--http2"]),
+ );
+ assertEquals(
+ "hello world!",
+ await curlRequest([
+ `http://localhost:${servePort}/path`,
+ "--http2",
+ "--http2-prior-knowledge",
+ ]),
+ );
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+// TODO(mmastrac): This test should eventually use fetch, when we support trailers there.
+// This test is ignored because it's flaky and relies on cURL's verbose output.
+Deno.test(
+ { permissions: { net: true, run: true, read: true }, ignore: true },
+ async function httpServerTrailers() {
+ const ac = new AbortController();
+ const { resolve } = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: () => {
+ const response = new Response("Hello World", {
+ headers: {
+ "trailer": "baz",
+ "transfer-encoding": "chunked",
+ "foo": "bar",
+ },
+ });
+ addTrailers(response, [["baz", "why"]]);
+ return response;
+ },
+ port: servePort,
+ signal: ac.signal,
+ onListen: onListen(resolve),
+ onError: createOnErrorCb(ac),
+ });
+
+ // We don't have a great way to access this right now, so just fetch the trailers with cURL
+ const [_, stderr] = await curlRequestWithStdErr([
+ `http://localhost:${servePort}/path`,
+ "-v",
+ "--http2",
+ "--http2-prior-knowledge",
+ ]);
+ assertMatch(stderr, /baz: why/);
+ ac.abort();
+ await server.finished;
+ },
+);
+
+// TODO(mmastrac): curl on CI stopped supporting --http2?
+Deno.test(
+ {
+ permissions: {
+ net: true,
+ run: true,
+ read: true,
+ },
+ ignore: Deno.build.os === "windows",
+ },
+ async function httpsServeCurlH2C() {
+ const ac = new AbortController();
+ const server = Deno.serve(
+ {
+ signal: ac.signal,
+ port: servePort,
+ cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
+ key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
+ },
+ () => new Response("hello world!"),
+ );
+
+ assertEquals(
+ "hello world!",
+ await curlRequest([`https://localhost:${servePort}/path`, "-k"]),
+ );
+ assertEquals(
+ "hello world!",
+ await curlRequest([
+ `https://localhost:${servePort}/path`,
+ "-k",
+ "--http2",
+ ]),
+ );
+ assertEquals(
+ "hello world!",
+ await curlRequest([
+ `https://localhost:${servePort}/path`,
+ "-k",
+ "--http2",
+ "--http2-prior-knowledge",
+ ]),
+ );
+
+ ac.abort();
+ await server.finished;
+ },
+);
+
+async function curlRequest(args: string[]) {
+ const { success, stdout, stderr } = await new Deno.Command("curl", {
+ args,
+ stdout: "piped",
+ stderr: "piped",
+ }).output();
+ assert(
+ success,
+ `Failed to cURL ${args}: stdout\n\n${stdout}\n\nstderr:\n\n${stderr}`,
+ );
+ return new TextDecoder().decode(stdout);
+}
+
+async function curlRequestWithStdErr(args: string[]) {
+ const { success, stdout, stderr } = await new Deno.Command("curl", {
+ args,
+ stdout: "piped",
+ stderr: "piped",
+ }).output();
+ assert(
+ success,
+ `Failed to cURL ${args}: stdout\n\n${stdout}\n\nstderr:\n\n${stderr}`,
+ );
+ return [new TextDecoder().decode(stdout), new TextDecoder().decode(stderr)];
+}
+
+Deno.test("Deno.HttpServer is not thenable", async () => {
+ // deno-lint-ignore require-await
+ async function serveTest() {
+ const server = Deno.serve({ port: servePort }, (_) => new Response(""));
+ assert(!("then" in server));
+ return server;
+ }
+ const server = await serveTest();
+ await server.shutdown();
+});
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { run: true, read: true, write: true },
+ },
+ async function httpServerUnixDomainSocket() {
+ const { promise, resolve } = Promise.withResolvers<{ path: string }>();
+ const ac = new AbortController();
+ const filePath = tmpUnixSocketPath();
+ const server = Deno.serve(
+ {
+ signal: ac.signal,
+ path: filePath,
+ onListen(info) {
+ resolve(info);
+ },
+ onError: createOnErrorCb(ac),
+ },
+ (_req, { remoteAddr }) => {
+ assertEquals(remoteAddr, { path: filePath, transport: "unix" });
+ return new Response("hello world!");
+ },
+ );
+
+ assertEquals(await promise, { path: filePath });
+ assertEquals(
+ "hello world!",
+ await curlRequest(["--unix-socket", filePath, "http://localhost"]),
+ );
+ ac.abort();
+ await server.finished;
+ },
+);
+
+// serve Handler must return Response class or promise that resolves Response class
+Deno.test(
+ { permissions: { net: true, run: true } },
+ async function handleServeCallbackReturn() {
+ const deferred = Promise.withResolvers<void>();
+ const listeningDeferred = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve(
+ {
+ port: servePort,
+ onListen: onListen(listeningDeferred.resolve),
+ signal: ac.signal,
+ onError: (error) => {
+ assert(error instanceof TypeError);
+ assert(
+ error.message ===
+ "Return value from serve handler must be a response or a promise resolving to a response",
+ );
+ deferred.resolve();
+ return new Response("Customized Internal Error from onError");
+ },
+ },
+ () => {
+ // Trick the typechecker
+ return <Response> <unknown> undefined;
+ },
+ );
+ await listeningDeferred.promise;
+ const respText = await curlRequest([`http://localhost:${servePort}`]);
+ await deferred.promise;
+ ac.abort();
+ await server.finished;
+ assert(respText === "Customized Internal Error from onError");
+ },
+);
+
+// onError Handler must return Response class or promise that resolves Response class
+Deno.test(
+ { permissions: { net: true, run: true } },
+ async function handleServeErrorCallbackReturn() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const ac = new AbortController();
+
+ const server = Deno.serve(
+ {
+ port: servePort,
+ onListen: onListen(resolve),
+ signal: ac.signal,
+ onError: () => {
+ // Trick the typechecker
+ return <Response> <unknown> undefined;
+ },
+ },
+ () => {
+ // Trick the typechecker
+ return <Response> <unknown> undefined;
+ },
+ );
+ await promise;
+ const respText = await curlRequest([`http://localhost:${servePort}`]);
+ ac.abort();
+ await server.finished;
+ assert(respText === "Internal Server Error");
+ },
+);
diff --git a/tests/unit/signal_test.ts b/tests/unit/signal_test.ts
new file mode 100644
index 000000000..2ba2ffb15
--- /dev/null
+++ b/tests/unit/signal_test.ts
@@ -0,0 +1,296 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals, assertThrows, delay } from "./test_util.ts";
+
+Deno.test(
+ { ignore: Deno.build.os !== "windows" },
+ function signalsNotImplemented() {
+ const msg =
+ "Windows only supports ctrl-c (SIGINT) and ctrl-break (SIGBREAK).";
+ assertThrows(
+ () => {
+ Deno.addSignalListener("SIGALRM", () => {});
+ },
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => {
+ Deno.addSignalListener("SIGCHLD", () => {});
+ },
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => {
+ Deno.addSignalListener("SIGHUP", () => {});
+ },
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => {
+ Deno.addSignalListener("SIGIO", () => {});
+ },
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => {
+ Deno.addSignalListener("SIGPIPE", () => {});
+ },
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => {
+ Deno.addSignalListener("SIGQUIT", () => {});
+ },
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => {
+ Deno.addSignalListener("SIGTERM", () => {});
+ },
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => {
+ Deno.addSignalListener("SIGUSR1", () => {});
+ },
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => {
+ Deno.addSignalListener("SIGUSR2", () => {});
+ },
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => {
+ Deno.addSignalListener("SIGWINCH", () => {});
+ },
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGKILL", () => {}),
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGSTOP", () => {}),
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGILL", () => {}),
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGFPE", () => {}),
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGSEGV", () => {}),
+ Error,
+ msg,
+ );
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { run: true },
+ },
+ async function signalListenerTest() {
+ let c = 0;
+ const listener = () => {
+ c += 1;
+ };
+ // This test needs to be careful that it doesn't accidentally aggregate multiple
+ // signals into one. Sending two or more SIGxxx before the handler can be run will
+ // result in signal coalescing.
+ Deno.addSignalListener("SIGUSR1", listener);
+ // Sends SIGUSR1 3 times.
+ for (let i = 1; i <= 3; i++) {
+ await delay(1);
+ Deno.kill(Deno.pid, "SIGUSR1");
+ while (c < i) {
+ await delay(20);
+ }
+ }
+ Deno.removeSignalListener("SIGUSR1", listener);
+ await delay(100);
+ assertEquals(c, 3);
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { run: true },
+ },
+ async function multipleSignalListenerTest() {
+ let c = "";
+ const listener0 = () => {
+ c += "0";
+ };
+ const listener1 = () => {
+ c += "1";
+ };
+ // This test needs to be careful that it doesn't accidentally aggregate multiple
+ // signals into one. Sending two or more SIGxxx before the handler can be run will
+ // result in signal coalescing.
+ Deno.addSignalListener("SIGUSR2", listener0);
+ Deno.addSignalListener("SIGUSR2", listener1);
+
+ // Sends SIGUSR2 3 times.
+ for (let i = 1; i <= 3; i++) {
+ await delay(1);
+ Deno.kill(Deno.pid, "SIGUSR2");
+ while (c.length < i * 2) {
+ await delay(20);
+ }
+ }
+
+ Deno.removeSignalListener("SIGUSR2", listener1);
+
+ // Sends SIGUSR2 3 times.
+ for (let i = 1; i <= 3; i++) {
+ await delay(1);
+ Deno.kill(Deno.pid, "SIGUSR2");
+ while (c.length < 6 + i) {
+ await delay(20);
+ }
+ }
+
+ // Sends SIGUSR1 (irrelevant signal) 3 times.
+ for (const _ of Array(3)) {
+ await delay(20);
+ Deno.kill(Deno.pid, "SIGUSR1");
+ }
+
+ // No change
+ assertEquals(c, "010101000");
+
+ Deno.removeSignalListener("SIGUSR2", listener0);
+
+ await delay(100);
+
+ // The first 3 events are handled by both handlers
+ // The last 3 events are handled only by handler0
+ assertEquals(c, "010101000");
+ },
+);
+
+// This tests that pending op_signal_poll doesn't block the runtime from exiting the process.
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ },
+ async function canExitWhileListeningToSignal() {
+ const { code } = await new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "--unstable",
+ "Deno.addSignalListener('SIGINT', () => {})",
+ ],
+ }).output();
+ assertEquals(code, 0);
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os !== "windows",
+ permissions: { run: true },
+ },
+ function windowsThrowsOnNegativeProcessIdTest() {
+ assertThrows(
+ () => {
+ Deno.kill(-1, "SIGKILL");
+ },
+ TypeError,
+ "Invalid pid",
+ );
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os !== "windows",
+ permissions: { run: true },
+ },
+ function noOpenSystemIdleProcessTest() {
+ let signal: Deno.Signal = "SIGKILL";
+
+ assertThrows(
+ () => {
+ Deno.kill(0, signal);
+ },
+ TypeError,
+ `Invalid pid`,
+ );
+
+ signal = "SIGTERM";
+ assertThrows(
+ () => {
+ Deno.kill(0, signal);
+ },
+ TypeError,
+ `Invalid pid`,
+ );
+ },
+);
+
+Deno.test(function signalInvalidHandlerTest() {
+ assertThrows(() => {
+ // deno-lint-ignore no-explicit-any
+ Deno.addSignalListener("SIGINT", "handler" as any);
+ });
+ assertThrows(() => {
+ // deno-lint-ignore no-explicit-any
+ Deno.removeSignalListener("SIGINT", "handler" as any);
+ });
+});
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { run: true },
+ },
+ function signalForbiddenSignalTest() {
+ assertThrows(
+ () => Deno.addSignalListener("SIGKILL", () => {}),
+ TypeError,
+ "Binding to signal 'SIGKILL' is not allowed",
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGSTOP", () => {}),
+ TypeError,
+ "Binding to signal 'SIGSTOP' is not allowed",
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGILL", () => {}),
+ TypeError,
+ "Binding to signal 'SIGILL' is not allowed",
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGFPE", () => {}),
+ TypeError,
+ "Binding to signal 'SIGFPE' is not allowed",
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGSEGV", () => {}),
+ TypeError,
+ "Binding to signal 'SIGSEGV' is not allowed",
+ );
+ },
+);
diff --git a/tests/unit/stat_test.ts b/tests/unit/stat_test.ts
new file mode 100644
index 000000000..6882edf25
--- /dev/null
+++ b/tests/unit/stat_test.ts
@@ -0,0 +1,342 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+ pathToAbsoluteFileUrl,
+} from "./test_util.ts";
+
+Deno.test({ permissions: { read: true } }, function fstatSyncSuccess() {
+ using file = Deno.openSync("README.md");
+ const fileInfo = Deno.fstatSync(file.rid);
+ assert(fileInfo.isFile);
+ assert(!fileInfo.isSymlink);
+ assert(!fileInfo.isDirectory);
+ assert(fileInfo.size);
+ assert(fileInfo.atime);
+ assert(fileInfo.mtime);
+ // The `birthtime` field is not available on Linux before kernel version 4.11.
+ assert(fileInfo.birthtime || Deno.build.os === "linux");
+});
+
+Deno.test({ permissions: { read: true } }, async function fstatSuccess() {
+ using file = await Deno.open("README.md");
+ const fileInfo = await Deno.fstat(file.rid);
+ assert(fileInfo.isFile);
+ assert(!fileInfo.isSymlink);
+ assert(!fileInfo.isDirectory);
+ assert(fileInfo.size);
+ assert(fileInfo.atime);
+ assert(fileInfo.mtime);
+ // The `birthtime` field is not available on Linux before kernel version 4.11.
+ assert(fileInfo.birthtime || Deno.build.os === "linux");
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function statSyncSuccess() {
+ const readmeInfo = Deno.statSync("README.md");
+ assert(readmeInfo.isFile);
+ assert(!readmeInfo.isSymlink);
+
+ const modulesInfo = Deno.statSync("tests/testdata/symlink_to_subdir");
+ assert(modulesInfo.isDirectory);
+ assert(!modulesInfo.isSymlink);
+
+ const testsInfo = Deno.statSync("tests");
+ assert(testsInfo.isDirectory);
+ assert(!testsInfo.isSymlink);
+
+ const tempFile = Deno.makeTempFileSync();
+ const tempInfo = Deno.statSync(tempFile);
+ let now = Date.now();
+ assert(tempInfo.atime !== null && now - tempInfo.atime.valueOf() < 1000);
+ assert(tempInfo.mtime !== null && now - tempInfo.mtime.valueOf() < 1000);
+ assert(
+ tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000,
+ );
+
+ const readmeInfoByUrl = Deno.statSync(pathToAbsoluteFileUrl("README.md"));
+ assert(readmeInfoByUrl.isFile);
+ assert(!readmeInfoByUrl.isSymlink);
+
+ const modulesInfoByUrl = Deno.statSync(
+ pathToAbsoluteFileUrl("tests/testdata/symlink_to_subdir"),
+ );
+ assert(modulesInfoByUrl.isDirectory);
+ assert(!modulesInfoByUrl.isSymlink);
+
+ const testsInfoByUrl = Deno.statSync(pathToAbsoluteFileUrl("tests"));
+ assert(testsInfoByUrl.isDirectory);
+ assert(!testsInfoByUrl.isSymlink);
+
+ const tempFileForUrl = Deno.makeTempFileSync();
+ const tempInfoByUrl = Deno.statSync(
+ new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempFileForUrl}`,
+ ),
+ );
+ now = Date.now();
+ assert(
+ tempInfoByUrl.atime !== null &&
+ now - tempInfoByUrl.atime.valueOf() < 1000,
+ );
+ assert(
+ tempInfoByUrl.mtime !== null &&
+ now - tempInfoByUrl.mtime.valueOf() < 1000,
+ );
+ assert(
+ tempInfoByUrl.birthtime === null ||
+ now - tempInfoByUrl.birthtime.valueOf() < 1000,
+ );
+
+ Deno.removeSync(tempFile, { recursive: true });
+ Deno.removeSync(tempFileForUrl, { recursive: true });
+ },
+);
+
+Deno.test({ permissions: { read: false } }, function statSyncPerm() {
+ assertThrows(() => {
+ Deno.statSync("README.md");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, function statSyncNotFound() {
+ assertThrows(
+ () => {
+ Deno.statSync("bad_file_name");
+ },
+ Deno.errors.NotFound,
+ `stat 'bad_file_name'`,
+ );
+});
+
+Deno.test({ permissions: { read: true } }, function lstatSyncSuccess() {
+ const packageInfo = Deno.lstatSync("README.md");
+ assert(packageInfo.isFile);
+ assert(!packageInfo.isSymlink);
+
+ const packageInfoByUrl = Deno.lstatSync(pathToAbsoluteFileUrl("README.md"));
+ assert(packageInfoByUrl.isFile);
+ assert(!packageInfoByUrl.isSymlink);
+
+ const modulesInfo = Deno.lstatSync("tests/testdata/symlink_to_subdir");
+ assert(!modulesInfo.isDirectory);
+ assert(modulesInfo.isSymlink);
+
+ const modulesInfoByUrl = Deno.lstatSync(
+ pathToAbsoluteFileUrl("tests/testdata/symlink_to_subdir"),
+ );
+ assert(!modulesInfoByUrl.isDirectory);
+ assert(modulesInfoByUrl.isSymlink);
+
+ const coreInfo = Deno.lstatSync("cli");
+ assert(coreInfo.isDirectory);
+ assert(!coreInfo.isSymlink);
+
+ const coreInfoByUrl = Deno.lstatSync(pathToAbsoluteFileUrl("cli"));
+ assert(coreInfoByUrl.isDirectory);
+ assert(!coreInfoByUrl.isSymlink);
+});
+
+Deno.test({ permissions: { read: false } }, function lstatSyncPerm() {
+ assertThrows(() => {
+ Deno.lstatSync("assets/hello.txt");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, function lstatSyncNotFound() {
+ assertThrows(
+ () => {
+ Deno.lstatSync("bad_file_name");
+ },
+ Deno.errors.NotFound,
+ `stat 'bad_file_name'`,
+ );
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function statSuccess() {
+ const readmeInfo = await Deno.stat("README.md");
+ assert(readmeInfo.isFile);
+ assert(!readmeInfo.isSymlink);
+
+ const readmeInfoByUrl = await Deno.stat(
+ pathToAbsoluteFileUrl("README.md"),
+ );
+ assert(readmeInfoByUrl.isFile);
+ assert(!readmeInfoByUrl.isSymlink);
+
+ const modulesInfo = await Deno.stat("tests/testdata/symlink_to_subdir");
+ assert(modulesInfo.isDirectory);
+ assert(!modulesInfo.isSymlink);
+
+ const modulesInfoByUrl = await Deno.stat(
+ pathToAbsoluteFileUrl("tests/testdata/symlink_to_subdir"),
+ );
+ assert(modulesInfoByUrl.isDirectory);
+ assert(!modulesInfoByUrl.isSymlink);
+
+ const testsInfo = await Deno.stat("tests");
+ assert(testsInfo.isDirectory);
+ assert(!testsInfo.isSymlink);
+
+ const testsInfoByUrl = await Deno.stat(pathToAbsoluteFileUrl("tests"));
+ assert(testsInfoByUrl.isDirectory);
+ assert(!testsInfoByUrl.isSymlink);
+
+ const tempFile = await Deno.makeTempFile();
+ const tempInfo = await Deno.stat(tempFile);
+ let now = Date.now();
+ assert(tempInfo.atime !== null && now - tempInfo.atime.valueOf() < 1000);
+ assert(tempInfo.mtime !== null && now - tempInfo.mtime.valueOf() < 1000);
+
+ assert(
+ tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000,
+ );
+
+ const tempFileForUrl = await Deno.makeTempFile();
+ const tempInfoByUrl = await Deno.stat(
+ new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempFileForUrl}`,
+ ),
+ );
+ now = Date.now();
+ assert(
+ tempInfoByUrl.atime !== null &&
+ now - tempInfoByUrl.atime.valueOf() < 1000,
+ );
+ assert(
+ tempInfoByUrl.mtime !== null &&
+ now - tempInfoByUrl.mtime.valueOf() < 1000,
+ );
+ assert(
+ tempInfoByUrl.birthtime === null ||
+ now - tempInfoByUrl.birthtime.valueOf() < 1000,
+ );
+
+ Deno.removeSync(tempFile, { recursive: true });
+ Deno.removeSync(tempFileForUrl, { recursive: true });
+ },
+);
+
+Deno.test({ permissions: { read: false } }, async function statPerm() {
+ await assertRejects(async () => {
+ await Deno.stat("README.md");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, async function statNotFound() {
+ await assertRejects(
+ async () => {
+ await Deno.stat("bad_file_name");
+ },
+ Deno.errors.NotFound,
+ `stat 'bad_file_name'`,
+ );
+});
+
+Deno.test({ permissions: { read: true } }, async function lstatSuccess() {
+ const readmeInfo = await Deno.lstat("README.md");
+ assert(readmeInfo.isFile);
+ assert(!readmeInfo.isSymlink);
+
+ const readmeInfoByUrl = await Deno.lstat(pathToAbsoluteFileUrl("README.md"));
+ assert(readmeInfoByUrl.isFile);
+ assert(!readmeInfoByUrl.isSymlink);
+
+ const modulesInfo = await Deno.lstat("tests/testdata/symlink_to_subdir");
+ assert(!modulesInfo.isDirectory);
+ assert(modulesInfo.isSymlink);
+
+ const modulesInfoByUrl = await Deno.lstat(
+ pathToAbsoluteFileUrl("tests/testdata/symlink_to_subdir"),
+ );
+ assert(!modulesInfoByUrl.isDirectory);
+ assert(modulesInfoByUrl.isSymlink);
+
+ const coreInfo = await Deno.lstat("cli");
+ assert(coreInfo.isDirectory);
+ assert(!coreInfo.isSymlink);
+
+ const coreInfoByUrl = await Deno.lstat(pathToAbsoluteFileUrl("cli"));
+ assert(coreInfoByUrl.isDirectory);
+ assert(!coreInfoByUrl.isSymlink);
+});
+
+Deno.test({ permissions: { read: false } }, async function lstatPerm() {
+ await assertRejects(async () => {
+ await Deno.lstat("README.md");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { read: true } }, async function lstatNotFound() {
+ await assertRejects(
+ async () => {
+ await Deno.lstat("bad_file_name");
+ },
+ Deno.errors.NotFound,
+ `stat 'bad_file_name'`,
+ );
+});
+
+Deno.test(
+ {
+ ignore: Deno.build.os !== "windows",
+ permissions: { read: true, write: true },
+ },
+ function statNoUnixFields() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+ const filename = tempDir + "/test.txt";
+ Deno.writeFileSync(filename, data, { mode: 0o666 });
+ const s = Deno.statSync(filename);
+ assert(s.dev !== 0);
+ assert(s.ino === null);
+ assert(s.mode === null);
+ assert(s.nlink === null);
+ assert(s.uid === null);
+ assert(s.gid === null);
+ assert(s.rdev === null);
+ assert(s.blksize === null);
+ assert(s.blocks === null);
+ assert(s.isBlockDevice === null);
+ assert(s.isCharDevice === null);
+ assert(s.isFifo === null);
+ assert(s.isSocket === null);
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ function statUnixFields() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+ const filename = tempDir + "/test.txt";
+ const filename2 = tempDir + "/test2.txt";
+ Deno.writeFileSync(filename, data, { mode: 0o666 });
+ // Create a link
+ Deno.linkSync(filename, filename2);
+ const s = Deno.statSync(filename);
+ assert(s.dev !== null);
+ assert(s.ino !== null);
+ assertEquals(s.mode! & 0o666, 0o666);
+ assertEquals(s.nlink, 2);
+ assert(s.uid !== null);
+ assert(s.gid !== null);
+ assert(s.rdev !== null);
+ assert(s.blksize !== null);
+ assert(s.blocks !== null);
+ assert(!s.isBlockDevice);
+ assert(!s.isCharDevice);
+ assert(!s.isFifo);
+ assert(!s.isSocket);
+ },
+);
diff --git a/tests/unit/stdio_test.ts b/tests/unit/stdio_test.ts
new file mode 100644
index 000000000..d24fdc8ef
--- /dev/null
+++ b/tests/unit/stdio_test.ts
@@ -0,0 +1,32 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+Deno.test(async function stdioStdinRead() {
+ const nread = await Deno.stdin.read(new Uint8Array(0));
+ assertEquals(nread, 0);
+});
+
+Deno.test(function stdioStdinReadSync() {
+ const nread = Deno.stdin.readSync(new Uint8Array(0));
+ assertEquals(nread, 0);
+});
+
+Deno.test(async function stdioStdoutWrite() {
+ const nwritten = await Deno.stdout.write(new Uint8Array(0));
+ assertEquals(nwritten, 0);
+});
+
+Deno.test(function stdioStdoutWriteSync() {
+ const nwritten = Deno.stdout.writeSync(new Uint8Array(0));
+ assertEquals(nwritten, 0);
+});
+
+Deno.test(async function stdioStderrWrite() {
+ const nwritten = await Deno.stderr.write(new Uint8Array(0));
+ assertEquals(nwritten, 0);
+});
+
+Deno.test(function stdioStderrWriteSync() {
+ const nwritten = Deno.stderr.writeSync(new Uint8Array(0));
+ assertEquals(nwritten, 0);
+});
diff --git a/tests/unit/streams_test.ts b/tests/unit/streams_test.ts
new file mode 100644
index 000000000..6db9f666c
--- /dev/null
+++ b/tests/unit/streams_test.ts
@@ -0,0 +1,478 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals, fail } from "./test_util.ts";
+
+const {
+ core,
+ resourceForReadableStream,
+ // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
+} = Deno[Deno.internal];
+
+const LOREM =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
+
+// Hello world, with optional close
+function helloWorldStream(
+ close?: boolean,
+ cancelResolve?: (value: unknown) => void,
+) {
+ return new ReadableStream({
+ start(controller) {
+ controller.enqueue("hello, world");
+ if (close == true) {
+ controller.close();
+ }
+ },
+ cancel(reason) {
+ if (cancelResolve != undefined) {
+ cancelResolve(reason);
+ }
+ },
+ }).pipeThrough(new TextEncoderStream());
+}
+
+// Hello world, with optional close
+function errorStream(type: "string" | "controller" | "TypeError") {
+ return new ReadableStream({
+ start(controller) {
+ controller.enqueue("hello, world");
+ },
+ pull(controller) {
+ if (type == "string") {
+ throw "Uh oh (string)!";
+ }
+ if (type == "TypeError") {
+ throw TypeError("Uh oh (TypeError)!");
+ }
+ controller.error("Uh oh (controller)!");
+ },
+ }).pipeThrough(new TextEncoderStream());
+}
+
+// Long stream with Lorem Ipsum text.
+function longStream() {
+ return new ReadableStream({
+ start(controller) {
+ for (let i = 0; i < 4; i++) {
+ setTimeout(() => {
+ controller.enqueue(LOREM);
+ if (i == 3) {
+ controller.close();
+ }
+ }, i * 100);
+ }
+ },
+ }).pipeThrough(new TextEncoderStream());
+}
+
+// Long stream with Lorem Ipsum text.
+function longAsyncStream(cancelResolve?: (value: unknown) => void) {
+ let currentTimeout: number | undefined = undefined;
+ return new ReadableStream({
+ async start(controller) {
+ for (let i = 0; i < 100; i++) {
+ await new Promise((r) => currentTimeout = setTimeout(r, 1));
+ currentTimeout = undefined;
+ controller.enqueue(LOREM);
+ }
+ controller.close();
+ },
+ cancel(reason) {
+ if (cancelResolve != undefined) {
+ cancelResolve(reason);
+ }
+ if (currentTimeout !== undefined) {
+ clearTimeout(currentTimeout);
+ }
+ },
+ }).pipeThrough(new TextEncoderStream());
+}
+
+// Empty stream, closes either immediately or on a call to pull.
+function emptyStream(onPull: boolean) {
+ return new ReadableStream({
+ start(controller) {
+ if (!onPull) {
+ controller.close();
+ }
+ },
+ pull(controller) {
+ if (onPull) {
+ controller.close();
+ }
+ },
+ }).pipeThrough(new TextEncoderStream());
+}
+
+function largePacketStream(packetSize: number, count: number) {
+ return new ReadableStream({
+ pull(controller) {
+ if (count-- > 0) {
+ const buffer = new Uint8Array(packetSize);
+ for (let i = 0; i < 256; i++) {
+ buffer[i * (packetSize / 256)] = i;
+ }
+ controller.enqueue(buffer);
+ } else {
+ controller.close();
+ }
+ },
+ });
+}
+
+// Include an empty chunk
+function emptyChunkStream() {
+ return new ReadableStream({
+ start(controller) {
+ controller.enqueue(new Uint8Array([1]));
+ controller.enqueue(new Uint8Array([]));
+ controller.enqueue(new Uint8Array([2]));
+ controller.close();
+ },
+ });
+}
+
+// Try to blow up any recursive reads.
+function veryLongTinyPacketStream(length: number) {
+ return new ReadableStream({
+ start(controller) {
+ for (let i = 0; i < length; i++) {
+ controller.enqueue(new Uint8Array([1]));
+ }
+ controller.close();
+ },
+ });
+}
+
+// Creates a stream with the given number of packets, a configurable delay between packets, and a final
+// action (either "Throw" or "Close").
+function makeStreamWithCount(
+ count: number,
+ delay: number,
+ action: "Throw" | "Close",
+): ReadableStream {
+ function doAction(controller: ReadableStreamDefaultController, i: number) {
+ if (i == count) {
+ if (action == "Throw") {
+ controller.error(new Error("Expected error!"));
+ } else {
+ controller.close();
+ }
+ } else {
+ controller.enqueue(String.fromCharCode("a".charCodeAt(0) + i));
+
+ if (delay == 0) {
+ doAction(controller, i + 1);
+ } else {
+ setTimeout(() => doAction(controller, i + 1), delay);
+ }
+ }
+ }
+
+ return new ReadableStream({
+ start(controller) {
+ if (delay == 0) {
+ doAction(controller, 0);
+ } else {
+ setTimeout(() => doAction(controller, 0), delay);
+ }
+ },
+ }).pipeThrough(new TextEncoderStream());
+}
+
+// Normal stream operation
+Deno.test(async function readableStream() {
+ const rid = resourceForReadableStream(helloWorldStream());
+ const buffer = new Uint8Array(1024);
+ const nread = await core.read(rid, buffer);
+ assertEquals(nread, 12);
+ core.close(rid);
+});
+
+// Close the stream after reading everything
+Deno.test(async function readableStreamClose() {
+ const cancel = Promise.withResolvers();
+ const rid = resourceForReadableStream(
+ helloWorldStream(false, cancel.resolve),
+ );
+ const buffer = new Uint8Array(1024);
+ const nread = await core.read(rid, buffer);
+ assertEquals(nread, 12);
+ core.close(rid);
+ assertEquals(await cancel.promise, "resource closed");
+});
+
+// Close the stream without reading everything
+Deno.test(async function readableStreamClosePartialRead() {
+ const cancel = Promise.withResolvers();
+ const rid = resourceForReadableStream(
+ helloWorldStream(false, cancel.resolve),
+ );
+ const buffer = new Uint8Array(5);
+ const nread = await core.read(rid, buffer);
+ assertEquals(nread, 5);
+ core.close(rid);
+ assertEquals(await cancel.promise, "resource closed");
+});
+
+// Close the stream without reading anything
+Deno.test(async function readableStreamCloseWithoutRead() {
+ const cancel = Promise.withResolvers();
+ const rid = resourceForReadableStream(
+ helloWorldStream(false, cancel.resolve),
+ );
+ core.close(rid);
+ assertEquals(await cancel.promise, "resource closed");
+});
+
+// Close the stream without reading anything
+Deno.test(async function readableStreamCloseWithoutRead2() {
+ const cancel = Promise.withResolvers();
+ const rid = resourceForReadableStream(longAsyncStream(cancel.resolve));
+ core.close(rid);
+ assertEquals(await cancel.promise, "resource closed");
+});
+
+Deno.test(async function readableStreamPartial() {
+ const rid = resourceForReadableStream(helloWorldStream());
+ const buffer = new Uint8Array(5);
+ const nread = await core.read(rid, buffer);
+ assertEquals(nread, 5);
+ const buffer2 = new Uint8Array(1024);
+ const nread2 = await core.read(rid, buffer2);
+ assertEquals(nread2, 7);
+ core.close(rid);
+});
+
+Deno.test(async function readableStreamLongReadAll() {
+ const rid = resourceForReadableStream(longStream());
+ const buffer = await core.readAll(rid);
+ assertEquals(buffer.length, LOREM.length * 4);
+ core.close(rid);
+});
+
+Deno.test(async function readableStreamLongAsyncReadAll() {
+ const rid = resourceForReadableStream(longAsyncStream());
+ const buffer = await core.readAll(rid);
+ assertEquals(buffer.length, LOREM.length * 100);
+ core.close(rid);
+});
+
+Deno.test(async function readableStreamVeryLongReadAll() {
+ const rid = resourceForReadableStream(veryLongTinyPacketStream(1_000_000));
+ const buffer = await core.readAll(rid);
+ assertEquals(buffer.length, 1_000_000);
+ core.close(rid);
+});
+
+Deno.test(async function readableStreamLongByPiece() {
+ const rid = resourceForReadableStream(longStream());
+ let total = 0;
+ for (let i = 0; i < 100; i++) {
+ const length = await core.read(rid, new Uint8Array(16));
+ total += length;
+ if (length == 0) {
+ break;
+ }
+ }
+ assertEquals(total, LOREM.length * 4);
+ core.close(rid);
+});
+
+for (
+ const type of [
+ "string",
+ "TypeError",
+ "controller",
+ ] as ("string" | "TypeError" | "controller")[]
+) {
+ Deno.test(`readableStreamError_${type}`, async function () {
+ const rid = resourceForReadableStream(errorStream(type));
+ let nread;
+ try {
+ nread = await core.read(rid, new Uint8Array(16));
+ } catch (_) {
+ fail("Should not have thrown");
+ }
+ assertEquals(12, nread);
+ try {
+ await core.read(rid, new Uint8Array(1));
+ fail();
+ } catch (e) {
+ assertEquals(e.message, `Uh oh (${type})!`);
+ }
+ core.close(rid);
+ });
+}
+
+Deno.test(async function readableStreamEmptyOnStart() {
+ const rid = resourceForReadableStream(emptyStream(true));
+ const buffer = new Uint8Array(1024);
+ const nread = await core.read(rid, buffer);
+ assertEquals(nread, 0);
+ core.close(rid);
+});
+
+Deno.test(async function readableStreamEmptyOnPull() {
+ const rid = resourceForReadableStream(emptyStream(false));
+ const buffer = new Uint8Array(1024);
+ const nread = await core.read(rid, buffer);
+ assertEquals(nread, 0);
+ core.close(rid);
+});
+
+Deno.test(async function readableStreamEmptyReadAll() {
+ const rid = resourceForReadableStream(emptyStream(false));
+ const buffer = await core.readAll(rid);
+ assertEquals(buffer.length, 0);
+ core.close(rid);
+});
+
+Deno.test(async function readableStreamWithEmptyChunk() {
+ const rid = resourceForReadableStream(emptyChunkStream());
+ const buffer = await core.readAll(rid);
+ assertEquals(buffer, new Uint8Array([1, 2]));
+ core.close(rid);
+});
+
+Deno.test(async function readableStreamWithEmptyChunkOneByOne() {
+ const rid = resourceForReadableStream(emptyChunkStream());
+ assertEquals(1, await core.read(rid, new Uint8Array(1)));
+ assertEquals(1, await core.read(rid, new Uint8Array(1)));
+ assertEquals(0, await core.read(rid, new Uint8Array(1)));
+ core.close(rid);
+});
+
+// Ensure that we correctly transmit all the sub-chunks of the larger chunks.
+Deno.test(async function readableStreamReadSmallerChunks() {
+ const packetSize = 16 * 1024;
+ const rid = resourceForReadableStream(largePacketStream(packetSize, 1));
+ const buffer = new Uint8Array(packetSize);
+ for (let i = 0; i < packetSize / 1024; i++) {
+ await core.read(rid, buffer.subarray(i * 1024, i * 1024 + 1024));
+ }
+ for (let i = 0; i < 256; i++) {
+ assertEquals(
+ i,
+ buffer[i * (packetSize / 256)],
+ `at index ${i * (packetSize / 256)}`,
+ );
+ }
+ core.close(rid);
+});
+
+Deno.test(async function readableStreamLargePackets() {
+ const packetSize = 128 * 1024;
+ const rid = resourceForReadableStream(largePacketStream(packetSize, 1024));
+ for (let i = 0; i < 1024; i++) {
+ const buffer = new Uint8Array(packetSize);
+ assertEquals(packetSize, await core.read(rid, buffer));
+ for (let i = 0; i < 256; i++) {
+ assertEquals(
+ i,
+ buffer[i * (packetSize / 256)],
+ `at index ${i * (packetSize / 256)}`,
+ );
+ }
+ }
+ assertEquals(0, await core.read(rid, new Uint8Array(1)));
+ core.close(rid);
+});
+
+Deno.test(async function readableStreamVeryLargePackets() {
+ // 1024 packets of 1MB
+ const rid = resourceForReadableStream(largePacketStream(1024 * 1024, 1024));
+ let total = 0;
+ // Read 96kB up to 12,288 times (96kB is not an even multiple of the 1MB packet size to test this)
+ const readCounts: Record<number, number> = {};
+ for (let i = 0; i < 12 * 1024; i++) {
+ const nread = await core.read(rid, new Uint8Array(96 * 1024));
+ total += nread;
+ readCounts[nread] = (readCounts[nread] || 0) + 1;
+ if (nread == 0) {
+ break;
+ }
+ }
+ assertEquals({ 0: 1, 65536: 1024, 98304: 10 * 1024 }, readCounts);
+ assertEquals(total, 1024 * 1024 * 1024);
+ core.close(rid);
+});
+
+for (const count of [0, 1, 2, 3]) {
+ for (const delay of [0, 1, 10]) {
+ // Creating a stream that errors in start will throw
+ if (delay > 0) {
+ createStreamTest(count, delay, "Throw");
+ }
+ createStreamTest(count, delay, "Close");
+ }
+}
+
+function createStreamTest(
+ count: number,
+ delay: number,
+ action: "Throw" | "Close",
+) {
+ Deno.test(`streamCount${count}Delay${delay}${action}`, async () => {
+ let rid;
+ try {
+ rid = resourceForReadableStream(
+ makeStreamWithCount(count, delay, action),
+ );
+ for (let i = 0; i < count; i++) {
+ const buffer = new Uint8Array(1);
+ await core.read(rid, buffer);
+ }
+ if (action == "Throw") {
+ try {
+ const buffer = new Uint8Array(1);
+ assertEquals(1, await core.read(rid, buffer));
+ fail();
+ } catch (e) {
+ // We expect this to be thrown
+ assertEquals(e.message, "Expected error!");
+ }
+ } else {
+ const buffer = new Uint8Array(1);
+ assertEquals(0, await core.read(rid, buffer));
+ }
+ } finally {
+ core.close(rid);
+ }
+ });
+}
+
+// 1024 is the size of the internal packet buffer -- we want to make sure we fill the internal pipe fully.
+for (const packetCount of [1, 1024]) {
+ Deno.test(`readableStreamWithAggressiveResourceClose_${packetCount}`, async function () {
+ let first = true;
+ const { promise, resolve } = Promise.withResolvers();
+ const rid = resourceForReadableStream(
+ new ReadableStream({
+ pull(controller) {
+ if (first) {
+ // We queue this up and then immediately close the resource (not the reader)
+ for (let i = 0; i < packetCount; i++) {
+ controller.enqueue(new Uint8Array(1));
+ }
+ core.close(rid);
+ // This doesn't throw, even though the resource is closed
+ controller.enqueue(new Uint8Array(1));
+ first = false;
+ }
+ },
+ cancel(reason) {
+ resolve(reason);
+ },
+ }),
+ );
+ try {
+ for (let i = 0; i < packetCount; i++) {
+ await core.read(rid, new Uint8Array(1));
+ }
+ fail();
+ } catch (e) {
+ assertEquals(e.message, "operation canceled");
+ }
+ assertEquals(await promise, "resource closed");
+ });
+}
diff --git a/tests/unit/structured_clone_test.ts b/tests/unit/structured_clone_test.ts
new file mode 100644
index 000000000..314a276dd
--- /dev/null
+++ b/tests/unit/structured_clone_test.ts
@@ -0,0 +1,55 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assert, assertEquals, assertThrows } from "./test_util.ts";
+
+// Basic tests for the structured clone algorithm. Mainly tests TypeScript
+// typings. Actual functionality is tested in WPT.
+
+Deno.test("self.structuredClone", async () => {
+ const arrayOriginal = ["hello world"];
+ const channelOriginal = new MessageChannel();
+ const [arrayCloned, portTransferred] = self
+ .structuredClone([arrayOriginal, channelOriginal.port2], {
+ transfer: [channelOriginal.port2],
+ });
+ assert(arrayOriginal !== arrayCloned); // not the same identity
+ assertEquals(arrayCloned, arrayOriginal); // but same value
+ channelOriginal.port1.postMessage("1");
+ await new Promise((resolve) => portTransferred.onmessage = () => resolve(1));
+ channelOriginal.port1.close();
+ portTransferred.close();
+});
+
+Deno.test("correct DataCloneError message", () => {
+ assertThrows(
+ () => {
+ const sab = new SharedArrayBuffer(1024);
+ structuredClone(sab, { transfer: [sab] });
+ },
+ DOMException,
+ "Value not transferable",
+ );
+
+ const ab = new ArrayBuffer(1);
+ // detach ArrayBuffer
+ structuredClone(ab, { transfer: [ab] });
+ assertThrows(
+ () => {
+ structuredClone(ab, { transfer: [ab] });
+ },
+ DOMException,
+ "ArrayBuffer at index 0 is already detached",
+ );
+
+ const ab2 = new ArrayBuffer(0);
+ assertThrows(
+ () => {
+ structuredClone([ab2, ab], { transfer: [ab2, ab] });
+ },
+ DOMException,
+ "ArrayBuffer at index 1 is already detached",
+ );
+
+ // ab2 should not be detached after above failure
+ structuredClone(ab2, { transfer: [ab2] });
+});
diff --git a/tests/unit/symbol_test.ts b/tests/unit/symbol_test.ts
new file mode 100644
index 000000000..54db7f5ba
--- /dev/null
+++ b/tests/unit/symbol_test.ts
@@ -0,0 +1,11 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert } from "./test_util.ts";
+
+// Test that `Symbol.metadata` is defined. This file can be removed when V8
+// supports `Symbol.metadata` natively.
+
+Deno.test(
+ function symbolMetadataIsDefined() {
+ assert(typeof Symbol.metadata === "symbol");
+ },
+);
diff --git a/tests/unit/symlink_test.ts b/tests/unit/symlink_test.ts
new file mode 100644
index 000000000..310c36930
--- /dev/null
+++ b/tests/unit/symlink_test.ts
@@ -0,0 +1,140 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertRejects,
+ assertThrows,
+ pathToAbsoluteFileUrl,
+} from "./test_util.ts";
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function symlinkSyncSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const oldname = testDir + "/oldname";
+ const newname = testDir + "/newname";
+ Deno.mkdirSync(oldname);
+ Deno.symlinkSync(oldname, newname);
+ const newNameInfoLStat = Deno.lstatSync(newname);
+ const newNameInfoStat = Deno.statSync(newname);
+ assert(newNameInfoLStat.isSymlink);
+ assert(newNameInfoStat.isDirectory);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function symlinkSyncURL() {
+ const testDir = Deno.makeTempDirSync();
+ const oldname = testDir + "/oldname";
+ const newname = testDir + "/newname";
+ Deno.mkdirSync(oldname);
+ Deno.symlinkSync(
+ pathToAbsoluteFileUrl(oldname),
+ pathToAbsoluteFileUrl(newname),
+ );
+ const newNameInfoLStat = Deno.lstatSync(newname);
+ const newNameInfoStat = Deno.statSync(newname);
+ assert(newNameInfoLStat.isSymlink);
+ assert(newNameInfoStat.isDirectory);
+ },
+);
+
+Deno.test(
+ { permissions: { read: false, write: false } },
+ function symlinkSyncPerm() {
+ assertThrows(() => {
+ Deno.symlinkSync("oldbaddir", "newbaddir");
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function symlinkSyncAlreadyExist() {
+ const existingFile = Deno.makeTempFileSync();
+ const existingFile2 = Deno.makeTempFileSync();
+ assertThrows(
+ () => {
+ Deno.symlinkSync(existingFile, existingFile2);
+ },
+ Deno.errors.AlreadyExists,
+ `symlink '${existingFile}' -> '${existingFile2}'`,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function symlinkSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const oldname = testDir + "/oldname";
+ const newname = testDir + "/newname";
+ Deno.mkdirSync(oldname);
+ await Deno.symlink(oldname, newname);
+ const newNameInfoLStat = Deno.lstatSync(newname);
+ const newNameInfoStat = Deno.statSync(newname);
+ assert(newNameInfoLStat.isSymlink, "NOT SYMLINK");
+ assert(newNameInfoStat.isDirectory, "NOT DIRECTORY");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function symlinkURL() {
+ const testDir = Deno.makeTempDirSync();
+ const oldname = testDir + "/oldname";
+ const newname = testDir + "/newname";
+ Deno.mkdirSync(oldname);
+ await Deno.symlink(
+ pathToAbsoluteFileUrl(oldname),
+ pathToAbsoluteFileUrl(newname),
+ );
+ const newNameInfoLStat = Deno.lstatSync(newname);
+ const newNameInfoStat = Deno.statSync(newname);
+ assert(newNameInfoLStat.isSymlink, "NOT SYMLINK");
+ assert(newNameInfoStat.isDirectory, "NOT DIRECTORY");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function symlinkAlreadyExist() {
+ const existingFile = Deno.makeTempFileSync();
+ const existingFile2 = Deno.makeTempFileSync();
+ await assertRejects(
+ async () => {
+ await Deno.symlink(existingFile, existingFile2);
+ },
+ Deno.errors.AlreadyExists,
+ `symlink '${existingFile}' -> '${existingFile2}'`,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: ["."] } },
+ async function symlinkNoFullWritePermissions() {
+ await assertRejects(
+ () => Deno.symlink("old", "new"),
+ Deno.errors.PermissionDenied,
+ );
+ assertThrows(
+ () => Deno.symlinkSync("old", "new"),
+ Deno.errors.PermissionDenied,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: ["."], write: true } },
+ async function symlinkNoFullReadPermissions() {
+ await assertRejects(
+ () => Deno.symlink("old", "new"),
+ Deno.errors.PermissionDenied,
+ );
+ assertThrows(
+ () => Deno.symlinkSync("old", "new"),
+ Deno.errors.PermissionDenied,
+ );
+ },
+);
diff --git a/tests/unit/sync_test.ts b/tests/unit/sync_test.ts
new file mode 100644
index 000000000..40a8054c0
--- /dev/null
+++ b/tests/unit/sync_test.ts
@@ -0,0 +1,69 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function fdatasyncSyncSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test_fdatasyncSync.txt";
+ using file = Deno.openSync(filename, {
+ read: true,
+ write: true,
+ create: true,
+ });
+ const data = new Uint8Array(64);
+ Deno.writeSync(file.rid, data);
+ Deno.fdatasyncSync(file.rid);
+ Deno.removeSync(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function fdatasyncSuccess() {
+ const filename = (await Deno.makeTempDir()) + "/test_fdatasync.txt";
+ using file = await Deno.open(filename, {
+ read: true,
+ write: true,
+ create: true,
+ });
+ const data = new Uint8Array(64);
+ await file.write(data);
+ await Deno.fdatasync(file.rid);
+ assertEquals(await Deno.readFile(filename), data);
+ await Deno.remove(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function fsyncSyncSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test_fsyncSync.txt";
+ using file = Deno.openSync(filename, {
+ read: true,
+ write: true,
+ create: true,
+ });
+ const size = 64;
+ file.truncateSync(size);
+ Deno.fsyncSync(file.rid);
+ assertEquals(Deno.statSync(filename).size, size);
+ Deno.removeSync(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function fsyncSuccess() {
+ const filename = (await Deno.makeTempDir()) + "/test_fsync.txt";
+ using file = await Deno.open(filename, {
+ read: true,
+ write: true,
+ create: true,
+ });
+ const size = 64;
+ await file.truncate(size);
+ await Deno.fsync(file.rid);
+ assertEquals((await Deno.stat(filename)).size, size);
+ await Deno.remove(filename);
+ },
+);
diff --git a/tests/unit/test_util.ts b/tests/unit/test_util.ts
new file mode 100644
index 000000000..2f2730794
--- /dev/null
+++ b/tests/unit/test_util.ts
@@ -0,0 +1,87 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import * as colors from "@test_util/std/fmt/colors.ts";
+export { colors };
+import { join, resolve } from "@test_util/std/path/mod.ts";
+export {
+ assert,
+ assertEquals,
+ assertFalse,
+ AssertionError,
+ assertIsError,
+ assertMatch,
+ assertNotEquals,
+ assertNotStrictEquals,
+ assertRejects,
+ assertStrictEquals,
+ assertStringIncludes,
+ assertThrows,
+ fail,
+ unimplemented,
+ unreachable,
+} from "@test_util/std/assert/mod.ts";
+export { delay } from "@test_util/std/async/delay.ts";
+export { readLines } from "@test_util/std/io/read_lines.ts";
+export { parse as parseArgs } from "@test_util/std/flags/mod.ts";
+
+export function pathToAbsoluteFileUrl(path: string): URL {
+ path = resolve(path);
+
+ return new URL(`file://${Deno.build.os === "windows" ? "/" : ""}${path}`);
+}
+
+export function execCode(code: string): Promise<readonly [number, string]> {
+ return execCode2(code).finished();
+}
+
+export function execCode2(code: string) {
+ const command = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "--unstable",
+ "--no-check",
+ code,
+ ],
+ stdout: "piped",
+ stderr: "inherit",
+ });
+
+ const child = command.spawn();
+ const stdout = child.stdout.pipeThrough(new TextDecoderStream()).getReader();
+ let output = "";
+
+ return {
+ async waitStdoutText(text: string) {
+ while (true) {
+ const readData = await stdout.read();
+ if (readData.value) {
+ output += readData.value;
+ if (output.includes(text)) {
+ return;
+ }
+ }
+ if (readData.done) {
+ throw new Error(`Did not find text '${text}' in stdout.`);
+ }
+ }
+ },
+ async finished() {
+ while (true) {
+ const readData = await stdout.read();
+ if (readData.value) {
+ output += readData.value;
+ }
+ if (readData.done) {
+ break;
+ }
+ }
+ const status = await child.status;
+ return [status.code, output] as const;
+ },
+ };
+}
+
+export function tmpUnixSocketPath(): string {
+ const folder = Deno.makeTempDirSync();
+ return join(folder, "socket");
+}
diff --git a/tests/unit/testing_test.ts b/tests/unit/testing_test.ts
new file mode 100644
index 000000000..e04ab921c
--- /dev/null
+++ b/tests/unit/testing_test.ts
@@ -0,0 +1,154 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals, assertRejects, assertThrows } from "./test_util.ts";
+
+Deno.test(function testWrongOverloads() {
+ assertThrows(
+ () => {
+ // @ts-ignore Testing invalid overloads
+ Deno.test("some name", { fn: () => {} }, () => {});
+ },
+ TypeError,
+ "Unexpected 'fn' field in options, test function is already provided as the third argument.",
+ );
+ assertThrows(
+ () => {
+ // @ts-ignore Testing invalid overloads
+ Deno.test("some name", { name: "some name2" }, () => {});
+ },
+ TypeError,
+ "Unexpected 'name' field in options, test name is already provided as the first argument.",
+ );
+ assertThrows(
+ () => {
+ // @ts-ignore Testing invalid overloads
+ Deno.test(() => {});
+ },
+ TypeError,
+ "The test function must have a name",
+ );
+ assertThrows(
+ () => {
+ // @ts-ignore Testing invalid overloads
+ Deno.test(function foo() {}, {});
+ },
+ TypeError,
+ "Unexpected second argument to Deno.test()",
+ );
+ assertThrows(
+ () => {
+ // @ts-ignore Testing invalid overloads
+ Deno.test({ fn: () => {} }, function foo() {});
+ },
+ TypeError,
+ "Unexpected 'fn' field in options, test function is already provided as the second argument.",
+ );
+ assertThrows(
+ () => {
+ // @ts-ignore Testing invalid overloads
+ Deno.test({});
+ },
+ TypeError,
+ "Expected 'fn' field in the first argument to be a test function.",
+ );
+ assertThrows(
+ () => {
+ // @ts-ignore Testing invalid overloads
+ Deno.test({ fn: "boo!" });
+ },
+ TypeError,
+ "Expected 'fn' field in the first argument to be a test function.",
+ );
+});
+
+Deno.test(function nameOfTestCaseCantBeEmpty() {
+ assertThrows(
+ () => {
+ Deno.test("", () => {});
+ },
+ TypeError,
+ "The test name can't be empty",
+ );
+ assertThrows(
+ () => {
+ Deno.test({
+ name: "",
+ fn: () => {},
+ });
+ },
+ TypeError,
+ "The test name can't be empty",
+ );
+});
+
+Deno.test(async function invalidStepArguments(t) {
+ await assertRejects(
+ async () => {
+ // deno-lint-ignore no-explicit-any
+ await (t as any).step("test");
+ },
+ TypeError,
+ "Expected function for second argument.",
+ );
+
+ await assertRejects(
+ async () => {
+ // deno-lint-ignore no-explicit-any
+ await (t as any).step("test", "not a function");
+ },
+ TypeError,
+ "Expected function for second argument.",
+ );
+
+ await assertRejects(
+ async () => {
+ // deno-lint-ignore no-explicit-any
+ await (t as any).step();
+ },
+ TypeError,
+ "Expected a test definition or name and function.",
+ );
+
+ await assertRejects(
+ async () => {
+ // deno-lint-ignore no-explicit-any
+ await (t as any).step(() => {});
+ },
+ TypeError,
+ "The step function must have a name.",
+ );
+});
+
+Deno.test(async function nameOnTextContext(t1) {
+ await assertEquals(t1.name, "nameOnTextContext");
+ await t1.step("step", async (t2) => {
+ await assertEquals(t2.name, "step");
+ await t2.step("nested step", async (t3) => {
+ await assertEquals(t3.name, "nested step");
+ });
+ });
+});
+
+Deno.test(async function originOnTextContext(t1) {
+ await assertEquals(t1.origin, Deno.mainModule);
+ await t1.step("step", async (t2) => {
+ await assertEquals(t2.origin, Deno.mainModule);
+ await t2.step("nested step", async (t3) => {
+ await assertEquals(t3.origin, Deno.mainModule);
+ });
+ });
+});
+
+Deno.test(async function parentOnTextContext(t1) {
+ await assertEquals(t1.parent, undefined);
+ await t1.step("step", async (t2) => {
+ await assertEquals(t1, t2.parent);
+ await t2.step("nested step", async (t3) => {
+ await assertEquals(t2, t3.parent);
+ });
+ });
+});
+
+Deno.test("explicit undefined for boolean options", {
+ ignore: undefined,
+ only: undefined,
+}, () => {});
diff --git a/tests/unit/text_encoding_test.ts b/tests/unit/text_encoding_test.ts
new file mode 100644
index 000000000..719e5907e
--- /dev/null
+++ b/tests/unit/text_encoding_test.ts
@@ -0,0 +1,326 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertStrictEquals,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test(function btoaSuccess() {
+ const text = "hello world";
+ const encoded = btoa(text);
+ assertEquals(encoded, "aGVsbG8gd29ybGQ=");
+});
+
+Deno.test(function atobSuccess() {
+ const encoded = "aGVsbG8gd29ybGQ=";
+ const decoded = atob(encoded);
+ assertEquals(decoded, "hello world");
+});
+
+Deno.test(function atobWithAsciiWhitespace() {
+ const encodedList = [
+ " aGVsbG8gd29ybGQ=",
+ " aGVsbG8gd29ybGQ=",
+ "aGVsbG8gd29ybGQ= ",
+ "aGVsbG8gd29ybGQ=\n",
+ "aGVsbG\t8gd29ybGQ=",
+ `aGVsbG\t8g
+ d29ybGQ=`,
+ ];
+
+ for (const encoded of encodedList) {
+ const decoded = atob(encoded);
+ assertEquals(decoded, "hello world");
+ }
+});
+
+Deno.test(function atobThrows() {
+ let threw = false;
+ try {
+ atob("aGVsbG8gd29ybGQ==");
+ } catch (_e) {
+ threw = true;
+ }
+ assert(threw);
+});
+
+Deno.test(function atobThrows2() {
+ let threw = false;
+ try {
+ atob("aGVsbG8gd29ybGQ===");
+ } catch (_e) {
+ threw = true;
+ }
+ assert(threw);
+});
+
+Deno.test(function atobThrows3() {
+ let threw = false;
+ try {
+ atob("foobar!!");
+ } catch (e) {
+ if (
+ e instanceof DOMException &&
+ e.toString().startsWith("InvalidCharacterError:")
+ ) {
+ threw = true;
+ }
+ }
+ assert(threw);
+});
+
+Deno.test(function btoaFailed() {
+ const text = "你好";
+ assertThrows(() => {
+ btoa(text);
+ }, DOMException);
+});
+
+Deno.test(function textDecoder2() {
+ // deno-fmt-ignore
+ const fixture = new Uint8Array([
+ 0xf0, 0x9d, 0x93, 0xbd,
+ 0xf0, 0x9d, 0x93, 0xae,
+ 0xf0, 0x9d, 0x94, 0x81,
+ 0xf0, 0x9d, 0x93, 0xbd
+ ]);
+ const decoder = new TextDecoder();
+ assertEquals(decoder.decode(fixture), "𝓽𝓮𝔁𝓽");
+});
+
+// ignoreBOM is tested through WPT
+
+Deno.test(function textDecoderASCII() {
+ const fixture = new Uint8Array([0x89, 0x95, 0x9f, 0xbf]);
+ const decoder = new TextDecoder("ascii");
+ assertEquals(decoder.decode(fixture), "‰•Ÿ¿");
+});
+
+Deno.test(function textDecoderErrorEncoding() {
+ let didThrow = false;
+ try {
+ new TextDecoder("Foo");
+ } catch (e) {
+ didThrow = true;
+ assert(e instanceof Error);
+ assertEquals(e.message, "The encoding label provided ('Foo') is invalid.");
+ }
+ assert(didThrow);
+});
+
+Deno.test(function textEncoder() {
+ const fixture = "𝓽𝓮𝔁𝓽";
+ const encoder = new TextEncoder();
+ // deno-fmt-ignore
+ assertEquals(Array.from(encoder.encode(fixture)), [
+ 0xf0, 0x9d, 0x93, 0xbd,
+ 0xf0, 0x9d, 0x93, 0xae,
+ 0xf0, 0x9d, 0x94, 0x81,
+ 0xf0, 0x9d, 0x93, 0xbd
+ ]);
+});
+
+Deno.test(function textEncodeInto() {
+ const fixture = "text";
+ const encoder = new TextEncoder();
+ const bytes = new Uint8Array(5);
+ const result = encoder.encodeInto(fixture, bytes);
+ assertEquals(result.read, 4);
+ assertEquals(result.written, 4);
+ // deno-fmt-ignore
+ assertEquals(Array.from(bytes), [
+ 0x74, 0x65, 0x78, 0x74, 0x00,
+ ]);
+});
+
+Deno.test(function textEncodeInto2() {
+ const fixture = "𝓽𝓮𝔁𝓽";
+ const encoder = new TextEncoder();
+ const bytes = new Uint8Array(17);
+ const result = encoder.encodeInto(fixture, bytes);
+ assertEquals(result.read, 8);
+ assertEquals(result.written, 16);
+ // deno-fmt-ignore
+ assertEquals(Array.from(bytes), [
+ 0xf0, 0x9d, 0x93, 0xbd,
+ 0xf0, 0x9d, 0x93, 0xae,
+ 0xf0, 0x9d, 0x94, 0x81,
+ 0xf0, 0x9d, 0x93, 0xbd, 0x00,
+ ]);
+});
+
+Deno.test(function textEncodeInto3() {
+ const fixture = "𝓽𝓮𝔁𝓽";
+ const encoder = new TextEncoder();
+ const bytes = new Uint8Array(5);
+ const result = encoder.encodeInto(fixture, bytes);
+ assertEquals(result.read, 2);
+ assertEquals(result.written, 4);
+ // deno-fmt-ignore
+ assertEquals(Array.from(bytes), [
+ 0xf0, 0x9d, 0x93, 0xbd, 0x00,
+ ]);
+});
+
+Deno.test(function loneSurrogateEncodeInto() {
+ const fixture = "lone𝄞\ud888surrogate";
+ const encoder = new TextEncoder();
+ const bytes = new Uint8Array(20);
+ const result = encoder.encodeInto(fixture, bytes);
+ assertEquals(result.read, 16);
+ assertEquals(result.written, 20);
+ // deno-fmt-ignore
+ assertEquals(Array.from(bytes), [
+ 0x6c, 0x6f, 0x6e, 0x65,
+ 0xf0, 0x9d, 0x84, 0x9e,
+ 0xef, 0xbf, 0xbd, 0x73,
+ 0x75, 0x72, 0x72, 0x6f,
+ 0x67, 0x61, 0x74, 0x65
+ ]);
+});
+
+Deno.test(function loneSurrogateEncodeInto2() {
+ const fixture = "\ud800";
+ const encoder = new TextEncoder();
+ const bytes = new Uint8Array(3);
+ const result = encoder.encodeInto(fixture, bytes);
+ assertEquals(result.read, 1);
+ assertEquals(result.written, 3);
+ // deno-fmt-ignore
+ assertEquals(Array.from(bytes), [
+ 0xef, 0xbf, 0xbd
+ ]);
+});
+
+Deno.test(function loneSurrogateEncodeInto3() {
+ const fixture = "\udc00";
+ const encoder = new TextEncoder();
+ const bytes = new Uint8Array(3);
+ const result = encoder.encodeInto(fixture, bytes);
+ assertEquals(result.read, 1);
+ assertEquals(result.written, 3);
+ // deno-fmt-ignore
+ assertEquals(Array.from(bytes), [
+ 0xef, 0xbf, 0xbd
+ ]);
+});
+
+Deno.test(function swappedSurrogatePairEncodeInto4() {
+ const fixture = "\udc00\ud800";
+ const encoder = new TextEncoder();
+ const bytes = new Uint8Array(8);
+ const result = encoder.encodeInto(fixture, bytes);
+ assertEquals(result.read, 2);
+ assertEquals(result.written, 6);
+ // deno-fmt-ignore
+ assertEquals(Array.from(bytes), [
+ 0xef, 0xbf, 0xbd, 0xef, 0xbf, 0xbd, 0x00, 0x00
+ ]);
+});
+
+Deno.test(function textDecoderSharedUint8Array() {
+ const ab = new SharedArrayBuffer(6);
+ const dataView = new DataView(ab);
+ const charCodeA = "A".charCodeAt(0);
+ for (let i = 0; i < ab.byteLength; i++) {
+ dataView.setUint8(i, charCodeA + i);
+ }
+ const ui8 = new Uint8Array(ab);
+ const decoder = new TextDecoder();
+ const actual = decoder.decode(ui8);
+ assertEquals(actual, "ABCDEF");
+});
+
+Deno.test(function textDecoderSharedInt32Array() {
+ const ab = new SharedArrayBuffer(8);
+ const dataView = new DataView(ab);
+ const charCodeA = "A".charCodeAt(0);
+ for (let i = 0; i < ab.byteLength; i++) {
+ dataView.setUint8(i, charCodeA + i);
+ }
+ const i32 = new Int32Array(ab);
+ const decoder = new TextDecoder();
+ const actual = decoder.decode(i32);
+ assertEquals(actual, "ABCDEFGH");
+});
+
+Deno.test(function toStringShouldBeWebCompatibility() {
+ const encoder = new TextEncoder();
+ assertEquals(encoder.toString(), "[object TextEncoder]");
+
+ const decoder = new TextDecoder();
+ assertEquals(decoder.toString(), "[object TextDecoder]");
+});
+
+Deno.test(function textEncoderShouldCoerceToString() {
+ const encoder = new TextEncoder();
+ const fixtureText = "text";
+ const fixture = {
+ toString() {
+ return fixtureText;
+ },
+ };
+
+ const bytes = encoder.encode(fixture as unknown as string);
+ const decoder = new TextDecoder();
+ const decoded = decoder.decode(bytes);
+ assertEquals(decoded, fixtureText);
+});
+
+Deno.test(function binaryEncode() {
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ const core = Deno[Deno.internal].core;
+ function asBinaryString(bytes: Uint8Array): string {
+ return Array.from(bytes).map(
+ (v: number) => String.fromCodePoint(v),
+ ).join("");
+ }
+
+ function decodeBinary(binaryString: string) {
+ const chars: string[] = Array.from(binaryString);
+ return chars.map((v: string): number | undefined => v.codePointAt(0));
+ }
+ const inputs = [
+ "σ😀",
+ "Кириллица is Cyrillic",
+ "𝓽𝓮𝔁𝓽",
+ "lone𝄞\ud888surrogate",
+ "\udc00\ud800",
+ "\ud800",
+ ];
+ for (const input of inputs) {
+ const bytes = new TextEncoder().encode(input);
+ const binaryString = core.encodeBinaryString(bytes);
+ assertEquals(
+ binaryString,
+ asBinaryString(bytes),
+ );
+
+ assertEquals(Array.from(bytes), decodeBinary(binaryString));
+ }
+});
+
+Deno.test(
+ { permissions: { read: true } },
+ async function textDecoderStreamCleansUpOnCancel() {
+ let cancelled = false;
+ const readable = new ReadableStream({
+ start: (controller) => {
+ controller.enqueue(new Uint8Array(12));
+ },
+ cancel: () => {
+ cancelled = true;
+ },
+ }).pipeThrough(new TextDecoderStream());
+ const chunks = [];
+ for await (const chunk of readable) {
+ chunks.push(chunk);
+ // breaking out of the loop prevents normal shutdown at end of async iterator values and triggers the cancel method of the stream instead
+ break;
+ }
+ assertEquals(chunks.length, 1);
+ assertEquals(chunks[0].length, 12);
+ assertStrictEquals(cancelled, true);
+ },
+);
diff --git a/tests/unit/timers_test.ts b/tests/unit/timers_test.ts
new file mode 100644
index 000000000..17b137231
--- /dev/null
+++ b/tests/unit/timers_test.ts
@@ -0,0 +1,763 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertNotEquals,
+ delay,
+ execCode,
+ unreachable,
+} from "./test_util.ts";
+
+Deno.test(async function functionParameterBindingSuccess() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let count = 0;
+
+ const nullProto = (newCount: number) => {
+ count = newCount;
+ resolve();
+ };
+
+ Reflect.setPrototypeOf(nullProto, null);
+
+ setTimeout(nullProto, 500, 1);
+ await promise;
+ // count should be reassigned
+ assertEquals(count, 1);
+});
+
+Deno.test(async function stringifyAndEvalNonFunctions() {
+ // eval can only access global scope
+ const global = globalThis as unknown as {
+ globalPromise: ReturnType<typeof Promise.withResolvers<void>>;
+ globalCount: number;
+ };
+
+ global.globalPromise = Promise.withResolvers<void>();
+ global.globalCount = 0;
+
+ const notAFunction =
+ "globalThis.globalCount++; globalThis.globalPromise.resolve();" as unknown as () =>
+ void;
+
+ setTimeout(notAFunction, 500);
+
+ await global.globalPromise.promise;
+
+ // count should be incremented
+ assertEquals(global.globalCount, 1);
+
+ Reflect.deleteProperty(global, "globalPromise");
+ Reflect.deleteProperty(global, "globalCount");
+});
+
+Deno.test(async function timeoutSuccess() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let count = 0;
+ setTimeout(() => {
+ count++;
+ resolve();
+ }, 500);
+ await promise;
+ // count should increment
+ assertEquals(count, 1);
+});
+
+Deno.test(async function timeoutEvalNoScopeLeak() {
+ // eval can only access global scope
+ const global = globalThis as unknown as {
+ globalPromise: ReturnType<typeof Promise.withResolvers<Error>>;
+ };
+ global.globalPromise = Promise.withResolvers();
+ setTimeout(
+ `
+ try {
+ console.log(core);
+ globalThis.globalPromise.reject(new Error("Didn't throw."));
+ } catch (error) {
+ globalThis.globalPromise.resolve(error);
+ }` as unknown as () => void,
+ 0,
+ );
+ const error = await global.globalPromise.promise;
+ assertEquals(error.name, "ReferenceError");
+ Reflect.deleteProperty(global, "globalPromise");
+});
+
+Deno.test(async function evalPrimordial() {
+ const global = globalThis as unknown as {
+ globalPromise: ReturnType<typeof Promise.withResolvers<void>>;
+ };
+ global.globalPromise = Promise.withResolvers<void>();
+ const originalEval = globalThis.eval;
+ let wasCalled = false;
+ globalThis.eval = (argument) => {
+ wasCalled = true;
+ return originalEval(argument);
+ };
+ setTimeout(
+ "globalThis.globalPromise.resolve();" as unknown as () => void,
+ 0,
+ );
+ await global.globalPromise.promise;
+ assert(!wasCalled);
+ Reflect.deleteProperty(global, "globalPromise");
+ globalThis.eval = originalEval;
+});
+
+Deno.test(async function timeoutArgs() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const arg = 1;
+ let capturedArgs: unknown[] = [];
+ setTimeout(
+ function () {
+ capturedArgs = [...arguments];
+ resolve();
+ },
+ 10,
+ arg,
+ arg.toString(),
+ [arg],
+ );
+ await promise;
+ assertEquals(capturedArgs, [
+ arg,
+ arg.toString(),
+ [arg],
+ ]);
+});
+
+Deno.test(async function timeoutCancelSuccess() {
+ let count = 0;
+ const id = setTimeout(() => {
+ count++;
+ }, 1);
+ // Cancelled, count should not increment
+ clearTimeout(id);
+ await delay(600);
+ assertEquals(count, 0);
+});
+
+Deno.test(async function timeoutCancelMultiple() {
+ function uncalled(): never {
+ throw new Error("This function should not be called.");
+ }
+
+ // Set timers and cancel them in the same order.
+ const t1 = setTimeout(uncalled, 10);
+ const t2 = setTimeout(uncalled, 10);
+ const t3 = setTimeout(uncalled, 10);
+ clearTimeout(t1);
+ clearTimeout(t2);
+ clearTimeout(t3);
+
+ // Set timers and cancel them in reverse order.
+ const t4 = setTimeout(uncalled, 20);
+ const t5 = setTimeout(uncalled, 20);
+ const t6 = setTimeout(uncalled, 20);
+ clearTimeout(t6);
+ clearTimeout(t5);
+ clearTimeout(t4);
+
+ // Sleep until we're certain that the cancelled timers aren't gonna fire.
+ await delay(50);
+});
+
+Deno.test(async function timeoutCancelInvalidSilentFail() {
+ // Expect no panic
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let count = 0;
+ const id = setTimeout(() => {
+ count++;
+ // Should have no effect
+ clearTimeout(id);
+ resolve();
+ }, 500);
+ await promise;
+ assertEquals(count, 1);
+
+ // Should silently fail (no panic)
+ clearTimeout(2147483647);
+});
+
+Deno.test(async function intervalSuccess() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let count = 0;
+ const id = setInterval(() => {
+ count++;
+ clearInterval(id);
+ resolve();
+ }, 100);
+ await promise;
+ // Clear interval
+ clearInterval(id);
+ // count should increment twice
+ assertEquals(count, 1);
+ // Similar false async leaking alarm.
+ // Force next round of polling.
+ await delay(0);
+});
+
+Deno.test(async function intervalCancelSuccess() {
+ let count = 0;
+ const id = setInterval(() => {
+ count++;
+ }, 1);
+ clearInterval(id);
+ await delay(500);
+ assertEquals(count, 0);
+});
+
+Deno.test(async function intervalOrdering() {
+ const timers: number[] = [];
+ let timeouts = 0;
+ function onTimeout() {
+ ++timeouts;
+ for (let i = 1; i < timers.length; i++) {
+ clearTimeout(timers[i]);
+ }
+ }
+ for (let i = 0; i < 10; i++) {
+ timers[i] = setTimeout(onTimeout, 1);
+ }
+ await delay(500);
+ assertEquals(timeouts, 1);
+});
+
+Deno.test(function intervalCancelInvalidSilentFail() {
+ // Should silently fail (no panic)
+ clearInterval(2147483647);
+});
+
+Deno.test(async function callbackTakesLongerThanInterval() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ let timeEndOfFirstCallback: number | undefined;
+ const interval = setInterval(() => {
+ if (timeEndOfFirstCallback === undefined) {
+ // First callback
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300);
+ timeEndOfFirstCallback = Date.now();
+ } else {
+ // Second callback
+ assert(Date.now() - 100 >= timeEndOfFirstCallback);
+ clearInterval(interval);
+ resolve();
+ }
+ }, 100);
+
+ await promise;
+});
+
+// https://github.com/denoland/deno/issues/11398
+Deno.test(async function clearTimeoutAfterNextTimerIsDue1() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ setTimeout(() => {
+ resolve();
+ }, 300);
+
+ const interval = setInterval(() => {
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 400);
+ // Both the interval and the timeout's due times are now in the past.
+ clearInterval(interval);
+ }, 100);
+
+ await promise;
+});
+
+// https://github.com/denoland/deno/issues/11398
+Deno.test(async function clearTimeoutAfterNextTimerIsDue2() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+
+ const timeout1 = setTimeout(unreachable, 100);
+
+ setTimeout(() => {
+ resolve();
+ }, 200);
+
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300);
+ // Both of the timeouts' due times are now in the past.
+ clearTimeout(timeout1);
+
+ await promise;
+});
+
+Deno.test(async function fireCallbackImmediatelyWhenDelayOverMaxValue() {
+ let count = 0;
+ setTimeout(() => {
+ count++;
+ }, 2 ** 31);
+ await delay(1);
+ assertEquals(count, 1);
+});
+
+Deno.test(async function timeoutCallbackThis() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let capturedThis: unknown;
+ const obj = {
+ foo() {
+ capturedThis = this;
+ resolve();
+ },
+ };
+ setTimeout(obj.foo, 1);
+ await promise;
+ assertEquals(capturedThis, window);
+});
+
+Deno.test(async function timeoutBindThis() {
+ const thisCheckPassed = [null, undefined, window, globalThis];
+
+ const thisCheckFailed = [
+ 0,
+ "",
+ true,
+ false,
+ {},
+ [],
+ "foo",
+ () => {},
+ Object.prototype,
+ ];
+
+ for (const thisArg of thisCheckPassed) {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ let hasThrown = 0;
+ try {
+ setTimeout.call(thisArg, () => resolve(), 1);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ await promise;
+ assertEquals(hasThrown, 1);
+ }
+
+ for (const thisArg of thisCheckFailed) {
+ let hasThrown = 0;
+ try {
+ setTimeout.call(thisArg, () => {}, 1);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ }
+});
+
+Deno.test(function clearTimeoutShouldConvertToNumber() {
+ let called = false;
+ const obj = {
+ valueOf(): number {
+ called = true;
+ return 1;
+ },
+ };
+ clearTimeout((obj as unknown) as number);
+ assert(called);
+});
+
+Deno.test(function setTimeoutShouldThrowWithBigint() {
+ let hasThrown = 0;
+ try {
+ setTimeout(() => {}, (1n as unknown) as number);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+});
+
+Deno.test(function clearTimeoutShouldThrowWithBigint() {
+ let hasThrown = 0;
+ try {
+ clearTimeout((1n as unknown) as number);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+});
+
+Deno.test(function testFunctionName() {
+ assertEquals(clearTimeout.name, "clearTimeout");
+ assertEquals(clearInterval.name, "clearInterval");
+});
+
+Deno.test(function testFunctionParamsLength() {
+ assertEquals(setTimeout.length, 1);
+ assertEquals(setInterval.length, 1);
+ assertEquals(clearTimeout.length, 0);
+ assertEquals(clearInterval.length, 0);
+});
+
+Deno.test(function clearTimeoutAndClearIntervalNotBeEquals() {
+ assertNotEquals(clearTimeout, clearInterval);
+});
+
+Deno.test(async function timerOrdering() {
+ const array: number[] = [];
+ const { promise: donePromise, resolve } = Promise.withResolvers<void>();
+
+ function push(n: number) {
+ array.push(n);
+ if (array.length === 6) {
+ resolve();
+ }
+ }
+
+ setTimeout(() => {
+ push(1);
+ setTimeout(() => push(4));
+ }, 0);
+ setTimeout(() => {
+ push(2);
+ setTimeout(() => push(5));
+ }, 0);
+ setTimeout(() => {
+ push(3);
+ setTimeout(() => push(6));
+ }, 0);
+
+ await donePromise;
+
+ assertEquals(array, [1, 2, 3, 4, 5, 6]);
+});
+
+Deno.test(async function timerBasicMicrotaskOrdering() {
+ let s = "";
+ let count = 0;
+ const { promise, resolve } = Promise.withResolvers<void>();
+ setTimeout(() => {
+ Promise.resolve().then(() => {
+ count++;
+ s += "de";
+ if (count === 2) {
+ resolve();
+ }
+ });
+ });
+ setTimeout(() => {
+ count++;
+ s += "no";
+ if (count === 2) {
+ resolve();
+ }
+ });
+ await promise;
+ assertEquals(s, "deno");
+});
+
+Deno.test(async function timerNestedMicrotaskOrdering() {
+ let s = "";
+ const { promise, resolve } = Promise.withResolvers<void>();
+ s += "0";
+ setTimeout(() => {
+ s += "4";
+ setTimeout(() => (s += "A"));
+ Promise.resolve()
+ .then(() => {
+ setTimeout(() => {
+ s += "B";
+ resolve();
+ });
+ })
+ .then(() => {
+ s += "5";
+ });
+ });
+ setTimeout(() => (s += "6"));
+ Promise.resolve().then(() => (s += "2"));
+ Promise.resolve().then(() =>
+ setTimeout(() => {
+ s += "7";
+ Promise.resolve()
+ .then(() => (s += "8"))
+ .then(() => {
+ s += "9";
+ });
+ })
+ );
+ Promise.resolve().then(() => Promise.resolve().then(() => (s += "3")));
+ s += "1";
+ await promise;
+ assertEquals(s, "0123456789AB");
+});
+
+Deno.test(function testQueueMicrotask() {
+ assertEquals(typeof queueMicrotask, "function");
+});
+
+Deno.test(async function timerIgnoresDateOverride() {
+ const OriginalDate = Date;
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
+ let hasThrown = 0;
+ try {
+ const overrideCalled: () => number = () => {
+ reject("global Date override used over original Date object");
+ return 0;
+ };
+ const DateOverride = () => {
+ overrideCalled();
+ };
+ globalThis.Date = DateOverride as DateConstructor;
+ globalThis.Date.now = overrideCalled;
+ globalThis.Date.UTC = overrideCalled;
+ globalThis.Date.parse = overrideCalled;
+ queueMicrotask(() => {
+ resolve();
+ });
+ await promise;
+ hasThrown = 1;
+ } catch (err) {
+ if (typeof err === "string") {
+ assertEquals(err, "global Date override used over original Date object");
+ hasThrown = 2;
+ } else if (err instanceof TypeError) {
+ hasThrown = 3;
+ } else {
+ hasThrown = 4;
+ }
+ } finally {
+ globalThis.Date = OriginalDate;
+ }
+ assertEquals(hasThrown, 1);
+});
+
+Deno.test({
+ name: "unrefTimer",
+ permissions: { run: true, read: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const timer = setTimeout(() => console.log("1"), 1);
+ Deno.unrefTimer(timer);
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "");
+ },
+});
+
+Deno.test({
+ name: "unrefTimer - mix ref and unref 1",
+ permissions: { run: true, read: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const timer1 = setTimeout(() => console.log("1"), 200);
+ const timer2 = setTimeout(() => console.log("2"), 400);
+ const timer3 = setTimeout(() => console.log("3"), 600);
+ Deno.unrefTimer(timer3);
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "1\n2\n");
+ },
+});
+
+Deno.test({
+ name: "unrefTimer - mix ref and unref 2",
+ permissions: { run: true, read: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const timer1 = setTimeout(() => console.log("1"), 200);
+ const timer2 = setTimeout(() => console.log("2"), 400);
+ const timer3 = setTimeout(() => console.log("3"), 600);
+ Deno.unrefTimer(timer1);
+ Deno.unrefTimer(timer2);
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "1\n2\n3\n");
+ },
+});
+
+Deno.test({
+ name: "unrefTimer - unref interval",
+ permissions: { run: true, read: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ let i = 0;
+ const timer1 = setInterval(() => {
+ console.log("1");
+ i++;
+ if (i === 5) {
+ Deno.unrefTimer(timer1);
+ }
+ }, 10);
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "1\n1\n1\n1\n1\n");
+ },
+});
+
+Deno.test({
+ name: "unrefTimer - unref then ref 1",
+ permissions: { run: true, read: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const timer1 = setTimeout(() => console.log("1"), 10);
+ Deno.unrefTimer(timer1);
+ Deno.refTimer(timer1);
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "1\n");
+ },
+});
+
+Deno.test({
+ name: "unrefTimer - unref then ref",
+ permissions: { run: true, read: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const timer1 = setTimeout(() => {
+ console.log("1");
+ Deno.refTimer(timer2);
+ }, 10);
+ const timer2 = setTimeout(() => console.log("2"), 20);
+ Deno.unrefTimer(timer2);
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "1\n2\n");
+ },
+});
+
+Deno.test({
+ name: "unrefTimer - invalid calls do nothing",
+ fn: () => {
+ Deno.unrefTimer(NaN);
+ Deno.refTimer(NaN);
+ },
+});
+
+Deno.test({
+ name: "AbortSignal.timeout() with no listeners",
+ permissions: { run: true, read: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const signal = AbortSignal.timeout(2000);
+
+ // This unref timer expires before the signal, and if it does expire, then
+ // it means the signal has kept the event loop alive.
+ const timer = setTimeout(() => console.log("Unexpected!"), 1500);
+ Deno.unrefTimer(timer);
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "");
+ },
+});
+
+Deno.test({
+ name: "AbortSignal.timeout() with listeners",
+ permissions: { run: true, read: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const signal = AbortSignal.timeout(1000);
+ signal.addEventListener("abort", () => console.log("Event fired!"));
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "Event fired!\n");
+ },
+});
+
+Deno.test({
+ name: "AbortSignal.timeout() with removed listeners",
+ permissions: { run: true, read: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const signal = AbortSignal.timeout(2000);
+
+ const callback = () => console.log("Unexpected: Event fired");
+ signal.addEventListener("abort", callback);
+
+ setTimeout(() => {
+ console.log("Removing the listener.");
+ signal.removeEventListener("abort", callback);
+ }, 500);
+
+ Deno.unrefTimer(
+ setTimeout(() => console.log("Unexpected: Unref timer"), 1500)
+ );
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "Removing the listener.\n");
+ },
+});
+
+Deno.test({
+ name: "AbortSignal.timeout() with listener for a non-abort event",
+ permissions: { run: true, read: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const signal = AbortSignal.timeout(2000);
+
+ signal.addEventListener("someOtherEvent", () => {
+ console.log("Unexpected: someOtherEvent called");
+ });
+
+ Deno.unrefTimer(
+ setTimeout(() => console.log("Unexpected: Unref timer"), 1500)
+ );
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "");
+ },
+});
+
+// Regression test for https://github.com/denoland/deno/issues/19866
+Deno.test({
+ name: "regression for #19866",
+ fn: async () => {
+ const timeoutsFired = [];
+
+ // deno-lint-ignore require-await
+ async function start(n: number) {
+ let i = 0;
+ const intervalId = setInterval(() => {
+ i++;
+ if (i > 2) {
+ clearInterval(intervalId!);
+ }
+ timeoutsFired.push(n);
+ }, 20);
+ }
+
+ for (let n = 0; n < 100; n++) {
+ start(n);
+ }
+
+ // 3s should be plenty of time for all the intervals to fire
+ // but it might still be flaky on CI.
+ await new Promise((resolve) => setTimeout(resolve, 3000));
+ assertEquals(timeoutsFired.length, 300);
+ },
+});
+
+// Regression test for https://github.com/denoland/deno/issues/20367
+Deno.test({
+ name: "regression for #20367",
+ fn: async () => {
+ const { promise, resolve } = Promise.withResolvers<number>();
+ const start = performance.now();
+ setTimeout(() => {
+ const end = performance.now();
+ resolve(end - start);
+ }, 1000);
+ clearTimeout(setTimeout(() => {}, 1000));
+
+ const result = await promise;
+ assert(result >= 1000);
+ },
+});
diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts
new file mode 100644
index 000000000..2bd7768bb
--- /dev/null
+++ b/tests/unit/tls_test.ts
@@ -0,0 +1,1546 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertNotEquals,
+ assertRejects,
+ assertStrictEquals,
+ assertThrows,
+} from "./test_util.ts";
+import { BufReader, BufWriter } from "@test_util/std/io/mod.ts";
+import { readAll } from "@test_util/std/streams/read_all.ts";
+import { writeAll } from "@test_util/std/streams/write_all.ts";
+import { TextProtoReader } from "../testdata/run/textproto.ts";
+
+const encoder = new TextEncoder();
+const decoder = new TextDecoder();
+const cert = await Deno.readTextFile("tests/testdata/tls/localhost.crt");
+const key = await Deno.readTextFile("tests/testdata/tls/localhost.key");
+const caCerts = [await Deno.readTextFile("tests/testdata/tls/RootCA.pem")];
+
+async function sleep(msec: number) {
+ await new Promise((res, _rej) => setTimeout(res, msec));
+}
+
+function unreachable(): never {
+ throw new Error("Unreachable code reached");
+}
+
+Deno.test({ permissions: { net: false } }, async function connectTLSNoPerm() {
+ await assertRejects(async () => {
+ await Deno.connectTls({ hostname: "deno.land", port: 443 });
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function connectTLSInvalidHost() {
+ await assertRejects(async () => {
+ await Deno.connectTls({ hostname: "256.0.0.0", port: 3567 });
+ }, TypeError);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: false } },
+ async function connectTLSCertFileNoReadPerm() {
+ await assertRejects(async () => {
+ await Deno.connectTls({
+ hostname: "deno.land",
+ port: 443,
+ certFile: "tests/testdata/tls/RootCA.crt",
+ });
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ function listenTLSNonExistentCertKeyFiles() {
+ const options = {
+ hostname: "localhost",
+ port: 3500,
+ certFile: "tests/testdata/tls/localhost.crt",
+ keyFile: "tests/testdata/tls/localhost.key",
+ };
+
+ assertThrows(() => {
+ Deno.listenTls({
+ ...options,
+ certFile: "./non/existent/file",
+ });
+ }, Deno.errors.NotFound);
+
+ assertThrows(() => {
+ Deno.listenTls({
+ ...options,
+ keyFile: "./non/existent/file",
+ });
+ }, Deno.errors.NotFound);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: false } },
+ function listenTLSNoReadPerm() {
+ assertThrows(() => {
+ Deno.listenTls({
+ hostname: "localhost",
+ port: 3500,
+ certFile: "tests/testdata/tls/localhost.crt",
+ keyFile: "tests/testdata/tls/localhost.key",
+ });
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { read: true, write: true, net: true },
+ },
+ function listenTLSEmptyKeyFile() {
+ const options = {
+ hostname: "localhost",
+ port: 3500,
+ certFile: "tests/testdata/tls/localhost.crt",
+ keyFile: "tests/testdata/tls/localhost.key",
+ };
+
+ const testDir = Deno.makeTempDirSync();
+ const keyFilename = testDir + "/key.pem";
+ Deno.writeFileSync(keyFilename, new Uint8Array([]), {
+ mode: 0o666,
+ });
+
+ assertThrows(() => {
+ Deno.listenTls({
+ ...options,
+ keyFile: keyFilename,
+ });
+ }, Error);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true, net: true } },
+ function listenTLSEmptyCertFile() {
+ const options = {
+ hostname: "localhost",
+ port: 3500,
+ certFile: "tests/testdata/tls/localhost.crt",
+ keyFile: "tests/testdata/tls/localhost.key",
+ };
+
+ const testDir = Deno.makeTempDirSync();
+ const certFilename = testDir + "/cert.crt";
+ Deno.writeFileSync(certFilename, new Uint8Array([]), {
+ mode: 0o666,
+ });
+
+ assertThrows(() => {
+ Deno.listenTls({
+ ...options,
+ certFile: certFilename,
+ });
+ }, Error);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function startTlsWithoutExclusiveAccessToTcpConn() {
+ const hostname = "localhost";
+ const port = getPort();
+
+ const tcpListener = Deno.listen({ hostname, port });
+ const [serverConn, clientConn] = await Promise.all([
+ tcpListener.accept(),
+ Deno.connect({ hostname, port }),
+ ]);
+
+ const buf = new Uint8Array(128);
+ const readPromise = clientConn.read(buf);
+ // `clientConn` is being used by a pending promise (`readPromise`) so
+ // `Deno.startTls` cannot consume the connection.
+ await assertRejects(
+ () => Deno.startTls(clientConn, { hostname }),
+ Deno.errors.BadResource,
+ );
+
+ serverConn.close();
+ tcpListener.close();
+ await readPromise;
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function dialAndListenTLS() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const hostname = "localhost";
+ const port = 3500;
+
+ const listener = Deno.listenTls({
+ hostname,
+ port,
+ cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"),
+ key: await Deno.readTextFile("tests/testdata/tls/localhost.key"),
+ });
+
+ const response = encoder.encode(
+ "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n",
+ );
+
+ listener.accept().then(
+ async (conn) => {
+ assert(conn.remoteAddr != null);
+ assert(conn.localAddr != null);
+ await conn.write(response);
+ // TODO(bartlomieju): this might be a bug
+ setTimeout(() => {
+ conn.close();
+ resolve();
+ }, 0);
+ },
+ );
+
+ const conn = await Deno.connectTls({ hostname, port, caCerts });
+ assert(conn.rid > 0);
+ const w = new BufWriter(conn);
+ const r = new BufReader(conn);
+ const body = `GET / HTTP/1.1\r\nHost: ${hostname}:${port}\r\n\r\n`;
+ const writeResult = await w.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await w.flush();
+ const tpr = new TextProtoReader(r);
+ const statusLine = await tpr.readLine();
+ assert(statusLine !== null, `line must be read: ${String(statusLine)}`);
+ const m = statusLine.match(/^(.+?) (.+?) (.+?)$/);
+ assert(m !== null, "must be matched");
+ const [_, proto, status, ok] = m;
+ assertEquals(proto, "HTTP/1.1");
+ assertEquals(status, "200");
+ assertEquals(ok, "OK");
+ const headers = await tpr.readMimeHeader();
+ assert(headers !== null);
+ const contentLength = parseInt(headers.get("content-length")!);
+ const bodyBuf = new Uint8Array(contentLength);
+ await r.readFull(bodyBuf);
+ assertEquals(decoder.decode(bodyBuf), "Hello World\n");
+ conn.close();
+ listener.close();
+ await promise;
+ },
+);
+Deno.test(
+ { permissions: { read: false, net: true } },
+ async function listenTlsWithCertAndKey() {
+ const { promise, resolve } = Promise.withResolvers<void>();
+ const hostname = "localhost";
+ const port = 3500;
+
+ const listener = Deno.listenTls({ hostname, port, cert, key });
+
+ const response = encoder.encode(
+ "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n",
+ );
+
+ listener.accept().then(
+ async (conn) => {
+ assert(conn.remoteAddr != null);
+ assert(conn.localAddr != null);
+ await conn.write(response);
+ setTimeout(() => {
+ conn.close();
+ resolve();
+ }, 0);
+ },
+ );
+
+ const conn = await Deno.connectTls({ hostname, port, caCerts });
+ assert(conn.rid > 0);
+ const w = new BufWriter(conn);
+ const r = new BufReader(conn);
+ const body = `GET / HTTP/1.1\r\nHost: ${hostname}:${port}\r\n\r\n`;
+ const writeResult = await w.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await w.flush();
+ const tpr = new TextProtoReader(r);
+ const statusLine = await tpr.readLine();
+ assert(statusLine !== null, `line must be read: ${String(statusLine)}`);
+ const m = statusLine.match(/^(.+?) (.+?) (.+?)$/);
+ assert(m !== null, "must be matched");
+ const [_, proto, status, ok] = m;
+ assertEquals(proto, "HTTP/1.1");
+ assertEquals(status, "200");
+ assertEquals(ok, "OK");
+ const headers = await tpr.readMimeHeader();
+ assert(headers !== null);
+ const contentLength = parseInt(headers.get("content-length")!);
+ const bodyBuf = new Uint8Array(contentLength);
+ await r.readFull(bodyBuf);
+ assertEquals(decoder.decode(bodyBuf), "Hello World\n");
+ conn.close();
+ listener.close();
+ await promise;
+ },
+);
+
+let nextPort = 3501;
+function getPort() {
+ return nextPort++;
+}
+
+async function tlsPair(): Promise<[Deno.Conn, Deno.Conn]> {
+ const port = getPort();
+ const listener = Deno.listenTls({
+ hostname: "localhost",
+ port,
+ cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"),
+ key: await Deno.readTextFile("tests/testdata/tls/localhost.key"),
+ });
+
+ const acceptPromise = listener.accept();
+ const connectPromise = Deno.connectTls({
+ hostname: "localhost",
+ port,
+ caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
+ });
+ const endpoints = await Promise.all([acceptPromise, connectPromise]);
+
+ listener.close();
+
+ return endpoints;
+}
+
+async function tlsAlpn(
+ useStartTls: boolean,
+): Promise<[Deno.TlsConn, Deno.TlsConn]> {
+ const port = getPort();
+ const listener = Deno.listenTls({
+ hostname: "localhost",
+ port,
+ cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"),
+ key: await Deno.readTextFile("tests/testdata/tls/localhost.key"),
+ alpnProtocols: ["deno", "rocks"],
+ });
+
+ const acceptPromise = listener.accept();
+
+ const caCerts = [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")];
+ const clientAlpnProtocols = ["rocks", "rises"];
+ let endpoints: [Deno.TlsConn, Deno.TlsConn];
+
+ if (!useStartTls) {
+ const connectPromise = Deno.connectTls({
+ hostname: "localhost",
+ port,
+ caCerts,
+ alpnProtocols: clientAlpnProtocols,
+ });
+ endpoints = await Promise.all([acceptPromise, connectPromise]);
+ } else {
+ const client = await Deno.connect({
+ hostname: "localhost",
+ port,
+ });
+ const connectPromise = Deno.startTls(client, {
+ hostname: "localhost",
+ caCerts,
+ alpnProtocols: clientAlpnProtocols,
+ });
+ endpoints = await Promise.all([acceptPromise, connectPromise]);
+ }
+
+ listener.close();
+ return endpoints;
+}
+
+async function sendThenCloseWriteThenReceive(
+ conn: Deno.Conn,
+ chunkCount: number,
+ chunkSize: number,
+) {
+ const byteCount = chunkCount * chunkSize;
+ const buf = new Uint8Array(chunkSize); // Note: buf is size of _chunk_.
+ let n: number;
+
+ // Slowly send 42s.
+ buf.fill(42);
+ for (let remaining = byteCount; remaining > 0; remaining -= n) {
+ n = await conn.write(buf.subarray(0, remaining));
+ assert(n >= 1);
+ await sleep(10);
+ }
+
+ // Send EOF.
+ await conn.closeWrite();
+
+ // Receive 69s.
+ for (let remaining = byteCount; remaining > 0; remaining -= n) {
+ buf.fill(0);
+ n = await conn.read(buf) as number;
+ assert(n >= 1);
+ assertStrictEquals(buf[0], 69);
+ assertStrictEquals(buf[n - 1], 69);
+ }
+
+ conn.close();
+}
+
+async function receiveThenSend(
+ conn: Deno.Conn,
+ chunkCount: number,
+ chunkSize: number,
+) {
+ const byteCount = chunkCount * chunkSize;
+ const buf = new Uint8Array(byteCount); // Note: buf size equals `byteCount`.
+ let n: number;
+
+ // Receive 42s.
+ for (let remaining = byteCount; remaining > 0; remaining -= n) {
+ buf.fill(0);
+ n = await conn.read(buf) as number;
+ assert(n >= 1);
+ assertStrictEquals(buf[0], 42);
+ assertStrictEquals(buf[n - 1], 42);
+ }
+
+ // Slowly send 69s.
+ buf.fill(69);
+ for (let remaining = byteCount; remaining > 0; remaining -= n) {
+ n = await conn.write(buf.subarray(0, remaining));
+ assert(n >= 1);
+ await sleep(10);
+ }
+
+ conn.close();
+}
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsServerAlpnListenConnect() {
+ const [serverConn, clientConn] = await tlsAlpn(false);
+ const [serverHS, clientHS] = await Promise.all([
+ serverConn.handshake(),
+ clientConn.handshake(),
+ ]);
+ assertStrictEquals(serverHS.alpnProtocol, "rocks");
+ assertStrictEquals(clientHS.alpnProtocol, "rocks");
+
+ serverConn.close();
+ clientConn.close();
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsServerAlpnListenStartTls() {
+ const [serverConn, clientConn] = await tlsAlpn(true);
+ const [serverHS, clientHS] = await Promise.all([
+ serverConn.handshake(),
+ clientConn.handshake(),
+ ]);
+ assertStrictEquals(serverHS.alpnProtocol, "rocks");
+ assertStrictEquals(clientHS.alpnProtocol, "rocks");
+
+ serverConn.close();
+ clientConn.close();
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsServerStreamHalfCloseSendOneByte() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ sendThenCloseWriteThenReceive(serverConn, 1, 1),
+ receiveThenSend(clientConn, 1, 1),
+ ]);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsClientStreamHalfCloseSendOneByte() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ sendThenCloseWriteThenReceive(clientConn, 1, 1),
+ receiveThenSend(serverConn, 1, 1),
+ ]);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsServerStreamHalfCloseSendOneChunk() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ sendThenCloseWriteThenReceive(serverConn, 1, 1 << 20 /* 1 MB */),
+ receiveThenSend(clientConn, 1, 1 << 20 /* 1 MB */),
+ ]);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsClientStreamHalfCloseSendOneChunk() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ sendThenCloseWriteThenReceive(clientConn, 1, 1 << 20 /* 1 MB */),
+ receiveThenSend(serverConn, 1, 1 << 20 /* 1 MB */),
+ ]);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsServerStreamHalfCloseSendManyBytes() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ sendThenCloseWriteThenReceive(serverConn, 100, 1),
+ receiveThenSend(clientConn, 100, 1),
+ ]);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsClientStreamHalfCloseSendManyBytes() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ sendThenCloseWriteThenReceive(clientConn, 100, 1),
+ receiveThenSend(serverConn, 100, 1),
+ ]);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsServerStreamHalfCloseSendManyChunks() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ sendThenCloseWriteThenReceive(serverConn, 100, 1 << 16 /* 64 kB */),
+ receiveThenSend(clientConn, 100, 1 << 16 /* 64 kB */),
+ ]);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsClientStreamHalfCloseSendManyChunks() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ sendThenCloseWriteThenReceive(clientConn, 100, 1 << 16 /* 64 kB */),
+ receiveThenSend(serverConn, 100, 1 << 16 /* 64 kB */),
+ ]);
+ },
+);
+
+const largeAmount = 1 << 20 /* 1 MB */;
+
+async function sendAlotReceiveNothing(conn: Deno.Conn) {
+ // Start receive op.
+ const readBuf = new Uint8Array(1024);
+ const readPromise = conn.read(readBuf);
+
+ const timeout = setTimeout(() => {
+ throw new Error("Failed to send buffer in a reasonable amount of time");
+ }, 10_000);
+
+ // Send 1 MB of data.
+ const writeBuf = new Uint8Array(largeAmount);
+ writeBuf.fill(42);
+ await writeAll(conn, writeBuf);
+
+ clearTimeout(timeout);
+
+ // Send EOF.
+ await conn.closeWrite();
+
+ // Close the connection.
+ conn.close();
+
+ // Read op should be canceled.
+ await assertRejects(
+ async () => await readPromise,
+ Deno.errors.Interrupted,
+ );
+}
+
+async function receiveAlotSendNothing(conn: Deno.Conn) {
+ const readBuf = new Uint8Array(1024);
+ let n: number | null;
+ let nread = 0;
+
+ const timeout = setTimeout(() => {
+ throw new Error(
+ `Failed to read buffer in a reasonable amount of time (got ${nread}/${largeAmount})`,
+ );
+ }, 10_000);
+
+ // Receive 1 MB of data.
+ try {
+ for (; nread < largeAmount; nread += n!) {
+ n = await conn.read(readBuf);
+ assertStrictEquals(typeof n, "number");
+ assert(n! > 0);
+ assertStrictEquals(readBuf[0], 42);
+ }
+ } catch (e) {
+ throw new Error(
+ `Got an error (${e.message}) after reading ${nread}/${largeAmount} bytes`,
+ { cause: e },
+ );
+ }
+ clearTimeout(timeout);
+
+ // Close the connection, without sending anything at all.
+ conn.close();
+}
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsServerStreamCancelRead() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ sendAlotReceiveNothing(serverConn),
+ receiveAlotSendNothing(clientConn),
+ ]);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsClientStreamCancelRead() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ sendAlotReceiveNothing(clientConn),
+ receiveAlotSendNothing(serverConn),
+ ]);
+ },
+);
+
+async function sendReceiveEmptyBuf(conn: Deno.Conn) {
+ const byteBuf = new Uint8Array([1]);
+ const emptyBuf = new Uint8Array(0);
+ let n: number | null;
+
+ n = await conn.write(emptyBuf);
+ assertStrictEquals(n, 0);
+
+ n = await conn.read(emptyBuf);
+ assertStrictEquals(n, 0);
+
+ n = await conn.write(byteBuf);
+ assertStrictEquals(n, 1);
+
+ n = await conn.read(byteBuf);
+ assertStrictEquals(n, 1);
+
+ await conn.closeWrite();
+
+ n = await conn.write(emptyBuf);
+ assertStrictEquals(n, 0);
+
+ await assertRejects(async () => {
+ await conn.write(byteBuf);
+ }, Deno.errors.NotConnected);
+
+ n = await conn.write(emptyBuf);
+ assertStrictEquals(n, 0);
+
+ n = await conn.read(byteBuf);
+ assertStrictEquals(n, null);
+
+ conn.close();
+}
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsStreamSendReceiveEmptyBuf() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ sendReceiveEmptyBuf(serverConn),
+ sendReceiveEmptyBuf(clientConn),
+ ]);
+ },
+);
+
+function immediateClose(conn: Deno.Conn) {
+ conn.close();
+ return Promise.resolve();
+}
+
+async function closeWriteAndClose(conn: Deno.Conn) {
+ await conn.closeWrite();
+
+ if (await conn.read(new Uint8Array(1)) !== null) {
+ throw new Error("did not expect to receive data on TLS stream");
+ }
+
+ conn.close();
+}
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsServerStreamImmediateClose() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ immediateClose(serverConn),
+ closeWriteAndClose(clientConn),
+ ]);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsClientStreamImmediateClose() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ closeWriteAndClose(serverConn),
+ immediateClose(clientConn),
+ ]);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsClientAndServerStreamImmediateClose() {
+ const [serverConn, clientConn] = await tlsPair();
+ await Promise.all([
+ immediateClose(serverConn),
+ immediateClose(clientConn),
+ ]);
+ },
+);
+
+async function tlsWithTcpFailureTestImpl(
+ phase: "handshake" | "traffic",
+ cipherByteCount: number,
+ failureMode: "corruption" | "shutdown",
+ reverse: boolean,
+) {
+ const tlsPort = getPort();
+ const tlsListener = Deno.listenTls({
+ hostname: "localhost",
+ port: tlsPort,
+ cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"),
+ key: await Deno.readTextFile("tests/testdata/tls/localhost.key"),
+ });
+
+ const tcpPort = getPort();
+ const tcpListener = Deno.listen({ hostname: "localhost", port: tcpPort });
+
+ const [tlsServerConn, tcpServerConn] = await Promise.all([
+ tlsListener.accept(),
+ Deno.connect({ hostname: "localhost", port: tlsPort }),
+ ]);
+
+ const [tcpClientConn, tlsClientConn] = await Promise.all([
+ tcpListener.accept(),
+ Deno.connectTls({
+ hostname: "localhost",
+ port: tcpPort,
+ caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
+ }),
+ ]);
+
+ tlsListener.close();
+ tcpListener.close();
+
+ const {
+ tlsConn1,
+ tlsConn2,
+ tcpConn1,
+ tcpConn2,
+ } = reverse
+ ? {
+ tlsConn1: tlsClientConn,
+ tlsConn2: tlsServerConn,
+ tcpConn1: tcpClientConn,
+ tcpConn2: tcpServerConn,
+ }
+ : {
+ tlsConn1: tlsServerConn,
+ tlsConn2: tlsClientConn,
+ tcpConn1: tcpServerConn,
+ tcpConn2: tcpClientConn,
+ };
+
+ const tcpForwardingInterruptDeferred1 = Promise.withResolvers<void>();
+ const tcpForwardingPromise1 = forwardBytes(
+ tcpConn2,
+ tcpConn1,
+ cipherByteCount,
+ tcpForwardingInterruptDeferred1,
+ );
+
+ const tcpForwardingInterruptDeferred2 = Promise.withResolvers<void>();
+ const tcpForwardingPromise2 = forwardBytes(
+ tcpConn1,
+ tcpConn2,
+ Infinity,
+ tcpForwardingInterruptDeferred2,
+ );
+
+ switch (phase) {
+ case "handshake": {
+ let expectedError;
+ switch (failureMode) {
+ case "corruption":
+ expectedError = Deno.errors.InvalidData;
+ break;
+ case "shutdown":
+ expectedError = Deno.errors.UnexpectedEof;
+ break;
+ default:
+ unreachable();
+ }
+
+ const tlsTrafficPromise1 = Promise.all([
+ assertRejects(
+ () => sendBytes(tlsConn1, 0x01, 1),
+ expectedError,
+ ),
+ assertRejects(
+ () => receiveBytes(tlsConn1, 0x02, 1),
+ expectedError,
+ ),
+ ]);
+
+ const tlsTrafficPromise2 = Promise.all([
+ assertRejects(
+ () => sendBytes(tlsConn2, 0x02, 1),
+ Deno.errors.UnexpectedEof,
+ ),
+ assertRejects(
+ () => receiveBytes(tlsConn2, 0x01, 1),
+ Deno.errors.UnexpectedEof,
+ ),
+ ]);
+
+ await tcpForwardingPromise1;
+
+ switch (failureMode) {
+ case "corruption":
+ await sendBytes(tcpConn1, 0xff, 1 << 14 /* 16 kB */);
+ break;
+ case "shutdown":
+ await tcpConn1.closeWrite();
+ break;
+ default:
+ unreachable();
+ }
+ await tlsTrafficPromise1;
+
+ tcpForwardingInterruptDeferred2.resolve();
+ await tcpForwardingPromise2;
+ await tcpConn2.closeWrite();
+ await tlsTrafficPromise2;
+
+ break;
+ }
+
+ case "traffic": {
+ await Promise.all([
+ sendBytes(tlsConn2, 0x88, 8888),
+ receiveBytes(tlsConn1, 0x88, 8888),
+ sendBytes(tlsConn1, 0x99, 99999),
+ receiveBytes(tlsConn2, 0x99, 99999),
+ ]);
+
+ tcpForwardingInterruptDeferred1.resolve();
+ await tcpForwardingInterruptDeferred1.promise;
+
+ switch (failureMode) {
+ case "corruption":
+ await sendBytes(tcpConn1, 0xff, 1 << 14 /* 16 kB */);
+ await assertRejects(
+ () => receiveEof(tlsConn1),
+ Deno.errors.InvalidData,
+ );
+ tcpForwardingInterruptDeferred2.resolve();
+ break;
+ case "shutdown":
+ await Promise.all([
+ tcpConn1.closeWrite(),
+ await assertRejects(
+ () => receiveEof(tlsConn1),
+ Deno.errors.UnexpectedEof,
+ ),
+ await tlsConn1.closeWrite(),
+ await receiveEof(tlsConn2),
+ ]);
+ break;
+ default:
+ unreachable();
+ }
+
+ await tcpForwardingPromise2;
+
+ break;
+ }
+
+ default:
+ unreachable();
+ }
+
+ tlsServerConn.close();
+ tlsClientConn.close();
+ tcpServerConn.close();
+ tcpClientConn.close();
+
+ async function sendBytes(
+ conn: Deno.Conn,
+ byte: number,
+ count: number,
+ ) {
+ let buf = new Uint8Array(1 << 12 /* 4 kB */);
+ buf.fill(byte);
+
+ while (count > 0) {
+ buf = buf.subarray(0, Math.min(buf.length, count));
+ const nwritten = await conn.write(buf);
+ assertStrictEquals(nwritten, buf.length);
+ count -= nwritten;
+ }
+ }
+
+ async function receiveBytes(
+ conn: Deno.Conn,
+ byte: number,
+ count: number,
+ ) {
+ let buf = new Uint8Array(1 << 12 /* 4 kB */);
+ while (count > 0) {
+ buf = buf.subarray(0, Math.min(buf.length, count));
+ const r = await conn.read(buf);
+ assertNotEquals(r, null);
+ assert(buf.subarray(0, r!).every((b) => b === byte));
+ count -= r!;
+ }
+ }
+
+ async function receiveEof(conn: Deno.Conn) {
+ const buf = new Uint8Array(1);
+ const r = await conn.read(buf);
+ assertStrictEquals(r, null);
+ }
+
+ async function forwardBytes(
+ source: Deno.Conn,
+ sink: Deno.Conn,
+ count: number,
+ interruptPromise: ReturnType<typeof Promise.withResolvers<void>>,
+ ) {
+ let buf = new Uint8Array(1 << 12 /* 4 kB */);
+ while (count > 0) {
+ buf = buf.subarray(0, Math.min(buf.length, count));
+ const nread = await Promise.race([
+ source.read(buf),
+ interruptPromise.promise,
+ ]);
+ if (nread == null) break; // Either EOF or interrupted.
+ const nwritten = await sink.write(buf.subarray(0, nread));
+ assertStrictEquals(nread, nwritten);
+ count -= nwritten;
+ }
+ }
+}
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsHandshakeWithTcpCorruptionImmediately() {
+ await tlsWithTcpFailureTestImpl("handshake", 0, "corruption", false);
+ await tlsWithTcpFailureTestImpl("handshake", 0, "corruption", true);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsHandshakeWithTcpShutdownImmediately() {
+ await tlsWithTcpFailureTestImpl("handshake", 0, "shutdown", false);
+ await tlsWithTcpFailureTestImpl("handshake", 0, "shutdown", true);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsHandshakeWithTcpCorruptionAfter70Bytes() {
+ await tlsWithTcpFailureTestImpl("handshake", 76, "corruption", false);
+ await tlsWithTcpFailureTestImpl("handshake", 78, "corruption", true);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsHandshakeWithTcpShutdownAfter70bytes() {
+ await tlsWithTcpFailureTestImpl("handshake", 77, "shutdown", false);
+ await tlsWithTcpFailureTestImpl("handshake", 79, "shutdown", true);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsHandshakeWithTcpCorruptionAfter200Bytes() {
+ await tlsWithTcpFailureTestImpl("handshake", 200, "corruption", false);
+ await tlsWithTcpFailureTestImpl("handshake", 202, "corruption", true);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsHandshakeWithTcpShutdownAfter200bytes() {
+ await tlsWithTcpFailureTestImpl("handshake", 201, "shutdown", false);
+ await tlsWithTcpFailureTestImpl("handshake", 203, "shutdown", true);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsTrafficWithTcpCorruption() {
+ await tlsWithTcpFailureTestImpl("traffic", Infinity, "corruption", false);
+ await tlsWithTcpFailureTestImpl("traffic", Infinity, "corruption", true);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsTrafficWithTcpShutdown() {
+ await tlsWithTcpFailureTestImpl("traffic", Infinity, "shutdown", false);
+ await tlsWithTcpFailureTestImpl("traffic", Infinity, "shutdown", true);
+ },
+);
+
+function createHttpsListener(port: number): Deno.Listener {
+ // Query format: `curl --insecure https://localhost:8443/z/12345`
+ // The server returns a response consisting of 12345 times the letter 'z'.
+ const listener = Deno.listenTls({
+ hostname: "localhost",
+ port,
+ cert: Deno.readTextFileSync("./tests/testdata/tls/localhost.crt"),
+ key: Deno.readTextFileSync("./tests/testdata/tls/localhost.key"),
+ });
+
+ serve(listener);
+ return listener;
+
+ async function serve(listener: Deno.Listener) {
+ for await (const conn of listener) {
+ const EOL = "\r\n";
+
+ // Read GET request plus headers.
+ const buf = new Uint8Array(1 << 12 /* 4 kB */);
+ const decoder = new TextDecoder();
+ let req = "";
+ while (!req.endsWith(EOL + EOL)) {
+ const n = await conn.read(buf);
+ if (n === null) throw new Error("Unexpected EOF");
+ req += decoder.decode(buf.subarray(0, n));
+ }
+
+ // Parse GET request.
+ const { filler, count, version } =
+ /^GET \/(?<filler>[^\/]+)\/(?<count>\d+) HTTP\/(?<version>1\.\d)\r\n/
+ .exec(req)!.groups as {
+ filler: string;
+ count: string;
+ version: string;
+ };
+
+ // Generate response.
+ const resBody = new TextEncoder().encode(filler.repeat(+count));
+ const resHead = new TextEncoder().encode(
+ [
+ `HTTP/${version} 200 OK`,
+ `Content-Length: ${resBody.length}`,
+ "Content-Type: text/plain",
+ ].join(EOL) + EOL + EOL,
+ );
+
+ // Send response.
+ await writeAll(conn, resHead);
+ await writeAll(conn, resBody);
+
+ // Close TCP connection.
+ conn.close();
+ }
+ }
+}
+
+async function curl(url: string): Promise<string> {
+ const { success, code, stdout, stderr } = await new Deno.Command("curl", {
+ args: ["--insecure", url],
+ }).output();
+
+ if (!success) {
+ throw new Error(
+ `curl ${url} failed: ${code}:\n${new TextDecoder().decode(stderr)}`,
+ );
+ }
+ return new TextDecoder().decode(stdout);
+}
+
+Deno.test(
+ { permissions: { read: true, net: true, run: true } },
+ async function curlFakeHttpsServer() {
+ const port = getPort();
+ const listener = createHttpsListener(port);
+
+ const res1 = await curl(`https://localhost:${port}/d/1`);
+ assertStrictEquals(res1, "d");
+
+ const res2 = await curl(`https://localhost:${port}/e/12345`);
+ assertStrictEquals(res2, "e".repeat(12345));
+
+ const count3 = 1 << 17; // 128 kB.
+ const res3 = await curl(`https://localhost:${port}/n/${count3}`);
+ assertStrictEquals(res3, "n".repeat(count3));
+
+ const count4 = 12345678;
+ const res4 = await curl(`https://localhost:${port}/o/${count4}`);
+ assertStrictEquals(res4, "o".repeat(count4));
+
+ listener.close();
+ },
+);
+
+Deno.test(
+ // Ignored because gmail appears to reject us on CI sometimes
+ { ignore: true, permissions: { read: true, net: true } },
+ async function startTls() {
+ const hostname = "smtp.gmail.com";
+ const port = 587;
+ const encoder = new TextEncoder();
+
+ const conn = await Deno.connect({
+ hostname,
+ port,
+ });
+
+ let writer = new BufWriter(conn);
+ let reader = new TextProtoReader(new BufReader(conn));
+
+ let line: string | null = (await reader.readLine()) as string;
+ assert(line.startsWith("220"));
+
+ await writer.write(encoder.encode(`EHLO ${hostname}\r\n`));
+ await writer.flush();
+
+ while ((line = (await reader.readLine()) as string)) {
+ assert(line.startsWith("250"));
+ if (line.startsWith("250 ")) break;
+ }
+
+ await writer.write(encoder.encode("STARTTLS\r\n"));
+ await writer.flush();
+
+ line = await reader.readLine();
+
+ // Received the message that the server is ready to establish TLS
+ assertEquals(line, "220 2.0.0 Ready to start TLS");
+
+ const tlsConn = await Deno.startTls(conn, { hostname });
+ writer = new BufWriter(tlsConn);
+ reader = new TextProtoReader(new BufReader(tlsConn));
+
+ // After that use TLS communication again
+ await writer.write(encoder.encode(`EHLO ${hostname}\r\n`));
+ await writer.flush();
+
+ while ((line = (await reader.readLine()) as string)) {
+ assert(line.startsWith("250"));
+ if (line.startsWith("250 ")) break;
+ }
+
+ tlsConn.close();
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function connectTLSBadClientCertPrivateKey(): Promise<void> {
+ await assertRejects(async () => {
+ await Deno.connectTls({
+ hostname: "deno.land",
+ port: 443,
+ certChain: "bad data",
+ privateKey: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.key",
+ ),
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function connectTLSBadPrivateKey(): Promise<void> {
+ await assertRejects(async () => {
+ await Deno.connectTls({
+ hostname: "deno.land",
+ port: 443,
+ certChain: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.crt",
+ ),
+ privateKey: "bad data",
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function connectTLSNotPrivateKey(): Promise<void> {
+ await assertRejects(async () => {
+ await Deno.connectTls({
+ hostname: "deno.land",
+ port: 443,
+ certChain: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.crt",
+ ),
+ privateKey: "",
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function connectWithClientCert() {
+ // The test_server running on port 4552 responds with 'PASS' if client
+ // authentication was successful. Try it by running test_server and
+ // curl --key tests/testdata/tls/localhost.key \
+ // --cert tests/testdata/tls/localhost.crt \
+ // --cacert tests/testdata/tls/RootCA.crt https://localhost:4552/
+ const conn = await Deno.connectTls({
+ hostname: "localhost",
+ port: 4552,
+ certChain: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.crt",
+ ),
+ privateKey: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.key",
+ ),
+ caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
+ });
+ const result = decoder.decode(await readAll(conn));
+ assertEquals(result, "PASS");
+ conn.close();
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function connectTLSCaCerts() {
+ const conn = await Deno.connectTls({
+ hostname: "localhost",
+ port: 4557,
+ caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
+ });
+ const result = decoder.decode(await readAll(conn));
+ assertEquals(result, "PASS");
+ conn.close();
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function connectTLSCertFile() {
+ const conn = await Deno.connectTls({
+ hostname: "localhost",
+ port: 4557,
+ certFile: "tests/testdata/tls/RootCA.pem",
+ });
+ const result = decoder.decode(await readAll(conn));
+ assertEquals(result, "PASS");
+ conn.close();
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function startTLSCaCerts() {
+ const plainConn = await Deno.connect({
+ hostname: "localhost",
+ port: 4557,
+ });
+ const conn = await Deno.startTls(plainConn, {
+ hostname: "localhost",
+ caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
+ });
+ const result = decoder.decode(await readAll(conn));
+ assertEquals(result, "PASS");
+ conn.close();
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsHandshakeSuccess() {
+ const hostname = "localhost";
+ const port = getPort();
+
+ const listener = Deno.listenTls({
+ hostname,
+ port,
+ cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"),
+ key: await Deno.readTextFile("tests/testdata/tls/localhost.key"),
+ });
+ const acceptPromise = listener.accept();
+ const connectPromise = Deno.connectTls({
+ hostname,
+ port,
+ certFile: "tests/testdata/tls/RootCA.crt",
+ });
+ const [conn1, conn2] = await Promise.all([acceptPromise, connectPromise]);
+ listener.close();
+
+ await Promise.all([conn1.handshake(), conn2.handshake()]);
+
+ // Begin sending a 10mb blob over the TLS connection.
+ const whole = new Uint8Array(10 << 20); // 10mb.
+ whole.fill(42);
+ const sendPromise = writeAll(conn1, whole);
+ // Set up the other end to receive half of the large blob.
+ const half = new Uint8Array(whole.byteLength / 2);
+ const receivePromise = readFull(conn2, half);
+
+ await conn1.handshake();
+ await conn2.handshake();
+
+ // Finish receiving the first 5mb.
+ assertEquals(await receivePromise, half.length);
+
+ // See that we can call `handshake()` in the middle of large reads and writes.
+ await conn1.handshake();
+ await conn2.handshake();
+
+ // Receive second half of large blob. Wait for the send promise and check it.
+ assertEquals(await readFull(conn2, half), half.length);
+ await sendPromise;
+
+ await conn1.handshake();
+ await conn2.handshake();
+
+ await conn1.closeWrite();
+ await conn2.closeWrite();
+
+ await conn1.handshake();
+ await conn2.handshake();
+
+ conn1.close();
+ conn2.close();
+
+ async function readFull(conn: Deno.Conn, buf: Uint8Array) {
+ let offset, n;
+ for (offset = 0; offset < buf.length; offset += n) {
+ n = await conn.read(buf.subarray(offset, buf.length));
+ assert(n != null && n > 0);
+ }
+ return offset;
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function tlsHandshakeFailure() {
+ const hostname = "localhost";
+ const port = getPort();
+
+ async function server() {
+ const listener = Deno.listenTls({
+ hostname,
+ port,
+ cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
+ key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
+ });
+ for await (const conn of listener) {
+ for (let i = 0; i < 10; i++) {
+ // Handshake fails because the client rejects the server certificate.
+ await assertRejects(
+ () => conn.handshake(),
+ Deno.errors.InvalidData,
+ "received fatal alert",
+ );
+ }
+ conn.close();
+ break;
+ }
+ }
+
+ async function connectTlsClient() {
+ const conn = await Deno.connectTls({ hostname, port });
+ // Handshake fails because the server presents a self-signed certificate.
+ await assertRejects(
+ () => conn.handshake(),
+ Deno.errors.InvalidData,
+ "invalid peer certificate: UnknownIssuer",
+ );
+ conn.close();
+ }
+
+ await Promise.all([server(), connectTlsClient()]);
+
+ async function startTlsClient() {
+ const tcpConn = await Deno.connect({ hostname, port });
+ const tlsConn = await Deno.startTls(tcpConn, {
+ hostname: "foo.land",
+ caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
+ });
+ // Handshake fails because hostname doesn't match the certificate.
+ await assertRejects(
+ () => tlsConn.handshake(),
+ Deno.errors.InvalidData,
+ "NotValidForName",
+ );
+ tlsConn.close();
+ }
+
+ await Promise.all([server(), startTlsClient()]);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function listenTlsWithReuseAddr() {
+ const deferred1 = Promise.withResolvers<void>();
+ const hostname = "localhost";
+ const port = 3500;
+
+ const listener1 = Deno.listenTls({ hostname, port, cert, key });
+
+ listener1.accept().then((conn) => {
+ conn.close();
+ deferred1.resolve();
+ });
+
+ const conn1 = await Deno.connectTls({ hostname, port, caCerts });
+ conn1.close();
+ await deferred1.promise;
+ listener1.close();
+
+ const deferred2 = Promise.withResolvers<void>();
+ const listener2 = Deno.listenTls({ hostname, port, cert, key });
+
+ listener2.accept().then((conn) => {
+ conn.close();
+ deferred2.resolve();
+ });
+
+ const conn2 = await Deno.connectTls({ hostname, port, caCerts });
+ conn2.close();
+ await deferred2.promise;
+ listener2.close();
+ },
+);
+
+Deno.test({
+ ignore: Deno.build.os !== "linux",
+ permissions: { net: true },
+}, async function listenTlsReusePort() {
+ const hostname = "localhost";
+ const port = 4003;
+ const listener1 = Deno.listenTls({
+ hostname,
+ port,
+ cert,
+ key,
+ reusePort: true,
+ });
+ const listener2 = Deno.listenTls({
+ hostname,
+ port,
+ cert,
+ key,
+ reusePort: true,
+ });
+ let p1;
+ let p2;
+ let listener1Recv = false;
+ let listener2Recv = false;
+ while (!listener1Recv || !listener2Recv) {
+ if (!p1) {
+ p1 = listener1.accept().then((conn) => {
+ conn.close();
+ listener1Recv = true;
+ p1 = undefined;
+ }).catch(() => {});
+ }
+ if (!p2) {
+ p2 = listener2.accept().then((conn) => {
+ conn.close();
+ listener2Recv = true;
+ p2 = undefined;
+ }).catch(() => {});
+ }
+ const conn = await Deno.connectTls({ hostname, port, caCerts });
+ conn.close();
+ await Promise.race([p1, p2]);
+ }
+ listener1.close();
+ listener2.close();
+});
+
+Deno.test({
+ ignore: Deno.build.os === "linux",
+ permissions: { net: true },
+}, function listenTlsReusePortDoesNothing() {
+ const hostname = "localhost";
+ const port = 4003;
+ const listener1 = Deno.listenTls({
+ hostname,
+ port,
+ cert,
+ key,
+ reusePort: true,
+ });
+ assertThrows(() => {
+ Deno.listenTls({ hostname, port, cert, key, reusePort: true });
+ }, Deno.errors.AddrInUse);
+ listener1.close();
+});
+
+Deno.test({
+ permissions: { net: true },
+}, function listenTlsDoesNotThrowOnStringPort() {
+ const listener = Deno.listenTls({
+ hostname: "localhost",
+ // @ts-ignore String port is not allowed by typing, but it shouldn't throw
+ // for backwards compatibility.
+ port: "0",
+ cert,
+ key,
+ });
+ listener.close();
+});
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ function listenTLSInvalidCert() {
+ assertThrows(() => {
+ Deno.listenTls({
+ hostname: "localhost",
+ port: 3500,
+ certFile: "tests/testdata/tls/invalid.crt",
+ keyFile: "tests/testdata/tls/localhost.key",
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ function listenTLSInvalidKey() {
+ assertThrows(() => {
+ Deno.listenTls({
+ hostname: "localhost",
+ port: 3500,
+ certFile: "tests/testdata/tls/localhost.crt",
+ keyFile: "tests/testdata/tls/invalid.key",
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
diff --git a/tests/unit/truncate_test.ts b/tests/unit/truncate_test.ts
new file mode 100644
index 000000000..95b76052d
--- /dev/null
+++ b/tests/unit/truncate_test.ts
@@ -0,0 +1,114 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals, assertRejects, assertThrows } from "./test_util.ts";
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function ftruncateSyncSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test_ftruncateSync.txt";
+ using file = Deno.openSync(filename, {
+ create: true,
+ read: true,
+ write: true,
+ });
+
+ file.truncateSync(20);
+ assertEquals(Deno.readFileSync(filename).byteLength, 20);
+ file.truncateSync(5);
+ assertEquals(Deno.readFileSync(filename).byteLength, 5);
+ file.truncateSync(-5);
+ assertEquals(Deno.readFileSync(filename).byteLength, 0);
+
+ Deno.removeSync(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function ftruncateSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test_ftruncate.txt";
+ using file = await Deno.open(filename, {
+ create: true,
+ read: true,
+ write: true,
+ });
+
+ await file.truncate(20);
+ assertEquals((await Deno.readFile(filename)).byteLength, 20);
+ await file.truncate(5);
+ assertEquals((await Deno.readFile(filename)).byteLength, 5);
+ await file.truncate(-5);
+ assertEquals((await Deno.readFile(filename)).byteLength, 0);
+
+ await Deno.remove(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function truncateSyncSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test_truncateSync.txt";
+ Deno.writeFileSync(filename, new Uint8Array(5));
+ Deno.truncateSync(filename, 20);
+ assertEquals(Deno.readFileSync(filename).byteLength, 20);
+ Deno.truncateSync(filename, 5);
+ assertEquals(Deno.readFileSync(filename).byteLength, 5);
+ Deno.truncateSync(filename, -5);
+ assertEquals(Deno.readFileSync(filename).byteLength, 0);
+ Deno.removeSync(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function truncateSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test_truncate.txt";
+ await Deno.writeFile(filename, new Uint8Array(5));
+ await Deno.truncate(filename, 20);
+ assertEquals((await Deno.readFile(filename)).byteLength, 20);
+ await Deno.truncate(filename, 5);
+ assertEquals((await Deno.readFile(filename)).byteLength, 5);
+ await Deno.truncate(filename, -5);
+ assertEquals((await Deno.readFile(filename)).byteLength, 0);
+ await Deno.remove(filename);
+ },
+);
+
+Deno.test({ permissions: { write: false } }, function truncateSyncPerm() {
+ assertThrows(() => {
+ Deno.truncateSync("/test_truncateSyncPermission.txt");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { write: false } }, async function truncatePerm() {
+ await assertRejects(async () => {
+ await Deno.truncate("/test_truncatePermission.txt");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function truncateSyncNotFound() {
+ const filename = "/badfile.txt";
+ assertThrows(
+ () => {
+ Deno.truncateSync(filename);
+ },
+ Deno.errors.NotFound,
+ `truncate '${filename}'`,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function truncateSyncNotFound() {
+ const filename = "/badfile.txt";
+ await assertRejects(
+ async () => {
+ await Deno.truncate(filename);
+ },
+ Deno.errors.NotFound,
+ `truncate '${filename}'`,
+ );
+ },
+);
diff --git a/tests/unit/tty_color_test.ts b/tests/unit/tty_color_test.ts
new file mode 100644
index 000000000..6f26891e3
--- /dev/null
+++ b/tests/unit/tty_color_test.ts
@@ -0,0 +1,55 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+// Note tests for Deno.stdin.setRaw is in integration tests.
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function noColorIfNotTty() {
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log(1)"],
+ }).output();
+ const output = new TextDecoder().decode(stdout);
+ assertEquals(output, "1\n");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function denoNoColorIsNotAffectedByNonTty() {
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log(Deno.noColor)"],
+ }).output();
+ const output = new TextDecoder().decode(stdout);
+ assertEquals(output, "false\n");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function denoNoColorTrueEmptyVar() {
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log(Deno.noColor)"],
+ env: {
+ // https://no-color.org/ -- should not be true when empty
+ NO_COLOR: "",
+ },
+ }).output();
+ const output = new TextDecoder().decode(stdout);
+ assertEquals(output, "false\n");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function denoNoColorTrueEmptyVar() {
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log(Deno.noColor)"],
+ env: {
+ NO_COLOR: "1",
+ },
+ }).output();
+ const output = new TextDecoder().decode(stdout);
+ assertEquals(output, "true\n");
+ },
+);
diff --git a/tests/unit/tty_test.ts b/tests/unit/tty_test.ts
new file mode 100644
index 000000000..f135ae7cf
--- /dev/null
+++ b/tests/unit/tty_test.ts
@@ -0,0 +1,32 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert } from "./test_util.ts";
+
+// Note tests for Deno.stdin.setRaw is in integration tests.
+
+Deno.test(function consoleSize() {
+ if (!Deno.stdout.isTerminal()) {
+ return;
+ }
+ const result = Deno.consoleSize();
+ assert(typeof result.columns !== "undefined");
+ assert(typeof result.rows !== "undefined");
+});
+
+Deno.test({ permissions: { read: true } }, function isatty() {
+ // CI not under TTY, so cannot test stdin/stdout/stderr.
+ const f = Deno.openSync("tests/testdata/assets/hello.txt");
+ assert(!Deno.isatty(f.rid));
+ f.close();
+});
+
+Deno.test(function isattyError() {
+ let caught = false;
+ try {
+ // Absurdly large rid.
+ Deno.isatty(0x7fffffff);
+ } catch (e) {
+ caught = true;
+ assert(e instanceof Deno.errors.BadResource);
+ }
+ assert(caught);
+});
diff --git a/tests/unit/umask_test.ts b/tests/unit/umask_test.ts
new file mode 100644
index 000000000..0e97f0d35
--- /dev/null
+++ b/tests/unit/umask_test.ts
@@ -0,0 +1,15 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ },
+ function umaskSuccess() {
+ const prevMask = Deno.umask(0o020);
+ const newMask = Deno.umask(prevMask);
+ const finalMask = Deno.umask();
+ assertEquals(newMask, 0o020);
+ assertEquals(finalMask, prevMask);
+ },
+);
diff --git a/tests/unit/url_search_params_test.ts b/tests/unit/url_search_params_test.ts
new file mode 100644
index 000000000..c547ef938
--- /dev/null
+++ b/tests/unit/url_search_params_test.ts
@@ -0,0 +1,356 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals } from "./test_util.ts";
+
+Deno.test(function urlSearchParamsWithMultipleSpaces() {
+ const init = { str: "this string has spaces in it" };
+ const searchParams = new URLSearchParams(init).toString();
+ assertEquals(searchParams, "str=this+string+has+spaces+in+it");
+});
+
+Deno.test(function urlSearchParamsWithExclamation() {
+ const init = [
+ ["str", "hello, world!"],
+ ];
+ const searchParams = new URLSearchParams(init).toString();
+ assertEquals(searchParams, "str=hello%2C+world%21");
+});
+
+Deno.test(function urlSearchParamsWithQuotes() {
+ const init = [
+ ["str", "'hello world'"],
+ ];
+ const searchParams = new URLSearchParams(init).toString();
+ assertEquals(searchParams, "str=%27hello+world%27");
+});
+
+Deno.test(function urlSearchParamsWithBracket() {
+ const init = [
+ ["str", "(hello world)"],
+ ];
+ const searchParams = new URLSearchParams(init).toString();
+ assertEquals(searchParams, "str=%28hello+world%29");
+});
+
+Deno.test(function urlSearchParamsWithTilde() {
+ const init = [
+ ["str", "hello~world"],
+ ];
+ const searchParams = new URLSearchParams(init).toString();
+ assertEquals(searchParams, "str=hello%7Eworld");
+});
+
+Deno.test(function urlSearchParamsInitString() {
+ const init = "c=4&a=2&b=3&%C3%A1=1";
+ const searchParams = new URLSearchParams(init);
+ assert(
+ init === searchParams.toString(),
+ "The init query string does not match",
+ );
+});
+
+Deno.test(function urlSearchParamsInitStringWithPlusCharacter() {
+ let params = new URLSearchParams("q=a+b");
+ assertEquals(params.toString(), "q=a+b");
+ assertEquals(params.get("q"), "a b");
+
+ params = new URLSearchParams("q=a+b+c");
+ assertEquals(params.toString(), "q=a+b+c");
+ assertEquals(params.get("q"), "a b c");
+});
+
+Deno.test(function urlSearchParamsInitStringWithMalformedParams() {
+ // These test cases are copied from Web Platform Tests
+ // https://github.com/web-platform-tests/wpt/blob/54c6d64/url/urlsearchparams-constructor.any.js#L60-L80
+ let params = new URLSearchParams("id=0&value=%");
+ assert(params != null, "constructor returned non-null value.");
+ assert(params.has("id"), 'Search params object has name "id"');
+ assert(params.has("value"), 'Search params object has name "value"');
+ assertEquals(params.get("id"), "0");
+ assertEquals(params.get("value"), "%");
+
+ params = new URLSearchParams("b=%2sf%2a");
+ assert(params != null, "constructor returned non-null value.");
+ assert(params.has("b"), 'Search params object has name "b"');
+ assertEquals(params.get("b"), "%2sf*");
+
+ params = new URLSearchParams("b=%2%2af%2a");
+ assert(params != null, "constructor returned non-null value.");
+ assert(params.has("b"), 'Search params object has name "b"');
+ assertEquals(params.get("b"), "%2*f*");
+
+ params = new URLSearchParams("b=%%2a");
+ assert(params != null, "constructor returned non-null value.");
+ assert(params.has("b"), 'Search params object has name "b"');
+ assertEquals(params.get("b"), "%*");
+});
+
+Deno.test(function urlSearchParamsInitIterable() {
+ const init = [
+ ["a", "54"],
+ ["b", "true"],
+ ];
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.toString(), "a=54&b=true");
+});
+
+Deno.test(function urlSearchParamsInitRecord() {
+ const init = { a: "54", b: "true" };
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.toString(), "a=54&b=true");
+});
+
+Deno.test(function urlSearchParamsInit() {
+ const params1 = new URLSearchParams("a=b");
+ assertEquals(params1.toString(), "a=b");
+ const params2 = new URLSearchParams(params1);
+ assertEquals(params2.toString(), "a=b");
+});
+
+Deno.test(function urlSearchParamsAppendSuccess() {
+ const searchParams = new URLSearchParams();
+ searchParams.append("a", "true");
+ assertEquals(searchParams.toString(), "a=true");
+});
+
+Deno.test(function urlSearchParamsDeleteSuccess() {
+ const init = "a=54&b=true";
+ const searchParams = new URLSearchParams(init);
+ searchParams.delete("b");
+ assertEquals(searchParams.toString(), "a=54");
+});
+
+Deno.test(function urlSearchParamsGetAllSuccess() {
+ const init = "a=54&b=true&a=true";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.getAll("a"), ["54", "true"]);
+ assertEquals(searchParams.getAll("b"), ["true"]);
+ assertEquals(searchParams.getAll("c"), []);
+});
+
+Deno.test(function urlSearchParamsGetSuccess() {
+ const init = "a=54&b=true&a=true";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.get("a"), "54");
+ assertEquals(searchParams.get("b"), "true");
+ assertEquals(searchParams.get("c"), null);
+});
+
+Deno.test(function urlSearchParamsHasSuccess() {
+ const init = "a=54&b=true&a=true";
+ const searchParams = new URLSearchParams(init);
+ assert(searchParams.has("a"));
+ assert(searchParams.has("b"));
+ assert(!searchParams.has("c"));
+});
+
+Deno.test(function urlSearchParamsSetReplaceFirstAndRemoveOthers() {
+ const init = "a=54&b=true&a=true";
+ const searchParams = new URLSearchParams(init);
+ searchParams.set("a", "false");
+ assertEquals(searchParams.toString(), "a=false&b=true");
+});
+
+Deno.test(function urlSearchParamsSetAppendNew() {
+ const init = "a=54&b=true&a=true";
+ const searchParams = new URLSearchParams(init);
+ searchParams.set("c", "foo");
+ assertEquals(searchParams.toString(), "a=54&b=true&a=true&c=foo");
+});
+
+Deno.test(function urlSearchParamsSortSuccess() {
+ const init = "c=4&a=2&b=3&a=1";
+ const searchParams = new URLSearchParams(init);
+ searchParams.sort();
+ assertEquals(searchParams.toString(), "a=2&a=1&b=3&c=4");
+});
+
+Deno.test(function urlSearchParamsForEachSuccess() {
+ const init = [
+ ["a", "54"],
+ ["b", "true"],
+ ];
+ const searchParams = new URLSearchParams(init);
+ let callNum = 0;
+ searchParams.forEach((value, key, parent) => {
+ assertEquals(searchParams, parent);
+ assertEquals(value, init[callNum][1]);
+ assertEquals(key, init[callNum][0]);
+ callNum++;
+ });
+ assertEquals(callNum, init.length);
+});
+
+Deno.test(function urlSearchParamsMissingName() {
+ const init = "=4";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.get(""), "4");
+ assertEquals(searchParams.toString(), "=4");
+});
+
+Deno.test(function urlSearchParamsMissingValue() {
+ const init = "4=";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.get("4"), "");
+ assertEquals(searchParams.toString(), "4=");
+});
+
+Deno.test(function urlSearchParamsMissingEqualSign() {
+ const init = "4";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.get("4"), "");
+ assertEquals(searchParams.toString(), "4=");
+});
+
+Deno.test(function urlSearchParamsMissingPair() {
+ const init = "c=4&&a=54&";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.toString(), "c=4&a=54");
+});
+
+Deno.test(function urlSearchParamsForShortEncodedChar() {
+ const init = { linefeed: "\n", tab: "\t" };
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.toString(), "linefeed=%0A&tab=%09");
+});
+
+// If pair does not contain exactly two items, then throw a TypeError.
+// ref https://url.spec.whatwg.org/#interface-urlsearchparams
+Deno.test(function urlSearchParamsShouldThrowTypeError() {
+ let hasThrown = 0;
+
+ try {
+ new URLSearchParams([["1"]]);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+
+ assertEquals(hasThrown, 2);
+
+ try {
+ new URLSearchParams([["1", "2", "3"]]);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+
+ assertEquals(hasThrown, 2);
+});
+
+Deno.test(function urlSearchParamsAppendArgumentsCheck() {
+ const methodRequireOneParam = ["delete", "getAll", "get", "has", "forEach"];
+
+ const methodRequireTwoParams = ["append", "set"];
+
+ methodRequireOneParam
+ .concat(methodRequireTwoParams)
+ .forEach((method: string) => {
+ const searchParams = new URLSearchParams();
+ let hasThrown = 0;
+ try {
+ // deno-lint-ignore no-explicit-any
+ (searchParams as any)[method]();
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ });
+
+ methodRequireTwoParams.forEach((method: string) => {
+ const searchParams = new URLSearchParams();
+ let hasThrown = 0;
+ try {
+ // deno-lint-ignore no-explicit-any
+ (searchParams as any)[method]("foo");
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ });
+});
+
+// ref: https://github.com/web-platform-tests/wpt/blob/master/url/urlsearchparams-delete.any.js
+Deno.test(function urlSearchParamsDeletingAppendedMultiple() {
+ const params = new URLSearchParams();
+ params.append("first", (1 as unknown) as string);
+ assert(params.has("first"));
+ assertEquals(params.get("first"), "1");
+ params.delete("first");
+ assertEquals(params.has("first"), false);
+ params.append("first", (1 as unknown) as string);
+ params.append("first", (10 as unknown) as string);
+ params.delete("first");
+ assertEquals(params.has("first"), false);
+});
+
+// ref: https://github.com/web-platform-tests/wpt/blob/master/url/urlsearchparams-constructor.any.js#L176-L182
+Deno.test(function urlSearchParamsCustomSymbolIterator() {
+ const params = new URLSearchParams();
+ params[Symbol.iterator] = function* (): IterableIterator<[string, string]> {
+ yield ["a", "b"];
+ };
+ const params1 = new URLSearchParams((params as unknown) as string[][]);
+ assertEquals(params1.get("a"), "b");
+});
+
+Deno.test(
+ function urlSearchParamsCustomSymbolIteratorWithNonStringParams() {
+ const params = {};
+ // deno-lint-ignore no-explicit-any
+ (params as any)[Symbol.iterator] = function* (): IterableIterator<
+ [number, number]
+ > {
+ yield [1, 2];
+ };
+ const params1 = new URLSearchParams((params as unknown) as string[][]);
+ assertEquals(params1.get("1"), "2");
+ },
+);
+
+// If a class extends URLSearchParams, override one method should not change another's behavior.
+Deno.test(
+ function urlSearchParamsOverridingAppendNotChangeConstructorAndSet() {
+ let overriddenAppendCalled = 0;
+ class CustomSearchParams extends URLSearchParams {
+ append(name: string, value: string) {
+ ++overriddenAppendCalled;
+ super.append(name, value);
+ }
+ }
+ new CustomSearchParams("foo=bar");
+ new CustomSearchParams([["foo", "bar"]]);
+ new CustomSearchParams(new CustomSearchParams({ foo: "bar" }));
+ new CustomSearchParams().set("foo", "bar");
+ assertEquals(overriddenAppendCalled, 0);
+ },
+);
+
+Deno.test(function urlSearchParamsOverridingEntriesNotChangeForEach() {
+ class CustomSearchParams extends URLSearchParams {
+ *entries(): IterableIterator<[string, string]> {
+ yield* [];
+ }
+ }
+ let loopCount = 0;
+ const params = new CustomSearchParams({ foo: "bar" });
+ params.forEach(() => void ++loopCount);
+ assertEquals(loopCount, 1);
+});
diff --git a/tests/unit/url_test.ts b/tests/unit/url_test.ts
new file mode 100644
index 000000000..b0dc86232
--- /dev/null
+++ b/tests/unit/url_test.ts
@@ -0,0 +1,529 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertStrictEquals,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test(function urlParsing() {
+ const url = new URL(
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat",
+ );
+ assertEquals(url.hash, "#qat");
+ assertEquals(url.host, "baz.qat:8000");
+ assertEquals(url.hostname, "baz.qat");
+ assertEquals(
+ url.href,
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat",
+ );
+ assertEquals(url.origin, "https://baz.qat:8000");
+ assertEquals(url.password, "bar");
+ assertEquals(url.pathname, "/qux/quux");
+ assertEquals(url.port, "8000");
+ assertEquals(url.protocol, "https:");
+ assertEquals(url.search, "?foo=bar&baz=12");
+ assertEquals(url.searchParams.getAll("foo"), ["bar"]);
+ assertEquals(url.searchParams.getAll("baz"), ["12"]);
+ assertEquals(url.username, "foo");
+ assertEquals(
+ String(url),
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat",
+ );
+});
+
+Deno.test(function emptyUrl() {
+ assertThrows(
+ // @ts-ignore for test
+ () => new URL(),
+ TypeError,
+ "1 argument required, but only 0 present",
+ );
+ assertThrows(
+ // @ts-ignore for test
+ () => URL.canParse(),
+ TypeError,
+ "1 argument required, but only 0 present",
+ );
+});
+
+Deno.test(function urlProtocolParsing() {
+ assertEquals(new URL("Aa+-.1://foo").protocol, "aa+-.1:");
+ assertEquals(new URL("aA+-.1://foo").protocol, "aa+-.1:");
+ assertThrows(() => new URL("1://foo"), TypeError, "Invalid URL: '1://foo'");
+ assertThrows(() => new URL("+://foo"), TypeError, "Invalid URL: '+://foo'");
+ assertThrows(() => new URL("-://foo"), TypeError, "Invalid URL: '-://foo'");
+ assertThrows(() => new URL(".://foo"), TypeError, "Invalid URL: '.://foo'");
+ assertThrows(() => new URL("_://foo"), TypeError, "Invalid URL: '_://foo'");
+ assertThrows(() => new URL("=://foo"), TypeError, "Invalid URL: '=://foo'");
+ assertThrows(() => new URL("!://foo"), TypeError, "Invalid URL: '!://foo'");
+ assertThrows(() => new URL(`"://foo`), TypeError, `Invalid URL: '"://foo'`);
+ assertThrows(() => new URL("$://foo"), TypeError, "Invalid URL: '$://foo'");
+ assertThrows(() => new URL("%://foo"), TypeError, "Invalid URL: '%://foo'");
+ assertThrows(() => new URL("^://foo"), TypeError, "Invalid URL: '^://foo'");
+ assertThrows(() => new URL("*://foo"), TypeError, "Invalid URL: '*://foo'");
+ assertThrows(() => new URL("*://foo"), TypeError, "Invalid URL: '*://foo'");
+ assertThrows(
+ () => new URL("!:", "*://foo"),
+ TypeError,
+ "Invalid URL: '!:' with base '*://foo'",
+ );
+});
+
+Deno.test(function urlAuthenticationParsing() {
+ const specialUrl = new URL("http://foo:bar@baz");
+ assertEquals(specialUrl.username, "foo");
+ assertEquals(specialUrl.password, "bar");
+ assertEquals(specialUrl.hostname, "baz");
+ assertThrows(() => new URL("file://foo:bar@baz"), TypeError, "Invalid URL");
+ const nonSpecialUrl = new URL("abcd://foo:bar@baz");
+ assertEquals(nonSpecialUrl.username, "foo");
+ assertEquals(nonSpecialUrl.password, "bar");
+ assertEquals(nonSpecialUrl.hostname, "baz");
+});
+
+Deno.test(function urlHostnameParsing() {
+ // IPv6.
+ assertEquals(new URL("http://[::1]").hostname, "[::1]");
+ assertEquals(new URL("file://[::1]").hostname, "[::1]");
+ assertEquals(new URL("abcd://[::1]").hostname, "[::1]");
+ assertEquals(new URL("http://[0:f:0:0:f:f:0:0]").hostname, "[0:f::f:f:0:0]");
+
+ // Forbidden host code point.
+ assertThrows(() => new URL("http:// a"), TypeError, "Invalid URL");
+ assertThrows(() => new URL("file:// a"), TypeError, "Invalid URL");
+ assertThrows(() => new URL("abcd:// a"), TypeError, "Invalid URL");
+ assertThrows(() => new URL("http://%"), TypeError, "Invalid URL");
+ assertThrows(() => new URL("file://%"), TypeError, "Invalid URL");
+ assertEquals(new URL("abcd://%").hostname, "%");
+
+ // Percent-decode.
+ assertEquals(new URL("http://%21").hostname, "!");
+ assertEquals(new URL("file://%21").hostname, "!");
+ assertEquals(new URL("abcd://%21").hostname, "%21");
+
+ // IPv4 parsing.
+ assertEquals(new URL("http://260").hostname, "0.0.1.4");
+ assertEquals(new URL("file://260").hostname, "0.0.1.4");
+ assertEquals(new URL("abcd://260").hostname, "260");
+ assertEquals(new URL("http://255.0.0.0").hostname, "255.0.0.0");
+ assertThrows(() => new URL("http://256.0.0.0"), TypeError, "Invalid URL");
+ assertEquals(new URL("http://0.255.0.0").hostname, "0.255.0.0");
+ assertThrows(() => new URL("http://0.256.0.0"), TypeError, "Invalid URL");
+ assertEquals(new URL("http://0.0.255.0").hostname, "0.0.255.0");
+ assertThrows(() => new URL("http://0.0.256.0"), TypeError, "Invalid URL");
+ assertEquals(new URL("http://0.0.0.255").hostname, "0.0.0.255");
+ assertThrows(() => new URL("http://0.0.0.256"), TypeError, "Invalid URL");
+ assertEquals(new URL("http://0.0.65535").hostname, "0.0.255.255");
+ assertThrows(() => new URL("http://0.0.65536"), TypeError, "Invalid URL");
+ assertEquals(new URL("http://0.16777215").hostname, "0.255.255.255");
+ assertThrows(() => new URL("http://0.16777216"), TypeError, "Invalid URL");
+ assertEquals(new URL("http://4294967295").hostname, "255.255.255.255");
+ assertThrows(() => new URL("http://4294967296"), TypeError, "Invalid URL");
+});
+
+Deno.test(function urlPortParsing() {
+ const specialUrl = new URL("http://foo:8000");
+ assertEquals(specialUrl.hostname, "foo");
+ assertEquals(specialUrl.port, "8000");
+ assertThrows(() => new URL("file://foo:8000"), TypeError, "Invalid URL");
+ const nonSpecialUrl = new URL("abcd://foo:8000");
+ assertEquals(nonSpecialUrl.hostname, "foo");
+ assertEquals(nonSpecialUrl.port, "8000");
+});
+
+Deno.test(function urlModifications() {
+ const url = new URL(
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat",
+ );
+ url.hash = "";
+ assertEquals(
+ url.href,
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12",
+ );
+ url.host = "qat.baz:8080";
+ assertEquals(
+ url.href,
+ "https://foo:bar@qat.baz:8080/qux/quux?foo=bar&baz=12",
+ );
+ url.hostname = "foo.bar";
+ assertEquals(
+ url.href,
+ "https://foo:bar@foo.bar:8080/qux/quux?foo=bar&baz=12",
+ );
+ url.password = "qux";
+ assertEquals(
+ url.href,
+ "https://foo:qux@foo.bar:8080/qux/quux?foo=bar&baz=12",
+ );
+ url.pathname = "/foo/bar%qat";
+ assertEquals(
+ url.href,
+ "https://foo:qux@foo.bar:8080/foo/bar%qat?foo=bar&baz=12",
+ );
+ url.port = "";
+ assertEquals(url.href, "https://foo:qux@foo.bar/foo/bar%qat?foo=bar&baz=12");
+ url.protocol = "http:";
+ assertEquals(url.href, "http://foo:qux@foo.bar/foo/bar%qat?foo=bar&baz=12");
+ url.search = "?foo=bar&foo=baz";
+ assertEquals(url.href, "http://foo:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz");
+ assertEquals(url.searchParams.getAll("foo"), ["bar", "baz"]);
+ url.username = "foo@bar";
+ assertEquals(
+ url.href,
+ "http://foo%40bar:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz",
+ );
+ url.searchParams.set("bar", "qat");
+ assertEquals(
+ url.href,
+ "http://foo%40bar:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz&bar=qat",
+ );
+ url.searchParams.delete("foo");
+ assertEquals(url.href, "http://foo%40bar:qux@foo.bar/foo/bar%qat?bar=qat");
+ url.searchParams.append("foo", "bar");
+ assertEquals(
+ url.href,
+ "http://foo%40bar:qux@foo.bar/foo/bar%qat?bar=qat&foo=bar",
+ );
+});
+
+Deno.test(function urlModifyHref() {
+ const url = new URL("http://example.com/");
+ url.href = "https://foo:bar@example.com:8080/baz/qat#qux";
+ assertEquals(url.protocol, "https:");
+ assertEquals(url.username, "foo");
+ assertEquals(url.password, "bar");
+ assertEquals(url.host, "example.com:8080");
+ assertEquals(url.hostname, "example.com");
+ assertEquals(url.pathname, "/baz/qat");
+ assertEquals(url.hash, "#qux");
+});
+
+Deno.test(function urlNormalize() {
+ const url = new URL("http://example.com");
+ assertEquals(url.pathname, "/");
+ assertEquals(url.href, "http://example.com/");
+});
+
+Deno.test(function urlModifyPathname() {
+ const url = new URL("http://foo.bar/baz%qat/qux%quux");
+ assertEquals(url.pathname, "/baz%qat/qux%quux");
+ // Self-assignment is to invoke the setter.
+ // deno-lint-ignore no-self-assign
+ url.pathname = url.pathname;
+ assertEquals(url.pathname, "/baz%qat/qux%quux");
+ url.pathname = "baz#qat qux";
+ assertEquals(url.pathname, "/baz%23qat%20qux");
+ // deno-lint-ignore no-self-assign
+ url.pathname = url.pathname;
+ assertEquals(url.pathname, "/baz%23qat%20qux");
+ url.pathname = "\\a\\b\\c";
+ assertEquals(url.pathname, "/a/b/c");
+});
+
+Deno.test(function urlModifyHash() {
+ const url = new URL("http://foo.bar");
+ url.hash = "%foo bar/qat%qux#bar";
+ assertEquals(url.hash, "#%foo%20bar/qat%qux#bar");
+ // deno-lint-ignore no-self-assign
+ url.hash = url.hash;
+ assertEquals(url.hash, "#%foo%20bar/qat%qux#bar");
+});
+
+Deno.test(function urlSearchParamsReuse() {
+ const url = new URL(
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat",
+ );
+ const sp = url.searchParams;
+ url.host = "baz.qat";
+ assert(sp === url.searchParams, "Search params should be reused.");
+});
+
+Deno.test(function urlBackSlashes() {
+ const url = new URL(
+ "https:\\\\foo:bar@baz.qat:8000\\qux\\quux?foo=bar&baz=12#qat",
+ );
+ assertEquals(
+ url.href,
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat",
+ );
+});
+
+Deno.test(function urlProtocolSlashes() {
+ assertEquals(new URL("http:foo").href, "http://foo/");
+ assertEquals(new URL("http://foo").href, "http://foo/");
+ assertEquals(new URL("file:foo").href, "file:///foo");
+ assertEquals(new URL("file://foo").href, "file://foo/");
+ assertEquals(new URL("abcd:foo").href, "abcd:foo");
+ assertEquals(new URL("abcd://foo").href, "abcd://foo");
+});
+
+Deno.test(function urlRequireHost() {
+ assertEquals(new URL("file:///").href, "file:///");
+ assertThrows(() => new URL("ftp:///"), TypeError, "Invalid URL");
+ assertThrows(() => new URL("http:///"), TypeError, "Invalid URL");
+ assertThrows(() => new URL("https:///"), TypeError, "Invalid URL");
+ assertThrows(() => new URL("ws:///"), TypeError, "Invalid URL");
+ assertThrows(() => new URL("wss:///"), TypeError, "Invalid URL");
+});
+
+Deno.test(function urlDriveLetter() {
+ assertEquals(new URL("file:///C:").href, "file:///C:");
+ assertEquals(new URL("file:///C:/").href, "file:///C:/");
+ assertEquals(new URL("file:///C:/..").href, "file:///C:/");
+
+ // Don't recognise drive letters with extra leading slashes.
+ // FIXME(nayeemrmn): This is true according to
+ // https://jsdom.github.io/whatwg-url/#url=ZmlsZTovLy8vQzovLi4=&base=ZmlsZTovLy8=
+ // but not the behavior of rust-url.
+ // assertEquals(new URL("file:////C:/..").href, "file:///");
+
+ // Drop the hostname if a drive letter is parsed.
+ assertEquals(new URL("file://foo/C:").href, "file:///C:");
+
+ // Don't recognise drive letters in non-file protocols.
+ // FIXME(nayeemrmn): This is true according to
+ // https://jsdom.github.io/whatwg-url/#url=YWJjZDovL2Zvby9DOi8uLg==&base=ZmlsZTovLy8=
+ // but not the behavior of rust-url.
+ // assertEquals(new URL("http://foo/C:/..").href, "http://foo/");
+ // assertEquals(new URL("abcd://foo/C:/..").href, "abcd://foo/");
+});
+
+Deno.test(function urlHostnameUpperCase() {
+ assertEquals(new URL("http://EXAMPLE.COM").href, "http://example.com/");
+ assertEquals(new URL("abcd://EXAMPLE.COM").href, "abcd://EXAMPLE.COM");
+});
+
+Deno.test(function urlEmptyPath() {
+ assertEquals(new URL("http://foo").pathname, "/");
+ assertEquals(new URL("file://foo").pathname, "/");
+ assertEquals(new URL("abcd://foo").pathname, "");
+});
+
+Deno.test(function urlPathRepeatedSlashes() {
+ assertEquals(new URL("http://foo//bar//").pathname, "//bar//");
+ assertEquals(new URL("file://foo///bar//").pathname, "/bar//");
+ assertEquals(new URL("abcd://foo//bar//").pathname, "//bar//");
+});
+
+Deno.test(function urlTrim() {
+ assertEquals(new URL(" http://example.com ").href, "http://example.com/");
+});
+
+Deno.test(function urlEncoding() {
+ assertEquals(
+ new URL("http://a !$&*()=,;+'\"@example.com").username,
+ "a%20!$&*()%3D,%3B+'%22",
+ );
+ assertEquals(
+ new URL("http://:a !$&*()=,;+'\"@example.com").password,
+ "a%20!$&*()%3D,%3B+'%22",
+ );
+ // https://url.spec.whatwg.org/#idna
+ assertEquals(new URL("http://mañana/c?d#e").hostname, "xn--maana-pta");
+ assertEquals(new URL("abcd://mañana/c?d#e").hostname, "ma%C3%B1ana");
+ assertEquals(
+ new URL("http://example.com/a ~!@$&*()=:/,;+'\"\\").pathname,
+ "/a%20~!@$&*()=:/,;+'%22/",
+ );
+ assertEquals(
+ new URL("http://example.com?a ~!@$&*()=:/,;?+'\"\\").search,
+ "?a%20~!@$&*()=:/,;?+%27%22\\",
+ );
+ assertEquals(
+ new URL("abcd://example.com?a ~!@$&*()=:/,;?+'\"\\").search,
+ "?a%20~!@$&*()=:/,;?+'%22\\",
+ );
+ assertEquals(
+ new URL("http://example.com#a ~!@#$&*()=:/,;?+'\"\\").hash,
+ "#a%20~!@#$&*()=:/,;?+'%22\\",
+ );
+});
+
+Deno.test(function urlBase() {
+ assertEquals(new URL("d", new URL("http://foo/a?b#c")).href, "http://foo/d");
+
+ assertEquals(new URL("", "http://foo/a/b?c#d").href, "http://foo/a/b?c");
+ assertEquals(new URL("", "file://foo/a/b?c#d").href, "file://foo/a/b?c");
+ assertEquals(new URL("", "abcd://foo/a/b?c#d").href, "abcd://foo/a/b?c");
+
+ assertEquals(new URL("#e", "http://foo/a/b?c#d").href, "http://foo/a/b?c#e");
+ assertEquals(new URL("#e", "file://foo/a/b?c#d").href, "file://foo/a/b?c#e");
+ assertEquals(new URL("#e", "abcd://foo/a/b?c#d").href, "abcd://foo/a/b?c#e");
+
+ assertEquals(new URL("?e", "http://foo/a/b?c#d").href, "http://foo/a/b?e");
+ assertEquals(new URL("?e", "file://foo/a/b?c#d").href, "file://foo/a/b?e");
+ assertEquals(new URL("?e", "abcd://foo/a/b?c#d").href, "abcd://foo/a/b?e");
+
+ assertEquals(new URL("e", "http://foo/a/b?c#d").href, "http://foo/a/e");
+ assertEquals(new URL("e", "file://foo/a/b?c#d").href, "file://foo/a/e");
+ assertEquals(new URL("e", "abcd://foo/a/b?c#d").href, "abcd://foo/a/e");
+
+ assertEquals(new URL(".", "http://foo/a/b?c#d").href, "http://foo/a/");
+ assertEquals(new URL(".", "file://foo/a/b?c#d").href, "file://foo/a/");
+ assertEquals(new URL(".", "abcd://foo/a/b?c#d").href, "abcd://foo/a/");
+
+ assertEquals(new URL("..", "http://foo/a/b?c#d").href, "http://foo/");
+ assertEquals(new URL("..", "file://foo/a/b?c#d").href, "file://foo/");
+ assertEquals(new URL("..", "abcd://foo/a/b?c#d").href, "abcd://foo/");
+
+ assertEquals(new URL("/e", "http://foo/a/b?c#d").href, "http://foo/e");
+ assertEquals(new URL("/e", "file://foo/a/b?c#d").href, "file://foo/e");
+ assertEquals(new URL("/e", "abcd://foo/a/b?c#d").href, "abcd://foo/e");
+
+ assertEquals(new URL("//bar", "http://foo/a/b?c#d").href, "http://bar/");
+ assertEquals(new URL("//bar", "file://foo/a/b?c#d").href, "file://bar/");
+ assertEquals(new URL("//bar", "abcd://foo/a/b?c#d").href, "abcd://bar");
+
+ assertEquals(new URL("efgh:", "http://foo/a/b?c#d").href, "efgh:");
+ assertEquals(new URL("efgh:", "file://foo/a/b?c#d").href, "efgh:");
+ assertEquals(new URL("efgh:", "abcd://foo/a/b?c#d").href, "efgh:");
+
+ assertEquals(new URL("/foo", "abcd:/").href, "abcd:/foo");
+});
+
+Deno.test(function urlDriveLetterBase() {
+ assertEquals(new URL("/b", "file:///C:/a/b").href, "file:///C:/b");
+ assertEquals(new URL("/D:", "file:///C:/a/b").href, "file:///D:");
+});
+
+Deno.test(function urlSameProtocolBase() {
+ assertEquals(new URL("http:", "http://foo/a").href, "http://foo/a");
+ assertEquals(new URL("file:", "file://foo/a").href, "file://foo/a");
+ assertEquals(new URL("abcd:", "abcd://foo/a").href, "abcd:");
+
+ assertEquals(new URL("http:b", "http://foo/a").href, "http://foo/b");
+ assertEquals(new URL("file:b", "file://foo/a").href, "file://foo/b");
+ assertEquals(new URL("abcd:b", "abcd://foo/a").href, "abcd:b");
+});
+
+Deno.test(function deletingAllParamsRemovesQuestionMarkFromURL() {
+ const url = new URL("http://example.com/?param1&param2");
+ url.searchParams.delete("param1");
+ url.searchParams.delete("param2");
+ assertEquals(url.href, "http://example.com/");
+ assertEquals(url.search, "");
+});
+
+Deno.test(function removingNonExistentParamRemovesQuestionMarkFromURL() {
+ const url = new URL("http://example.com/?");
+ assertEquals(url.href, "http://example.com/?");
+ url.searchParams.delete("param1");
+ assertEquals(url.href, "http://example.com/");
+ assertEquals(url.search, "");
+});
+
+Deno.test(function sortingNonExistentParamRemovesQuestionMarkFromURL() {
+ const url = new URL("http://example.com/?");
+ assertEquals(url.href, "http://example.com/?");
+ url.searchParams.sort();
+ assertEquals(url.href, "http://example.com/");
+ assertEquals(url.search, "");
+});
+
+Deno.test(function customInspectFunction() {
+ const url = new URL("http://example.com/?");
+ assertEquals(
+ Deno.inspect(url),
+ `URL {
+ href: "http://example.com/?",
+ origin: "http://example.com",
+ protocol: "http:",
+ username: "",
+ password: "",
+ host: "example.com",
+ hostname: "example.com",
+ port: "",
+ pathname: "/",
+ hash: "",
+ search: ""
+}`,
+ );
+});
+
+Deno.test(function protocolNotHttpOrFile() {
+ const url = new URL("about:blank");
+ assertEquals(url.href, "about:blank");
+ assertEquals(url.protocol, "about:");
+ assertEquals(url.origin, "null");
+});
+
+Deno.test(function throwForInvalidPortConstructor() {
+ const urls = [
+ // If port is greater than 2^16 − 1, validation error, return failure.
+ `https://baz.qat:${2 ** 16}`,
+ "https://baz.qat:-32",
+ "https://baz.qat:deno",
+ "https://baz.qat:9land",
+ "https://baz.qat:10.5",
+ ];
+
+ for (const url of urls) {
+ assertThrows(() => new URL(url), TypeError, "Invalid URL");
+ }
+
+ // Do not throw for 0 & 65535
+ new URL("https://baz.qat:65535");
+ new URL("https://baz.qat:0");
+});
+
+Deno.test(function doNotOverridePortIfInvalid() {
+ const initialPort = "3000";
+ const url = new URL(`https://deno.land:${initialPort}`);
+ // If port is greater than 2^16 − 1, validation error, return failure.
+ url.port = `${2 ** 16}`;
+ assertEquals(url.port, initialPort);
+});
+
+Deno.test(function emptyPortForSchemeDefaultPort() {
+ const nonDefaultPort = "3500";
+
+ const url = new URL("ftp://baz.qat:21");
+ assertEquals(url.port, "");
+ url.port = nonDefaultPort;
+ assertEquals(url.port, nonDefaultPort);
+ url.port = "21";
+ assertEquals(url.port, "");
+ url.protocol = "http";
+ assertEquals(url.port, "");
+
+ const url2 = new URL("https://baz.qat:443");
+ assertEquals(url2.port, "");
+ url2.port = nonDefaultPort;
+ assertEquals(url2.port, nonDefaultPort);
+ url2.port = "443";
+ assertEquals(url2.port, "");
+ url2.protocol = "http";
+ assertEquals(url2.port, "");
+});
+
+Deno.test(function assigningPortPropertyAffectsReceiverOnly() {
+ // Setting `.port` should update only the receiver.
+ const u1 = new URL("http://google.com/");
+ // deno-lint-ignore no-explicit-any
+ const u2 = new URL(u1 as any);
+ u2.port = "123";
+ assertStrictEquals(u1.port, "");
+ assertStrictEquals(u2.port, "123");
+});
+
+Deno.test(function urlSearchParamsIdentityPreserved() {
+ // URLSearchParams identity should not be lost when URL is updated.
+ const u = new URL("http://foo.com/");
+ const sp1 = u.searchParams;
+ u.href = "http://bar.com/?baz=42";
+ const sp2 = u.searchParams;
+ assertStrictEquals(sp1, sp2);
+});
+
+Deno.test(function urlTakeURLObjectAsParameter() {
+ const url = new URL(
+ new URL(
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat",
+ ),
+ );
+ assertEquals(
+ url.href,
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat",
+ );
+});
diff --git a/tests/unit/urlpattern_test.ts b/tests/unit/urlpattern_test.ts
new file mode 100644
index 000000000..7730dbe40
--- /dev/null
+++ b/tests/unit/urlpattern_test.ts
@@ -0,0 +1,65 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals } from "./test_util.ts";
+import { assertType, IsExact } from "@test_util/std/testing/types.ts";
+
+Deno.test(function urlPatternFromString() {
+ const pattern = new URLPattern("https://deno.land/foo/:bar");
+ assertEquals(pattern.protocol, "https");
+ assertEquals(pattern.hostname, "deno.land");
+ assertEquals(pattern.pathname, "/foo/:bar");
+
+ assert(pattern.test("https://deno.land/foo/x"));
+ assert(!pattern.test("https://deno.com/foo/x"));
+ const match = pattern.exec("https://deno.land/foo/x");
+ assert(match);
+ assertEquals(match.pathname.input, "/foo/x");
+ assertEquals(match.pathname.groups, { bar: "x" });
+
+ // group values should be nullable
+ const val = match.pathname.groups.val;
+ assertType<IsExact<typeof val, string | undefined>>(true);
+});
+
+Deno.test(function urlPatternFromStringWithBase() {
+ const pattern = new URLPattern("/foo/:bar", "https://deno.land");
+ assertEquals(pattern.protocol, "https");
+ assertEquals(pattern.hostname, "deno.land");
+ assertEquals(pattern.pathname, "/foo/:bar");
+
+ assert(pattern.test("https://deno.land/foo/x"));
+ assert(!pattern.test("https://deno.com/foo/x"));
+ const match = pattern.exec("https://deno.land/foo/x");
+ assert(match);
+ assertEquals(match.pathname.input, "/foo/x");
+ assertEquals(match.pathname.groups, { bar: "x" });
+});
+
+Deno.test(function urlPatternFromInit() {
+ const pattern = new URLPattern({
+ pathname: "/foo/:bar",
+ });
+ assertEquals(pattern.protocol, "*");
+ assertEquals(pattern.hostname, "*");
+ assertEquals(pattern.pathname, "/foo/:bar");
+
+ assert(pattern.test("https://deno.land/foo/x"));
+ assert(pattern.test("https://deno.com/foo/x"));
+ assert(!pattern.test("https://deno.com/bar/x"));
+
+ assert(pattern.test({ pathname: "/foo/x" }));
+});
+
+Deno.test(function urlPatternWithPrototypePollution() {
+ const originalExec = RegExp.prototype.exec;
+ try {
+ RegExp.prototype.exec = () => {
+ throw Error();
+ };
+ const pattern = new URLPattern({
+ pathname: "/foo/:bar",
+ });
+ assert(pattern.test("https://deno.land/foo/x"));
+ } finally {
+ RegExp.prototype.exec = originalExec;
+ }
+});
diff --git a/tests/unit/utime_test.ts b/tests/unit/utime_test.ts
new file mode 100644
index 000000000..9f5f25bee
--- /dev/null
+++ b/tests/unit/utime_test.ts
@@ -0,0 +1,337 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assertEquals,
+ assertRejects,
+ assertThrows,
+ pathToAbsoluteFileUrl,
+} from "./test_util.ts";
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function futimeSyncSuccess() {
+ const testDir = await Deno.makeTempDir();
+ const filename = testDir + "/file.txt";
+ using file = await Deno.open(filename, {
+ create: true,
+ write: true,
+ });
+
+ const atime = 1000;
+ const mtime = 50000;
+ await Deno.futime(file.rid, atime, mtime);
+ await file.syncData();
+
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.atime, new Date(atime * 1000));
+ assertEquals(fileInfo.mtime, new Date(mtime * 1000));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function fsFileUtimeSyncSuccess() {
+ const testDir = await Deno.makeTempDir();
+ const filename = testDir + "/file.txt";
+ using file = await Deno.open(filename, {
+ create: true,
+ write: true,
+ });
+
+ const atime = 1000;
+ const mtime = 50000;
+ await file.utime(atime, mtime);
+ await file.syncData();
+
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.atime, new Date(atime * 1000));
+ assertEquals(fileInfo.mtime, new Date(mtime * 1000));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function futimeSyncSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const filename = testDir + "/file.txt";
+ using file = Deno.openSync(filename, {
+ create: true,
+ write: true,
+ });
+
+ const atime = 1000;
+ const mtime = 50000;
+ Deno.futimeSync(file.rid, atime, mtime);
+ file.syncDataSync();
+
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.atime, new Date(atime * 1000));
+ assertEquals(fileInfo.mtime, new Date(mtime * 1000));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function futimeSyncSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const filename = testDir + "/file.txt";
+ using file = Deno.openSync(filename, {
+ create: true,
+ write: true,
+ });
+
+ const atime = 1000;
+ const mtime = 50000;
+ file.utimeSync(atime, mtime);
+ file.syncDataSync();
+
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.atime, new Date(atime * 1000));
+ assertEquals(fileInfo.mtime, new Date(mtime * 1000));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function utimeSyncFileSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const filename = testDir + "/file.txt";
+ Deno.writeFileSync(filename, new TextEncoder().encode("hello"), {
+ mode: 0o666,
+ });
+
+ const atime = 1000;
+ const mtime = 50000;
+ Deno.utimeSync(filename, atime, mtime);
+
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.atime, new Date(atime * 1000));
+ assertEquals(fileInfo.mtime, new Date(mtime * 1000));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function utimeSyncUrlSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const filename = testDir + "/file.txt";
+ Deno.writeFileSync(filename, new TextEncoder().encode("hello"), {
+ mode: 0o666,
+ });
+
+ const atime = 1000;
+ const mtime = 50000;
+ Deno.utimeSync(pathToAbsoluteFileUrl(filename), atime, mtime);
+
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.atime, new Date(atime * 1000));
+ assertEquals(fileInfo.mtime, new Date(mtime * 1000));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function utimeSyncDirectorySuccess() {
+ const testDir = Deno.makeTempDirSync();
+
+ const atime = 1000;
+ const mtime = 50000;
+ Deno.utimeSync(testDir, atime, mtime);
+
+ const dirInfo = Deno.statSync(testDir);
+ assertEquals(dirInfo.atime, new Date(atime * 1000));
+ assertEquals(dirInfo.mtime, new Date(mtime * 1000));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function utimeSyncDateSuccess() {
+ const testDir = Deno.makeTempDirSync();
+
+ const atime = new Date(1000_000);
+ const mtime = new Date(50000_000);
+ Deno.utimeSync(testDir, atime, mtime);
+
+ const dirInfo = Deno.statSync(testDir);
+ assertEquals(dirInfo.atime, atime);
+ assertEquals(dirInfo.mtime, mtime);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function utimeSyncFileDateSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const filename = testDir + "/file.txt";
+ Deno.writeFileSync(filename, new TextEncoder().encode("hello"), {
+ mode: 0o666,
+ });
+ const atime = new Date();
+ const mtime = new Date();
+ Deno.utimeSync(filename, atime, mtime);
+
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.atime, atime);
+ assertEquals(fileInfo.mtime, mtime);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function utimeSyncLargeNumberSuccess() {
+ const testDir = Deno.makeTempDirSync();
+
+ // There are Rust side caps (might be fs relate),
+ // so JUST make them slightly larger than UINT32_MAX.
+ const atime = 0x100000001;
+ const mtime = 0x100000002;
+ Deno.utimeSync(testDir, atime, mtime);
+
+ const dirInfo = Deno.statSync(testDir);
+ assertEquals(dirInfo.atime, new Date(atime * 1000));
+ assertEquals(dirInfo.mtime, new Date(mtime * 1000));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function utimeSyncNotFound() {
+ const atime = 1000;
+ const mtime = 50000;
+
+ assertThrows(
+ () => {
+ Deno.utimeSync("/baddir", atime, mtime);
+ },
+ Deno.errors.NotFound,
+ "utime '/baddir'",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: false } },
+ function utimeSyncPerm() {
+ const atime = 1000;
+ const mtime = 50000;
+
+ assertThrows(() => {
+ Deno.utimeSync("/some_dir", atime, mtime);
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function utimeFileSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const filename = testDir + "/file.txt";
+ Deno.writeFileSync(filename, new TextEncoder().encode("hello"), {
+ mode: 0o666,
+ });
+
+ const atime = 1000;
+ const mtime = 50000;
+ await Deno.utime(filename, atime, mtime);
+
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.atime, new Date(atime * 1000));
+ assertEquals(fileInfo.mtime, new Date(mtime * 1000));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function utimeUrlSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const filename = testDir + "/file.txt";
+ Deno.writeFileSync(filename, new TextEncoder().encode("hello"), {
+ mode: 0o666,
+ });
+
+ const atime = 1000;
+ const mtime = 50000;
+ await Deno.utime(pathToAbsoluteFileUrl(filename), atime, mtime);
+
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.atime, new Date(atime * 1000));
+ assertEquals(fileInfo.mtime, new Date(mtime * 1000));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function utimeDirectorySuccess() {
+ const testDir = Deno.makeTempDirSync();
+
+ const atime = 1000;
+ const mtime = 50000;
+ await Deno.utime(testDir, atime, mtime);
+
+ const dirInfo = Deno.statSync(testDir);
+ assertEquals(dirInfo.atime, new Date(atime * 1000));
+ assertEquals(dirInfo.mtime, new Date(mtime * 1000));
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function utimeDateSuccess() {
+ const testDir = Deno.makeTempDirSync();
+
+ const atime = new Date(100_000);
+ const mtime = new Date(5000_000);
+ await Deno.utime(testDir, atime, mtime);
+
+ const dirInfo = Deno.statSync(testDir);
+ assertEquals(dirInfo.atime, atime);
+ assertEquals(dirInfo.mtime, mtime);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function utimeFileDateSuccess() {
+ const testDir = Deno.makeTempDirSync();
+ const filename = testDir + "/file.txt";
+ Deno.writeFileSync(filename, new TextEncoder().encode("hello"), {
+ mode: 0o666,
+ });
+
+ const atime = new Date();
+ const mtime = new Date();
+ await Deno.utime(filename, atime, mtime);
+
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.atime, atime);
+ assertEquals(fileInfo.mtime, mtime);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function utimeNotFound() {
+ const atime = 1000;
+ const mtime = 50000;
+
+ await assertRejects(
+ async () => {
+ await Deno.utime("/baddir", atime, mtime);
+ },
+ Deno.errors.NotFound,
+ "utime '/baddir'",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: false } },
+ async function utimeSyncPerm() {
+ const atime = 1000;
+ const mtime = 50000;
+
+ await assertRejects(async () => {
+ await Deno.utime("/some_dir", atime, mtime);
+ }, Deno.errors.PermissionDenied);
+ },
+);
diff --git a/tests/unit/version_test.ts b/tests/unit/version_test.ts
new file mode 100644
index 000000000..4eadb7620
--- /dev/null
+++ b/tests/unit/version_test.ts
@@ -0,0 +1,10 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assert, assertEquals } from "./test_util.ts";
+
+Deno.test(function version() {
+ const pattern = /^\d+\.\d+\.\d+/;
+ assert(pattern.test(Deno.version.deno));
+ assert(pattern.test(Deno.version.v8));
+ assertEquals(Deno.version.typescript, "5.3.3");
+});
diff --git a/tests/unit/wasm_test.ts b/tests/unit/wasm_test.ts
new file mode 100644
index 000000000..fab9c9308
--- /dev/null
+++ b/tests/unit/wasm_test.ts
@@ -0,0 +1,104 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assert, assertEquals, assertRejects } from "./test_util.ts";
+
+// The following blob can be created by taking the following s-expr and pass
+// it through wat2wasm.
+// (module
+// (func $add (param $a i32) (param $b i32) (result i32)
+// local.get $a
+// local.get $b
+// i32.add)
+// (export "add" (func $add))
+// )
+// deno-fmt-ignore
+const simpleWasm = new Uint8Array([
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60,
+ 0x02, 0x7f, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01,
+ 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20,
+ 0x00, 0x20, 0x01, 0x6a, 0x0b
+]);
+
+Deno.test(async function wasmInstantiateWorksWithBuffer() {
+ const { module, instance } = await WebAssembly.instantiate(simpleWasm);
+ assertEquals(WebAssembly.Module.exports(module), [{
+ name: "add",
+ kind: "function",
+ }]);
+ assertEquals(WebAssembly.Module.imports(module), []);
+ assert(typeof instance.exports.add === "function");
+ const add = instance.exports.add as (a: number, b: number) => number;
+ assertEquals(add(1, 3), 4);
+});
+
+// V8's default implementation of `WebAssembly.instantiateStreaming()` if you
+// don't set the WASM streaming callback, is to take a byte source. Here we
+// check that our implementation of the callback disallows it.
+Deno.test(
+ async function wasmInstantiateStreamingFailsWithBuffer() {
+ await assertRejects(async () => {
+ await WebAssembly.instantiateStreaming(
+ // Bypassing the type system
+ simpleWasm as unknown as Promise<Response>,
+ );
+ }, TypeError);
+ },
+);
+
+Deno.test(
+ async function wasmInstantiateStreamingNoContentType() {
+ const response = new Response(simpleWasm);
+ // Rejects, not throws.
+ const wasmPromise = WebAssembly.instantiateStreaming(response);
+ await assertRejects(
+ () => wasmPromise,
+ TypeError,
+ "Invalid WebAssembly content type.",
+ );
+ },
+);
+
+Deno.test(async function wasmInstantiateStreaming() {
+ let isomorphic = "";
+ for (const byte of simpleWasm) {
+ isomorphic += String.fromCharCode(byte);
+ }
+ const base64Url = "data:application/wasm;base64," + btoa(isomorphic);
+
+ const { module, instance } = await WebAssembly.instantiateStreaming(
+ fetch(base64Url),
+ );
+ assertEquals(WebAssembly.Module.exports(module), [{
+ name: "add",
+ kind: "function",
+ }]);
+ assertEquals(WebAssembly.Module.imports(module), []);
+ assert(typeof instance.exports.add === "function");
+ const add = instance.exports.add as (a: number, b: number) => number;
+ assertEquals(add(1, 3), 4);
+});
+
+Deno.test(
+ { permissions: { read: true } },
+ async function wasmFileStreaming() {
+ const url = import.meta.resolve("../testdata/assets/unreachable.wasm");
+ assert(url.startsWith("file://"));
+
+ const { module } = await WebAssembly.instantiateStreaming(fetch(url));
+ assertEquals(WebAssembly.Module.exports(module), [{
+ name: "unreachable",
+ kind: "function",
+ }]);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function wasmStreamingNonTrivial() {
+ // deno-dom's WASM file is a real-world non-trivial case that gave us
+ // trouble when implementing this.
+ await WebAssembly.instantiateStreaming(fetch(
+ "http://localhost:4545/assets/deno_dom_0.1.3-alpha2.wasm",
+ ));
+ },
+);
diff --git a/tests/unit/webcrypto_test.ts b/tests/unit/webcrypto_test.ts
new file mode 100644
index 000000000..58f59edc6
--- /dev/null
+++ b/tests/unit/webcrypto_test.ts
@@ -0,0 +1,2047 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import {
+ assert,
+ assertEquals,
+ assertNotEquals,
+ assertRejects,
+} from "./test_util.ts";
+
+// https://github.com/denoland/deno/issues/11664
+Deno.test(async function testImportArrayBufferKey() {
+ const subtle = window.crypto.subtle;
+ assert(subtle);
+
+ // deno-fmt-ignore
+ const key = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
+
+ const cryptoKey = await subtle.importKey(
+ "raw",
+ key.buffer,
+ { name: "HMAC", hash: "SHA-1" },
+ true,
+ ["sign"],
+ );
+ assert(cryptoKey);
+
+ // Test key usage
+ await subtle.sign({ name: "HMAC" }, cryptoKey, new Uint8Array(8));
+});
+
+Deno.test(async function testSignVerify() {
+ const subtle = window.crypto.subtle;
+ assert(subtle);
+ for (const algorithm of ["RSA-PSS", "RSASSA-PKCS1-v1_5"]) {
+ for (
+ const hash of [
+ "SHA-1",
+ "SHA-256",
+ "SHA-384",
+ "SHA-512",
+ ]
+ ) {
+ const keyPair = await subtle.generateKey(
+ {
+ name: algorithm,
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash,
+ },
+ true,
+ ["sign", "verify"],
+ );
+
+ const data = new Uint8Array([1, 2, 3]);
+
+ const signAlgorithm = { name: algorithm, saltLength: 32 };
+
+ const signature = await subtle.sign(
+ signAlgorithm,
+ keyPair.privateKey,
+ data,
+ );
+
+ assert(signature);
+ assert(signature.byteLength > 0);
+ assert(signature.byteLength % 8 == 0);
+ assert(signature instanceof ArrayBuffer);
+
+ const verified = await subtle.verify(
+ signAlgorithm,
+ keyPair.publicKey,
+ signature,
+ data,
+ );
+ assert(verified);
+ }
+ }
+});
+
+// deno-fmt-ignore
+const plainText = new Uint8Array([95, 77, 186, 79, 50, 12, 12, 232, 118, 114, 90, 252, 229, 251, 210, 91, 248, 62, 90, 113, 37, 160, 140, 175, 231, 60, 62, 186, 196, 33, 119, 157, 249, 213, 93, 24, 12, 58, 233, 148, 38, 69, 225, 216, 47, 238, 140, 157, 41, 75, 60, 177, 160, 138, 153, 49, 32, 27, 60, 14, 129, 252, 71, 202, 207, 131, 21, 162, 175, 102, 50, 65, 19, 195, 182, 98, 48, 195, 70, 8, 196, 244, 89, 54, 52, 206, 2, 178, 103, 54, 34, 119, 240, 168, 64, 202, 116, 188, 61, 26, 98, 54, 149, 44, 94, 215, 170, 248, 168, 254, 203, 221, 250, 117, 132, 230, 151, 140, 234, 93, 42, 91, 159, 183, 241, 180, 140, 139, 11, 229, 138, 48, 82, 2, 117, 77, 131, 118, 16, 115, 116, 121, 60, 240, 38, 170, 238, 83, 0, 114, 125, 131, 108, 215, 30, 113, 179, 69, 221, 178, 228, 68, 70, 255, 197, 185, 1, 99, 84, 19, 137, 13, 145, 14, 163, 128, 152, 74, 144, 25, 16, 49, 50, 63, 22, 219, 204, 157, 107, 225, 104, 184, 72, 133, 56, 76, 160, 62, 18, 96, 10, 193, 194, 72, 2, 138, 243, 114, 108, 201, 52, 99, 136, 46, 168, 192, 42, 171]);
+
+// Passing
+const hashPlainTextVector = [
+ {
+ hash: "SHA-1",
+ plainText: plainText.slice(0, 214),
+ },
+ {
+ hash: "SHA-256",
+ plainText: plainText.slice(0, 190),
+ },
+ {
+ hash: "SHA-384",
+ plainText: plainText.slice(0, 158),
+ },
+ {
+ hash: "SHA-512",
+ plainText: plainText.slice(0, 126),
+ },
+];
+
+Deno.test(async function testEncryptDecrypt() {
+ const subtle = window.crypto.subtle;
+ assert(subtle);
+ for (
+ const { hash, plainText } of hashPlainTextVector
+ ) {
+ const keyPair = await subtle.generateKey(
+ {
+ name: "RSA-OAEP",
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash,
+ },
+ true,
+ ["encrypt", "decrypt"],
+ );
+
+ const encryptAlgorithm = { name: "RSA-OAEP" };
+ const cipherText = await subtle.encrypt(
+ encryptAlgorithm,
+ keyPair.publicKey,
+ plainText,
+ );
+
+ assert(cipherText);
+ assert(cipherText.byteLength > 0);
+ assertEquals(cipherText.byteLength * 8, 2048);
+ assert(cipherText instanceof ArrayBuffer);
+
+ const decrypted = await subtle.decrypt(
+ encryptAlgorithm,
+ keyPair.privateKey,
+ cipherText,
+ );
+ assert(decrypted);
+ assert(decrypted instanceof ArrayBuffer);
+ assertEquals(new Uint8Array(decrypted), plainText);
+
+ const badPlainText = new Uint8Array(plainText.byteLength + 1);
+ badPlainText.set(plainText, 0);
+ badPlainText.set(new Uint8Array([32]), plainText.byteLength);
+ await assertRejects(async () => {
+ // Should fail
+ await subtle.encrypt(
+ encryptAlgorithm,
+ keyPair.publicKey,
+ badPlainText,
+ );
+ throw new TypeError("unreachable");
+ }, DOMException);
+ }
+});
+
+Deno.test(async function testGenerateRSAKey() {
+ const subtle = window.crypto.subtle;
+ assert(subtle);
+
+ const keyPair = await subtle.generateKey(
+ {
+ name: "RSA-PSS",
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: "SHA-256",
+ },
+ true,
+ ["sign", "verify"],
+ );
+
+ assert(keyPair.privateKey);
+ assert(keyPair.publicKey);
+ assertEquals(keyPair.privateKey.extractable, true);
+ assert(keyPair.privateKey.usages.includes("sign"));
+});
+
+Deno.test(async function testGenerateHMACKey() {
+ const key = await window.crypto.subtle.generateKey(
+ {
+ name: "HMAC",
+ hash: "SHA-512",
+ },
+ true,
+ ["sign", "verify"],
+ );
+
+ assert(key);
+ assertEquals(key.extractable, true);
+ assert(key.usages.includes("sign"));
+});
+
+Deno.test(async function testECDSASignVerify() {
+ const key = await window.crypto.subtle.generateKey(
+ {
+ name: "ECDSA",
+ namedCurve: "P-384",
+ },
+ true,
+ ["sign", "verify"],
+ );
+
+ const encoder = new TextEncoder();
+ const encoded = encoder.encode("Hello, World!");
+ const signature = await window.crypto.subtle.sign(
+ { name: "ECDSA", hash: "SHA-384" },
+ key.privateKey,
+ encoded,
+ );
+
+ assert(signature);
+ assert(signature instanceof ArrayBuffer);
+
+ const verified = await window.crypto.subtle.verify(
+ { hash: { name: "SHA-384" }, name: "ECDSA" },
+ key.publicKey,
+ signature,
+ encoded,
+ );
+ assert(verified);
+});
+
+// Tests the "bad paths" as a temporary replacement for sign_verify/ecdsa WPT.
+Deno.test(async function testECDSASignVerifyFail() {
+ const key = await window.crypto.subtle.generateKey(
+ {
+ name: "ECDSA",
+ namedCurve: "P-384",
+ },
+ true,
+ ["sign", "verify"],
+ );
+
+ const encoded = new Uint8Array([1]);
+ // Signing with a public key (InvalidAccessError)
+ await assertRejects(async () => {
+ await window.crypto.subtle.sign(
+ { name: "ECDSA", hash: "SHA-384" },
+ key.publicKey,
+ new Uint8Array([1]),
+ );
+ throw new TypeError("unreachable");
+ }, DOMException);
+
+ // Do a valid sign for later verifying.
+ const signature = await window.crypto.subtle.sign(
+ { name: "ECDSA", hash: "SHA-384" },
+ key.privateKey,
+ encoded,
+ );
+
+ // Verifying with a private key (InvalidAccessError)
+ await assertRejects(async () => {
+ await window.crypto.subtle.verify(
+ { hash: { name: "SHA-384" }, name: "ECDSA" },
+ key.privateKey,
+ signature,
+ encoded,
+ );
+ throw new TypeError("unreachable");
+ }, DOMException);
+});
+
+// https://github.com/denoland/deno/issues/11313
+Deno.test(async function testSignRSASSAKey() {
+ const subtle = window.crypto.subtle;
+ assert(subtle);
+
+ const keyPair = await subtle.generateKey(
+ {
+ name: "RSASSA-PKCS1-v1_5",
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: "SHA-256",
+ },
+ true,
+ ["sign", "verify"],
+ );
+
+ assert(keyPair.privateKey);
+ assert(keyPair.publicKey);
+ assertEquals(keyPair.privateKey.extractable, true);
+ assert(keyPair.privateKey.usages.includes("sign"));
+
+ const encoder = new TextEncoder();
+ const encoded = encoder.encode("Hello, World!");
+
+ const signature = await window.crypto.subtle.sign(
+ { name: "RSASSA-PKCS1-v1_5" },
+ keyPair.privateKey,
+ encoded,
+ );
+
+ assert(signature);
+});
+
+// deno-fmt-ignore
+const rawKey = new Uint8Array([
+ 1, 2, 3, 4, 5, 6, 7, 8,
+ 9, 10, 11, 12, 13, 14, 15, 16
+]);
+
+const jwk: JsonWebKey = {
+ kty: "oct",
+ // unpadded base64 for rawKey.
+ k: "AQIDBAUGBwgJCgsMDQ4PEA",
+ alg: "HS256",
+ ext: true,
+ "key_ops": ["sign"],
+};
+
+Deno.test(async function subtleCryptoHmacImportExport() {
+ const key1 = await crypto.subtle.importKey(
+ "raw",
+ rawKey,
+ { name: "HMAC", hash: "SHA-256" },
+ true,
+ ["sign"],
+ );
+ const key2 = await crypto.subtle.importKey(
+ "jwk",
+ jwk,
+ { name: "HMAC", hash: "SHA-256" },
+ true,
+ ["sign"],
+ );
+ const actual1 = await crypto.subtle.sign(
+ { name: "HMAC" },
+ key1,
+ new Uint8Array([1, 2, 3, 4]),
+ );
+
+ const actual2 = await crypto.subtle.sign(
+ { name: "HMAC" },
+ key2,
+ new Uint8Array([1, 2, 3, 4]),
+ );
+ // deno-fmt-ignore
+ const expected = new Uint8Array([
+ 59, 170, 255, 216, 51, 141, 51, 194,
+ 213, 48, 41, 191, 184, 40, 216, 47,
+ 130, 165, 203, 26, 163, 43, 38, 71,
+ 23, 122, 222, 1, 146, 46, 182, 87,
+ ]);
+ assertEquals(
+ new Uint8Array(actual1),
+ expected,
+ );
+ assertEquals(
+ new Uint8Array(actual2),
+ expected,
+ );
+
+ const exportedKey1 = await crypto.subtle.exportKey("raw", key1);
+ assertEquals(new Uint8Array(exportedKey1), rawKey);
+
+ const exportedKey2 = await crypto.subtle.exportKey("jwk", key2);
+ assertEquals(exportedKey2, jwk);
+});
+
+// https://github.com/denoland/deno/issues/12085
+Deno.test(async function generateImportHmacJwk() {
+ const key = await crypto.subtle.generateKey(
+ {
+ name: "HMAC",
+ hash: "SHA-512",
+ },
+ true,
+ ["sign"],
+ );
+ assert(key);
+ assertEquals(key.type, "secret");
+ assertEquals(key.extractable, true);
+ assertEquals(key.usages, ["sign"]);
+
+ const exportedKey = await crypto.subtle.exportKey("jwk", key);
+ assertEquals(exportedKey.kty, "oct");
+ assertEquals(exportedKey.alg, "HS512");
+ assertEquals(exportedKey.key_ops, ["sign"]);
+ assertEquals(exportedKey.ext, true);
+ assert(typeof exportedKey.k == "string");
+ assertEquals(exportedKey.k.length, 171);
+});
+
+// 2048-bits publicExponent=65537
+const pkcs8TestVectors = [
+ // rsaEncryption
+ { pem: "tests/testdata/webcrypto/id_rsaEncryption.pem", hash: "SHA-256" },
+];
+
+Deno.test({ permissions: { read: true } }, async function importRsaPkcs8() {
+ const pemHeader = "-----BEGIN PRIVATE KEY-----";
+ const pemFooter = "-----END PRIVATE KEY-----";
+ for (const { pem, hash } of pkcs8TestVectors) {
+ const keyFile = await Deno.readTextFile(pem);
+ const pemContents = keyFile.substring(
+ pemHeader.length,
+ keyFile.length - pemFooter.length,
+ );
+ const binaryDerString = atob(pemContents);
+ const binaryDer = new Uint8Array(binaryDerString.length);
+ for (let i = 0; i < binaryDerString.length; i++) {
+ binaryDer[i] = binaryDerString.charCodeAt(i);
+ }
+
+ const key = await crypto.subtle.importKey(
+ "pkcs8",
+ binaryDer,
+ { name: "RSA-PSS", hash },
+ true,
+ ["sign"],
+ );
+
+ assert(key);
+ assertEquals(key.type, "private");
+ assertEquals(key.extractable, true);
+ assertEquals(key.usages, ["sign"]);
+ const algorithm = key.algorithm as RsaHashedKeyAlgorithm;
+ assertEquals(algorithm.name, "RSA-PSS");
+ assertEquals(algorithm.hash.name, hash);
+ assertEquals(algorithm.modulusLength, 2048);
+ assertEquals(algorithm.publicExponent, new Uint8Array([1, 0, 1]));
+ }
+});
+
+const nonInteroperableVectors = [
+ // id-RSASSA-PSS (sha256)
+ // `openssl genpkey -algorithm rsa-pss -pkeyopt rsa_pss_keygen_md:sha256 -out id_rsassaPss.pem`
+ { pem: "tests/testdata/webcrypto/id_rsassaPss.pem", hash: "SHA-256" },
+ // id-RSASSA-PSS (default parameters)
+ // `openssl genpkey -algorithm rsa-pss -out id_rsassaPss.pem`
+ {
+ pem: "tests/testdata/webcrypto/id_rsassaPss_default.pem",
+ hash: "SHA-1",
+ },
+ // id-RSASSA-PSS (default hash)
+ // `openssl genpkey -algorithm rsa-pss -pkeyopt rsa_pss_keygen_saltlen:30 -out rsaPss_saltLen_30.pem`
+ {
+ pem: "tests/testdata/webcrypto/id_rsassaPss_saltLen_30.pem",
+ hash: "SHA-1",
+ },
+];
+
+Deno.test(
+ { permissions: { read: true } },
+ async function importNonInteroperableRsaPkcs8() {
+ const pemHeader = "-----BEGIN PRIVATE KEY-----";
+ const pemFooter = "-----END PRIVATE KEY-----";
+ for (const { pem, hash } of nonInteroperableVectors) {
+ const keyFile = await Deno.readTextFile(pem);
+ const pemContents = keyFile.substring(
+ pemHeader.length,
+ keyFile.length - pemFooter.length,
+ );
+ const binaryDerString = atob(pemContents);
+ const binaryDer = new Uint8Array(binaryDerString.length);
+ for (let i = 0; i < binaryDerString.length; i++) {
+ binaryDer[i] = binaryDerString.charCodeAt(i);
+ }
+
+ await assertRejects(
+ () =>
+ crypto.subtle.importKey(
+ "pkcs8",
+ binaryDer,
+ { name: "RSA-PSS", hash },
+ true,
+ ["sign"],
+ ),
+ DOMException,
+ "unsupported algorithm",
+ );
+ }
+ },
+);
+
+// deno-fmt-ignore
+const asn1AlgorithmIdentifier = new Uint8Array([
+ 0x02, 0x01, 0x00, // INTEGER
+ 0x30, 0x0d, // SEQUENCE (2 elements)
+ 0x06, 0x09, // OBJECT IDENTIFIER
+ 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, // 1.2.840.113549.1.1.1 (rsaEncryption)
+ 0x05, 0x00, // NULL
+]);
+
+Deno.test(async function rsaExport() {
+ for (const algorithm of ["RSASSA-PKCS1-v1_5", "RSA-PSS", "RSA-OAEP"]) {
+ const keyPair = await crypto.subtle.generateKey(
+ {
+ name: algorithm,
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: "SHA-256",
+ },
+ true,
+ algorithm !== "RSA-OAEP" ? ["sign", "verify"] : ["encrypt", "decrypt"],
+ );
+
+ assert(keyPair.privateKey);
+ assert(keyPair.publicKey);
+ assertEquals(keyPair.privateKey.extractable, true);
+
+ const exportedPrivateKey = await crypto.subtle.exportKey(
+ "pkcs8",
+ keyPair.privateKey,
+ );
+
+ assert(exportedPrivateKey);
+ assert(exportedPrivateKey instanceof ArrayBuffer);
+
+ const pkcs8 = new Uint8Array(exportedPrivateKey);
+ assert(pkcs8.length > 0);
+
+ assertEquals(
+ pkcs8.slice(4, asn1AlgorithmIdentifier.byteLength + 4),
+ asn1AlgorithmIdentifier,
+ );
+
+ const exportedPublicKey = await crypto.subtle.exportKey(
+ "spki",
+ keyPair.publicKey,
+ );
+
+ const spki = new Uint8Array(exportedPublicKey);
+ assert(spki.length > 0);
+
+ assertEquals(
+ spki.slice(4, asn1AlgorithmIdentifier.byteLength + 1),
+ asn1AlgorithmIdentifier.slice(3),
+ );
+ }
+});
+
+Deno.test(async function testHkdfDeriveBits() {
+ const rawKey = crypto.getRandomValues(new Uint8Array(16));
+ const key = await crypto.subtle.importKey(
+ "raw",
+ rawKey,
+ { name: "HKDF", hash: "SHA-256" },
+ false,
+ ["deriveBits"],
+ );
+ const salt = crypto.getRandomValues(new Uint8Array(16));
+ const info = crypto.getRandomValues(new Uint8Array(16));
+ const result = await crypto.subtle.deriveBits(
+ {
+ name: "HKDF",
+ hash: "SHA-256",
+ salt: salt,
+ info: info,
+ },
+ key,
+ 128,
+ );
+ assertEquals(result.byteLength, 128 / 8);
+});
+
+Deno.test(async function testHkdfDeriveBitsWithLargeKeySize() {
+ const key = await crypto.subtle.importKey(
+ "raw",
+ new Uint8Array([0x00]),
+ "HKDF",
+ false,
+ ["deriveBits"],
+ );
+ await assertRejects(
+ () =>
+ crypto.subtle.deriveBits(
+ {
+ name: "HKDF",
+ hash: "SHA-1",
+ salt: new Uint8Array(),
+ info: new Uint8Array(),
+ },
+ key,
+ ((20 * 255) << 3) + 8,
+ ),
+ DOMException,
+ "The length provided for HKDF is too large",
+ );
+});
+
+Deno.test(async function testEcdhDeriveBitsWithShorterLength() {
+ const keypair = await crypto.subtle.generateKey(
+ {
+ name: "ECDH",
+ namedCurve: "P-384",
+ },
+ true,
+ ["deriveBits", "deriveKey"],
+ );
+ const result = await crypto.subtle.deriveBits(
+ {
+ name: "ECDH",
+ public: keypair.publicKey,
+ },
+ keypair.privateKey,
+ 256,
+ );
+ assertEquals(result.byteLength * 8, 256);
+});
+
+Deno.test(async function testEcdhDeriveBitsWithLongerLength() {
+ const keypair = await crypto.subtle.generateKey(
+ {
+ name: "ECDH",
+ namedCurve: "P-384",
+ },
+ true,
+ ["deriveBits", "deriveKey"],
+ );
+ await assertRejects(
+ () =>
+ crypto.subtle.deriveBits(
+ {
+ name: "ECDH",
+ public: keypair.publicKey,
+ },
+ keypair.privateKey,
+ 512,
+ ),
+ DOMException,
+ "Invalid length",
+ );
+});
+
+Deno.test(async function testEcdhDeriveBitsWithNullLength() {
+ const keypair = await crypto.subtle.generateKey(
+ {
+ name: "ECDH",
+ namedCurve: "P-384",
+ },
+ true,
+ ["deriveBits", "deriveKey"],
+ );
+ const result = await crypto.subtle.deriveBits(
+ {
+ name: "ECDH",
+ public: keypair.publicKey,
+ },
+ keypair.privateKey,
+ // @ts-ignore: necessary until .d.ts file allows passing null (see https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1416)
+ null,
+ );
+ assertEquals(result.byteLength * 8, 384);
+});
+
+Deno.test(async function testDeriveKey() {
+ // Test deriveKey
+ const rawKey = crypto.getRandomValues(new Uint8Array(16));
+ const key = await crypto.subtle.importKey(
+ "raw",
+ rawKey,
+ "PBKDF2",
+ false,
+ ["deriveKey", "deriveBits"],
+ );
+
+ const salt = crypto.getRandomValues(new Uint8Array(16));
+ const derivedKey = await crypto.subtle.deriveKey(
+ {
+ name: "PBKDF2",
+ salt,
+ iterations: 1000,
+ hash: "SHA-256",
+ },
+ key,
+ { name: "HMAC", hash: "SHA-256" },
+ true,
+ ["sign"],
+ );
+
+ assert(derivedKey instanceof CryptoKey);
+ assertEquals(derivedKey.type, "secret");
+ assertEquals(derivedKey.extractable, true);
+ assertEquals(derivedKey.usages, ["sign"]);
+
+ const algorithm = derivedKey.algorithm as HmacKeyAlgorithm;
+ assertEquals(algorithm.name, "HMAC");
+ assertEquals(algorithm.hash.name, "SHA-256");
+ assertEquals(algorithm.length, 512);
+});
+
+Deno.test(async function testAesCbcEncryptDecrypt() {
+ const key = await crypto.subtle.generateKey(
+ { name: "AES-CBC", length: 128 },
+ true,
+ ["encrypt", "decrypt"],
+ );
+
+ const iv = crypto.getRandomValues(new Uint8Array(16));
+ const encrypted = await crypto.subtle.encrypt(
+ {
+ name: "AES-CBC",
+ iv,
+ },
+ key as CryptoKey,
+ new Uint8Array([1, 2, 3, 4, 5, 6]),
+ );
+
+ assert(encrypted instanceof ArrayBuffer);
+ assertEquals(encrypted.byteLength, 16);
+
+ const decrypted = await crypto.subtle.decrypt(
+ {
+ name: "AES-CBC",
+ iv,
+ },
+ key as CryptoKey,
+ encrypted,
+ );
+
+ assert(decrypted instanceof ArrayBuffer);
+ assertEquals(decrypted.byteLength, 6);
+ assertEquals(new Uint8Array(decrypted), new Uint8Array([1, 2, 3, 4, 5, 6]));
+});
+
+Deno.test(async function testAesCtrEncryptDecrypt() {
+ async function aesCtrRoundTrip(
+ key: CryptoKey,
+ counter: Uint8Array,
+ length: number,
+ plainText: Uint8Array,
+ ) {
+ const cipherText = await crypto.subtle.encrypt(
+ {
+ name: "AES-CTR",
+ counter,
+ length,
+ },
+ key,
+ plainText,
+ );
+
+ assert(cipherText instanceof ArrayBuffer);
+ assertEquals(cipherText.byteLength, plainText.byteLength);
+ assertNotEquals(new Uint8Array(cipherText), plainText);
+
+ const decryptedText = await crypto.subtle.decrypt(
+ {
+ name: "AES-CTR",
+ counter,
+ length,
+ },
+ key,
+ cipherText,
+ );
+
+ assert(decryptedText instanceof ArrayBuffer);
+ assertEquals(decryptedText.byteLength, plainText.byteLength);
+ assertEquals(new Uint8Array(decryptedText), plainText);
+ }
+ for (const keySize of [128, 192, 256]) {
+ const key = await crypto.subtle.generateKey(
+ { name: "AES-CTR", length: keySize },
+ true,
+ ["encrypt", "decrypt"],
+ ) as CryptoKey;
+
+ // test normal operation
+ for (const length of [128 /*, 64, 128 */]) {
+ const counter = crypto.getRandomValues(new Uint8Array(16));
+
+ await aesCtrRoundTrip(
+ key,
+ counter,
+ length,
+ new Uint8Array([1, 2, 3, 4, 5, 6]),
+ );
+ }
+
+ // test counter-wrapping
+ for (const length of [32, 64, 128]) {
+ const plaintext1 = crypto.getRandomValues(new Uint8Array(32));
+ const counter = new Uint8Array(16);
+
+ // fixed upper part
+ for (let off = 0; off < 16 - (length / 8); ++off) {
+ counter[off] = off;
+ }
+ const ciphertext1 = await crypto.subtle.encrypt(
+ {
+ name: "AES-CTR",
+ counter,
+ length,
+ },
+ key,
+ plaintext1,
+ );
+
+ // Set lower [length] counter bits to all '1's
+ for (let off = 16 - (length / 8); off < 16; ++off) {
+ counter[off] = 0xff;
+ }
+
+ // = [ 1 block of 0x00 + plaintext1 ]
+ const plaintext2 = new Uint8Array(48);
+ plaintext2.set(plaintext1, 16);
+
+ const ciphertext2 = await crypto.subtle.encrypt(
+ {
+ name: "AES-CTR",
+ counter,
+ length,
+ },
+ key,
+ plaintext2,
+ );
+
+ // If counter wrapped, 2nd block of ciphertext2 should be equal to 1st block of ciphertext1
+ // since ciphertext1 used counter = 0x00...00
+ // and ciphertext2 used counter = 0xFF..FF which should wrap to 0x00..00 without affecting
+ // higher bits
+ assertEquals(
+ new Uint8Array(ciphertext1),
+ new Uint8Array(ciphertext2).slice(16),
+ );
+ }
+ }
+});
+
+Deno.test(async function testECDH() {
+ for (const keySize of [256, 384]) {
+ const keyPair = await crypto.subtle.generateKey(
+ {
+ name: "ECDH",
+ namedCurve: "P-" + keySize,
+ },
+ true,
+ ["deriveBits"],
+ );
+
+ const derivedKey = await crypto.subtle.deriveBits(
+ {
+ name: "ECDH",
+ public: keyPair.publicKey,
+ },
+ keyPair.privateKey,
+ keySize,
+ );
+
+ assert(derivedKey instanceof ArrayBuffer);
+ assertEquals(derivedKey.byteLength, keySize / 8);
+ }
+});
+
+Deno.test(async function testWrapKey() {
+ // Test wrapKey
+ const key = await crypto.subtle.generateKey(
+ {
+ name: "RSA-OAEP",
+ modulusLength: 4096,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: "SHA-256",
+ },
+ true,
+ ["wrapKey", "unwrapKey"],
+ );
+
+ const hmacKey = await crypto.subtle.generateKey(
+ {
+ name: "HMAC",
+ hash: "SHA-256",
+ length: 128,
+ },
+ true,
+ ["sign"],
+ );
+
+ const wrappedKey = await crypto.subtle.wrapKey(
+ "raw",
+ hmacKey,
+ key.publicKey,
+ {
+ name: "RSA-OAEP",
+ label: new Uint8Array(8),
+ },
+ );
+
+ assert(wrappedKey instanceof ArrayBuffer);
+ assertEquals(wrappedKey.byteLength, 512);
+});
+
+// Doesn't need to cover all cases.
+// Only for testing types.
+Deno.test(async function testAesKeyGen() {
+ const key = await crypto.subtle.generateKey(
+ {
+ name: "AES-GCM",
+ length: 256,
+ },
+ true,
+ ["encrypt", "decrypt"],
+ );
+
+ assert(key);
+ assertEquals(key.type, "secret");
+ assertEquals(key.extractable, true);
+ assertEquals(key.usages, ["encrypt", "decrypt"]);
+ const algorithm = key.algorithm as AesKeyAlgorithm;
+ assertEquals(algorithm.name, "AES-GCM");
+ assertEquals(algorithm.length, 256);
+});
+
+Deno.test(async function testUnwrapKey() {
+ const subtle = crypto.subtle;
+
+ const AES_KEY: AesKeyAlgorithm & AesCbcParams = {
+ name: "AES-CBC",
+ length: 128,
+ iv: new Uint8Array(16),
+ };
+
+ const RSA_KEY: RsaHashedKeyGenParams & RsaOaepParams = {
+ name: "RSA-OAEP",
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: "SHA-1",
+ };
+
+ const aesKey = await subtle.generateKey(AES_KEY, true, [
+ "encrypt",
+ "decrypt",
+ ]);
+
+ const rsaKeyPair = await subtle.generateKey(
+ {
+ name: "RSA-OAEP",
+ hash: "SHA-1",
+ publicExponent: new Uint8Array([1, 0, 1]),
+ modulusLength: 2048,
+ },
+ false,
+ ["wrapKey", "encrypt", "unwrapKey", "decrypt"],
+ );
+
+ const enc = await subtle.wrapKey(
+ "raw",
+ aesKey,
+ rsaKeyPair.publicKey,
+ RSA_KEY,
+ );
+ const unwrappedKey = await subtle.unwrapKey(
+ "raw",
+ enc,
+ rsaKeyPair.privateKey,
+ RSA_KEY,
+ AES_KEY,
+ false,
+ ["encrypt", "decrypt"],
+ );
+
+ assert(unwrappedKey instanceof CryptoKey);
+ assertEquals(unwrappedKey.type, "secret");
+ assertEquals(unwrappedKey.extractable, false);
+ assertEquals(unwrappedKey.usages, ["encrypt", "decrypt"]);
+});
+
+Deno.test(async function testDecryptWithInvalidIntializationVector() {
+ // deno-fmt-ignore
+ const data = new Uint8Array([42,42,42,42,42,42,42,42,42,42,42,42,42,42,42]);
+ const key = await crypto.subtle.importKey(
+ "raw",
+ new Uint8Array(16),
+ { name: "AES-CBC", length: 256 },
+ true,
+ ["encrypt", "decrypt"],
+ );
+ // deno-fmt-ignore
+ const initVector = new Uint8Array([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]);
+ const encrypted = await crypto.subtle.encrypt(
+ { name: "AES-CBC", iv: initVector },
+ key,
+ data,
+ );
+ // deno-fmt-ignore
+ const initVector2 = new Uint8Array([15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0]);
+ await assertRejects(async () => {
+ await crypto.subtle.decrypt(
+ { name: "AES-CBC", iv: initVector2 },
+ key,
+ encrypted,
+ );
+ }, DOMException);
+});
+
+const jwtRSAKeys = {
+ "1024": {
+ size: 1024,
+ publicJWK: {
+ kty: "RSA",
+ n: "zZn4sRGfjQos56yL_Qy1R9NI-THMnFynn94g5RxA6wGrJh4BJT3x6I9x0IbpS3q-d4ORA6R2vuDMh8dDFRr9RDH6XY-gUScc9U5Jz3UA2KmVfsCbnUPvcAmMV_ENA7_TF0ivVjuIFodyDTx7EKHNVTrHHSlrbt7spbmcivs23Zc",
+ e: "AQAB",
+ },
+ privateJWK: {
+ kty: "RSA",
+ n: "zZn4sRGfjQos56yL_Qy1R9NI-THMnFynn94g5RxA6wGrJh4BJT3x6I9x0IbpS3q-d4ORA6R2vuDMh8dDFRr9RDH6XY-gUScc9U5Jz3UA2KmVfsCbnUPvcAmMV_ENA7_TF0ivVjuIFodyDTx7EKHNVTrHHSlrbt7spbmcivs23Zc",
+ e: "AQAB",
+ d: "YqIK_GdH85F-GWZdgfgmv15NE78gOaL5h2g4v7DeM9-JC7A5PHSLKNYn87HFGcC4vv0PBIBRtyCA_mJJfEaGWORVCOXSBpWNepMYpio52n3w5uj5UZEsBnbtZc0EtWhVF2Auqa7VbiKrWcQUEgEI8V0gE5D4tyBg8GXv9975dQE",
+ p: "9BrAg5L1zfqGPuWJDuDCBX-TmtZdrOI3Ys4ZaN-yMPlTjwWSEPO0qnfjEZcw2VgXHgJJmbVco6TxckJCmEYqeQ",
+ q: "157jDJ1Ya5nmQvTPbhKAPAeMWogxCyaQTkBrp30pEKd6mGSB385hqr4BIk8s3f7MdXpM-USpaZgUoT4o_2VEjw",
+ dp:
+ "qdd_QUzcaB-6jkKo1Ug-1xKIAgDLFsIjJUUfWt_iHL8ti2Kl2dOnTcCypgebPm5TT1bqHN-agGYAdK5zpX2UiQ",
+ dq:
+ "hNRfwOSplNfhLvxLUN7a2qA3yYm-1MSz_1DWQP7srlLORlUcYPht2FZmsnEeDcAqynBGPQUcbG2Av_hgHz2OZw",
+ qi:
+ "zbpJQAhinrxSbVKxBQ2EZGFUD2e3WCXbAJRYpk8HVQ5AA52OhKTicOye2hEHnrgpFKzC8iznTsCG3FMkvwcj4Q",
+ },
+ },
+
+ "2048": {
+ size: 2048,
+ publicJWK: {
+ kty: "RSA",
+ // unpadded base64 for rawKey.
+ n: "09eVwAhT9SPBxdEN-74BBeEANGaVGwqH-YglIc4VV7jfhR2by5ivzVq8NCeQ1_ACDIlTDY8CTMQ5E1c1SEXmo_T7q84XUGXf8U9mx6uRg46sV7fF-hkwJR80BFVsvWxp4ahPlVJYj__94ft7rIVvchb5tyalOjrYFCJoFnSgq-i3ZjU06csI9XnO5klINucD_Qq0vUhO23_Add2HSYoRjab8YiJJR_Eths7Pq6HHd2RSXmwYp5foRnwe0_U75XmesHWDJlJUHYbwCZo0kP9G8g4QbucwU-MSNBkZOO2x2ZtZNexpHd0ThkATbnNlpVG_z2AGNORp_Ve3rlXwrGIXXw",
+ e: "AQAB",
+ },
+ privateJWK: {
+ kty: "RSA",
+ // unpadded base64 for rawKey.
+ n: "09eVwAhT9SPBxdEN-74BBeEANGaVGwqH-YglIc4VV7jfhR2by5ivzVq8NCeQ1_ACDIlTDY8CTMQ5E1c1SEXmo_T7q84XUGXf8U9mx6uRg46sV7fF-hkwJR80BFVsvWxp4ahPlVJYj__94ft7rIVvchb5tyalOjrYFCJoFnSgq-i3ZjU06csI9XnO5klINucD_Qq0vUhO23_Add2HSYoRjab8YiJJR_Eths7Pq6HHd2RSXmwYp5foRnwe0_U75XmesHWDJlJUHYbwCZo0kP9G8g4QbucwU-MSNBkZOO2x2ZtZNexpHd0ThkATbnNlpVG_z2AGNORp_Ve3rlXwrGIXXw",
+ e: "AQAB",
+ d: "H4xboN2co0VP9kXL71G8lUOM5EDis8Q9u8uqu_4U75t4rjpamVeD1vFMVfgOehokM_m_hKVnkkcmuNqj9L90ObaiRFPM5QxG7YkFpXbHlPAKeoXD1hsqMF0VQg_2wb8DhberInHA_rEA_kaVhHvavQLu7Xez45gf1d_J4I4931vjlCB6cupbLL0H5hHsxbMsX_5nnmAJdL_U3gD-U7ZdQheUPhDBJR2KeGzvnTm3KVKpOnwn-1Cd45MU4-KDdP0FcBVEuBsSrsQHliTaciBgkbyj__BangPj3edDxTkb-fKkEvhkXRjAoJs1ixt8nfSGDce9cM_GqAX9XGb4s2QkAQ",
+ dp:
+ "mM82RBwzGzi9LAqjGbi-badLtHRRBoH9sfMrJuOtzxRnmwBFccg_lwy-qAhUTqnN9kvD0H1FzXWzoFPFJbyi-AOmumYGpWm_PvzQGldne5CPJ02pYaeg-t1BePsT3OpIq0Am8E2Kjf9polpRJwIjO7Kx8UJKkhg5bISnsy0V8wE",
+ dq:
+ "ZlM4AvrWIpXwqsH_5Q-6BsLJdbnN_GypFCXoT9VXniXncSBZIWCkgDndBdWkSzyzIN65NiMRBfZaf9yduTFj4kvOPwb3ch3J0OxGJk0Ary4OGSlS1zNwMl93ALGal1FzpWUuiia9L9RraGqXAUr13L7TIIMRobRjpAV-z7M-ruM",
+ p: "7VwGt_tJcAFQHrmDw5dM1EBru6fidM45NDv6VVOEbxKuD5Sh2EfAHfm5c6oouA1gZqwvKH0sn_XpB1NsyYyHEQd3sBVdK0zRjTo-E9mRP-1s-LMd5YDXVq6HE339nxpXsmO25slQEF6zBrj1bSNNXBFc7fgDnlq-HIeleMvsY_E",
+ q: "5HqMHLzb4IgXhUl4pLz7E4kjY8PH2YGzaQfK805zJMbOXzmlZK0hizKo34Qqd2nB9xos7QgzOYQrNfSWheARwVsSQzAE0vGvw3zHIPP_lTtChBlCTPctQcURjw4dXcnK1oQ-IT321FNOW3EO-YTsyGcypJqJujlZrLbxYjOjQE8",
+ qi:
+ "OQXzi9gypDnpdHatIi0FaUGP8LSzfVH0AUugURJXs4BTJpvA9y4hcpBQLrcl7H_vq6kbGmvC49V-9I5HNVX_AuxGIXKuLZr5WOxPq8gLTqHV7X5ZJDtWIP_nq2NNgCQQyNNRrxebiWlwGK9GnX_unewT6jopI_oFhwp0Q13rBR0",
+ },
+ },
+ "4096": {
+ size: 4096,
+ publicJWK: {
+ kty: "RSA",
+ n: "2qr2TL2c2JmbsN0OLIRnaAB_ZKb1-Gh9H0qb4lrBuDaqkW_eFPwT-JIsvnNJvDT7BLJ57tTMIj56ZMtv6efSSTWSk9MOoW2J1K_iEretZ2cegB_aRX7qQVjnoFsz9U02BKfAIUT0o_K7b9G08d1rrAUohi_SVQhwObodg7BddMbKUmz70QNIS487LN44WUVnn9OgE9atTYUARNukT0DuQb3J-K20ksTuVujXbSelohDmLobqlGoi5sY_548Qs9BtFmQ2nGuEHNB2zdlZ5EvEqbUFVZ2QboG6jXdoos6qcwdgUvAhj1Hz10Ngic_RFqL7bNDoIOzNp66hdA35uxbwuaygZ16ikxoPj7eTYud1hrkyQCgeGw2YhCiKIE6eos_U5dL7WHRD5aSkkzsgXtnF8pVmStsuf0QcdAoC-eeCex0tSTgRw9AtGTz8Yr1tGQD9l_580zAXnE6jmrwRRQ68EEA7vohGov3tnG8pGyg_zcxeADLtPlfTc1tEwmh3SGrioDClioYCipm1JvkweEgP9eMPpEC8SgRU1VNDSVe1SF4uNsH8vA7PHFKfg6juqJEc5ht-l10FYER-Qq6bZXsU2oNcfE5SLDeLTWmxiHmxK00M8ABMFIV5gUkPoMiWcl87O6XwzA2chsIERp7Vb-Vn2O-EELiXzv7lPhc6fTGQ0Nc",
+ e: "AQAB",
+ },
+ privateJWK: {
+ kty: "RSA",
+ n: "2qr2TL2c2JmbsN0OLIRnaAB_ZKb1-Gh9H0qb4lrBuDaqkW_eFPwT-JIsvnNJvDT7BLJ57tTMIj56ZMtv6efSSTWSk9MOoW2J1K_iEretZ2cegB_aRX7qQVjnoFsz9U02BKfAIUT0o_K7b9G08d1rrAUohi_SVQhwObodg7BddMbKUmz70QNIS487LN44WUVnn9OgE9atTYUARNukT0DuQb3J-K20ksTuVujXbSelohDmLobqlGoi5sY_548Qs9BtFmQ2nGuEHNB2zdlZ5EvEqbUFVZ2QboG6jXdoos6qcwdgUvAhj1Hz10Ngic_RFqL7bNDoIOzNp66hdA35uxbwuaygZ16ikxoPj7eTYud1hrkyQCgeGw2YhCiKIE6eos_U5dL7WHRD5aSkkzsgXtnF8pVmStsuf0QcdAoC-eeCex0tSTgRw9AtGTz8Yr1tGQD9l_580zAXnE6jmrwRRQ68EEA7vohGov3tnG8pGyg_zcxeADLtPlfTc1tEwmh3SGrioDClioYCipm1JvkweEgP9eMPpEC8SgRU1VNDSVe1SF4uNsH8vA7PHFKfg6juqJEc5ht-l10FYER-Qq6bZXsU2oNcfE5SLDeLTWmxiHmxK00M8ABMFIV5gUkPoMiWcl87O6XwzA2chsIERp7Vb-Vn2O-EELiXzv7lPhc6fTGQ0Nc",
+ e: "AQAB",
+ d: "uXPRXBhcE5-DWabBRKQuhxgU8ype5gTISWefeYP7U96ZHqu_sBByZ5ihdgyU9pgAZGVx4Ep9rnVKnH2lNr2zrP9Qhyqy99nM0aMxmypIWLAuP__DwLj4t99M4sU29c48CAq1egHfccSFjzpNuetOTCA71EJuokt70pm0OmGzgTyvjuR7VTLxd5PMXitBowSn8_cphmnFpT8tkTiuy8CH0R3DU7MOuINomDD1s8-yPBcVAVTPUnwJiauNuzestLQKMLlhT5wn-cAbYk36XRKdgkjSc2AkhHRl4WDqT1nzWYdh_DVIYSLiKSktkPO9ovMrRYiPtozfhl0m9SR9Ll0wXtcnnDlWXc_MSGpw18vmUBSJ4PIhkiFsvLn-db3wUkA8uve-iqqfk0sxlGWughWx03kGmZDmprWbXugCBHfsI4X93w4exznXH_tapxPnmjbhVUQR6p41MvO2lcHWPLwGJgLIoejBHpnn3TmMN0UjFZki7q9B_dJ3fXh0mX9DzAlC0sil1NgCPhMPq02393_giinQquMknrBvgKxGSfGUrDKuflCx611ZZlRM3R7YMX2OIy1g4DyhPzBVjxRMtm8PnIs3m3Hi-O-C_PHF93w9J8Wqd0yIw7SpavDqZXLPC6Cqi8K7MBZyVECXHtRj1bBqT-h_xZmFCDjSU0NqfOdgApE",
+ p: "9NrXwq4kY9kBBOwLoFZVQc4kJI_NbKa_W9FLdQdRIbMsZZHXJ3XDUR9vJAcaaR75WwIC7X6N55nVtWTq28Bys9flJ9RrCTfciOntHEphBhYaL5ZTUl-6khYmsOf_psff2VaOOCvHGff5ejuOmBQxkw2E-cv7knRgWFHoLWpku2NJIMuGHt9ks7OAUfIZVYl9YJnw4FYUzhgaxemknjLeZ8XTkGW2zckzF-d95YI9i8zD80Umubsw-YxriSfqFQ0rGHBsbQ8ZOTd_KJju42BWnXIjNDYmjFUqdzVjI4XQ8EGrCEf_8_iwphGyXD7LOJ4fqd97B3bYpoRTPnCgY_SEHQ",
+ q: "5J758_NeKr1XPZiLxXohYQQnh0Lb4QtGZ1xzCgjhBQLcIBeTOG_tYjCues9tmLt93LpJfypSJ-SjDLwkR2s069_IByYGpxyeGtV-ulqYhSw1nD2CXKMDGyO5jXDs9tJrS_UhfobXKQH03CRdFugyPkSNmXY-AafFynG7xLr7oYBC05FnhUXPm3VBTPt9K-BpqwYd_h9vkAWeprSPo83UlwcLMupSJY9LaHxhRdz2yi0ZKNwXXHRwcszGjDBvvzUcCYbqWqjzbEvFY6KtH8Jh4LhM46rHaoEOTernJsDF6a6W8Df88RthqTExcwnaQf0O_dlbjSxEIPfbxx8t1EQugw",
+ dp:
+ "4Y7Hu5tYAnLhMXuQqj9dgqU3PkcKYdCp7xc6f7Ah2P2JJHfYz4z4RD7Ez1eLyNKzulZ8A_PVHUjlSZiRkaYTBAEaJDrV70P6cFWuC6WpA0ZREQ1V7EgrQnANbGILa8QsPbYyhSQu4YlB1IwQq5_OmzyVBtgWA7AZIMMzMsMT0FuB_if-gWohBjmRN-vh0p45VUf6UW568-_YmgDFmMYbg1UFs7s_TwrNenPR0h7MO4CB8hP9vJLoZrooRczzIjljPbwy5bRG9CJfjTJ0vhj9MUT3kR1hHV1HJVGU5iBbfTfBKnvJGSI6-IDM4ZUm-B0R5hbs6s9cfOjhFmACIJIbMQ",
+ dq:
+ "gT4iPbfyHyVEwWyQb4X4grjvg7bXSKSwG1SXMDAOzV9tg7LwJjKYNy8gJAtJgNNVdsfVLs-E_Epzpoph1AIWO9YZZXkov6Yc9zyEVONMX9S7ReU74hTBd8E9b2lMfMg9ogYk9jtSPTt-6kigW4fOh4cHqZ6_tP3cgfLD3JZ8FDPHE4WaySvLDq49yUBO5dQKyIU_xV6OGhQjOUjP_yEoMmzn9tOittsIHTxbXTxqQ6c1FvU9O6YTv8Jl5_Cl66khfX1I1RG38xvurcHULyUbYgeuZ_Iuo9XreT73h9_owo9RguGT29XH4vcNZmRGf5GIvRb4e5lvtleIZkwJA3u78w",
+ qi:
+ "JHmVKb1zwW5iRR6RCeexYnh2fmY-3DrPSdM8Dxhr0F8dayi-tlRqEdnG0hvp45n8gLUskWWcB9EXlUJObZGKDfGuxgMa3g_xeLA2vmFQ12MxPsyH4iCNZvsgmGxx7TuOHrnDh5EBVnM4_de63crEJON2sYI8Ozi-xp2OEmAr2seWKq4sxkFni6exLhqb-NE4m9HMKlng1EtQh2rLBFG1VYD3SYYpMLc5fxzqGvSxn3Fa-Xgg-IZPY3ubrcm52KYgmLUGmnYStfVqGSWSdhDXHlNgI5pdAA0FzpyBk3ZX-JsxhwcnneKrYBBweq06kRMGWgvdbdAQ-7wSeGqqj5VPwA",
+ },
+ },
+};
+
+Deno.test(async function testImportRsaJwk() {
+ const subtle = window.crypto.subtle;
+ assert(subtle);
+
+ for (const [_key, jwkData] of Object.entries(jwtRSAKeys)) {
+ const { size, publicJWK, privateJWK } = jwkData;
+ if (size < 2048) {
+ continue;
+ }
+
+ // 1. Test import PSS
+ for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) {
+ const hashMapPSS: Record<string, string> = {
+ "SHA-1": "PS1",
+ "SHA-256": "PS256",
+ "SHA-384": "PS384",
+ "SHA-512": "PS512",
+ };
+
+ if (size == 1024 && hash == "SHA-512") {
+ continue;
+ }
+
+ const privateKeyPSS = await crypto.subtle.importKey(
+ "jwk",
+ {
+ alg: hashMapPSS[hash],
+ ...privateJWK,
+ ext: true,
+ "key_ops": ["sign"],
+ },
+ { name: "RSA-PSS", hash },
+ true,
+ ["sign"],
+ );
+
+ const publicKeyPSS = await crypto.subtle.importKey(
+ "jwk",
+ {
+ alg: hashMapPSS[hash],
+ ...publicJWK,
+ ext: true,
+ "key_ops": ["verify"],
+ },
+ { name: "RSA-PSS", hash },
+ true,
+ ["verify"],
+ );
+
+ const signaturePSS = await crypto.subtle.sign(
+ { name: "RSA-PSS", saltLength: 32 },
+ privateKeyPSS,
+ new Uint8Array([1, 2, 3, 4]),
+ );
+
+ const verifyPSS = await crypto.subtle.verify(
+ { name: "RSA-PSS", saltLength: 32 },
+ publicKeyPSS,
+ signaturePSS,
+ new Uint8Array([1, 2, 3, 4]),
+ );
+ assert(verifyPSS);
+ }
+
+ // 2. Test import PKCS1
+ for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) {
+ const hashMapPKCS1: Record<string, string> = {
+ "SHA-1": "RS1",
+ "SHA-256": "RS256",
+ "SHA-384": "RS384",
+ "SHA-512": "RS512",
+ };
+
+ if (size == 1024 && hash == "SHA-512") {
+ continue;
+ }
+
+ const privateKeyPKCS1 = await crypto.subtle.importKey(
+ "jwk",
+ {
+ alg: hashMapPKCS1[hash],
+ ...privateJWK,
+ ext: true,
+ "key_ops": ["sign"],
+ },
+ { name: "RSASSA-PKCS1-v1_5", hash },
+ true,
+ ["sign"],
+ );
+
+ const publicKeyPKCS1 = await crypto.subtle.importKey(
+ "jwk",
+ {
+ alg: hashMapPKCS1[hash],
+ ...publicJWK,
+ ext: true,
+ "key_ops": ["verify"],
+ },
+ { name: "RSASSA-PKCS1-v1_5", hash },
+ true,
+ ["verify"],
+ );
+
+ const signaturePKCS1 = await crypto.subtle.sign(
+ { name: "RSASSA-PKCS1-v1_5", saltLength: 32 },
+ privateKeyPKCS1,
+ new Uint8Array([1, 2, 3, 4]),
+ );
+
+ const verifyPKCS1 = await crypto.subtle.verify(
+ { name: "RSASSA-PKCS1-v1_5", saltLength: 32 },
+ publicKeyPKCS1,
+ signaturePKCS1,
+ new Uint8Array([1, 2, 3, 4]),
+ );
+ assert(verifyPKCS1);
+ }
+
+ // 3. Test import OAEP
+ for (
+ const { hash, plainText } of hashPlainTextVector
+ ) {
+ const hashMapOAEP: Record<string, string> = {
+ "SHA-1": "RSA-OAEP",
+ "SHA-256": "RSA-OAEP-256",
+ "SHA-384": "RSA-OAEP-384",
+ "SHA-512": "RSA-OAEP-512",
+ };
+
+ if (size == 1024 && hash == "SHA-512") {
+ continue;
+ }
+
+ const encryptAlgorithm = { name: "RSA-OAEP" };
+
+ const privateKeyOAEP = await crypto.subtle.importKey(
+ "jwk",
+ {
+ alg: hashMapOAEP[hash],
+ ...privateJWK,
+ ext: true,
+ "key_ops": ["decrypt"],
+ },
+ { ...encryptAlgorithm, hash },
+ true,
+ ["decrypt"],
+ );
+
+ const publicKeyOAEP = await crypto.subtle.importKey(
+ "jwk",
+ {
+ alg: hashMapOAEP[hash],
+ ...publicJWK,
+ ext: true,
+ "key_ops": ["encrypt"],
+ },
+ { ...encryptAlgorithm, hash },
+ true,
+ ["encrypt"],
+ );
+ const cipherText = await subtle.encrypt(
+ encryptAlgorithm,
+ publicKeyOAEP,
+ plainText,
+ );
+
+ assert(cipherText);
+ assert(cipherText.byteLength > 0);
+ assertEquals(cipherText.byteLength * 8, size);
+ assert(cipherText instanceof ArrayBuffer);
+
+ const decrypted = await subtle.decrypt(
+ encryptAlgorithm,
+ privateKeyOAEP,
+ cipherText,
+ );
+ assert(decrypted);
+ assert(decrypted instanceof ArrayBuffer);
+ assertEquals(new Uint8Array(decrypted), plainText);
+ }
+ }
+});
+
+const jwtECKeys = {
+ "256": {
+ size: 256,
+ algo: "ES256",
+ publicJWK: {
+ kty: "EC",
+ crv: "P-256",
+ x: "0hCwpvnZ8BKGgFi0P6T0cQGFQ7ugDJJQ35JXwqyuXdE",
+ y: "zgN1UtSBRQzjm00QlXAbF1v6s0uObAmeGPHBmDWDYeg",
+ },
+ privateJWK: {
+ kty: "EC",
+ crv: "P-256",
+ x: "0hCwpvnZ8BKGgFi0P6T0cQGFQ7ugDJJQ35JXwqyuXdE",
+ y: "zgN1UtSBRQzjm00QlXAbF1v6s0uObAmeGPHBmDWDYeg",
+ d: "E9M6LVq_nPnrsh_4YNSu_m5W53eQ9N7ptAiE69M1ROo",
+ },
+ },
+ "384": {
+ size: 384,
+ algo: "ES384",
+ publicJWK: {
+ kty: "EC",
+ crv: "P-384",
+ x: "IZwU1mYXs27G2IVrOFtzp000T9iude8EZDXdpU47RL1fvevR0I3Wni19wdwhjLQ1",
+ y: "vSgTjMd4M3qEL2vWGyQOdCSfJGZ8KlgQp2v8KOAzX4imUB3sAZdtqFr7AIactqzo",
+ },
+ privateJWK: {
+ kty: "EC",
+ crv: "P-384",
+ x: "IZwU1mYXs27G2IVrOFtzp000T9iude8EZDXdpU47RL1fvevR0I3Wni19wdwhjLQ1",
+ y: "vSgTjMd4M3qEL2vWGyQOdCSfJGZ8KlgQp2v8KOAzX4imUB3sAZdtqFr7AIactqzo",
+ d: "RTe1mQeE08LSLpao-S-hqkku6HPldqQVguFEGDyYiNEOa560ztSyzEAS5KxeqEBz",
+ },
+ },
+};
+
+type JWK = Record<string, string>;
+
+function equalJwk(expected: JWK, got: JWK): boolean {
+ const fields = Object.keys(expected);
+
+ for (let i = 0; i < fields.length; i++) {
+ const fieldName = fields[i];
+
+ if (!(fieldName in got)) {
+ return false;
+ }
+ if (expected[fieldName] !== got[fieldName]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+Deno.test(async function testImportExportEcDsaJwk() {
+ const subtle = crypto.subtle;
+ assert(subtle);
+
+ for (
+ const [_key, keyData] of Object.entries(jwtECKeys)
+ ) {
+ const { publicJWK, privateJWK, algo } = keyData;
+
+ // 1. Test import EcDsa
+ const privateKeyECDSA = await subtle.importKey(
+ "jwk",
+ {
+ alg: algo,
+ ...privateJWK,
+ ext: true,
+ "key_ops": ["sign"],
+ },
+ { name: "ECDSA", namedCurve: privateJWK.crv },
+ true,
+ ["sign"],
+ );
+ const expPrivateKeyJWK = await subtle.exportKey(
+ "jwk",
+ privateKeyECDSA,
+ );
+ assert(equalJwk(privateJWK, expPrivateKeyJWK as JWK));
+
+ const publicKeyECDSA = await subtle.importKey(
+ "jwk",
+ {
+ alg: algo,
+ ...publicJWK,
+ ext: true,
+ "key_ops": ["verify"],
+ },
+ { name: "ECDSA", namedCurve: publicJWK.crv },
+ true,
+ ["verify"],
+ );
+
+ const expPublicKeyJWK = await subtle.exportKey(
+ "jwk",
+ publicKeyECDSA,
+ );
+
+ assert(equalJwk(publicJWK, expPublicKeyJWK as JWK));
+
+ const signatureECDSA = await subtle.sign(
+ { name: "ECDSA", hash: `SHA-${keyData.size}` },
+ privateKeyECDSA,
+ new Uint8Array([1, 2, 3, 4]),
+ );
+
+ const verifyECDSA = await subtle.verify(
+ { name: "ECDSA", hash: `SHA-${keyData.size}` },
+ publicKeyECDSA,
+ signatureECDSA,
+ new Uint8Array([1, 2, 3, 4]),
+ );
+ assert(verifyECDSA);
+ }
+});
+
+Deno.test(async function testImportEcDhJwk() {
+ const subtle = crypto.subtle;
+ assert(subtle);
+
+ for (
+ const [_key, jwkData] of Object.entries(jwtECKeys)
+ ) {
+ const { size, publicJWK, privateJWK } = jwkData;
+
+ // 1. Test import EcDsa
+ const privateKeyECDH = await subtle.importKey(
+ "jwk",
+ {
+ ...privateJWK,
+ ext: true,
+ "key_ops": ["deriveBits"],
+ },
+ { name: "ECDH", namedCurve: privateJWK.crv },
+ true,
+ ["deriveBits"],
+ );
+
+ const expPrivateKeyJWK = await subtle.exportKey(
+ "jwk",
+ privateKeyECDH,
+ );
+ assert(equalJwk(privateJWK, expPrivateKeyJWK as JWK));
+
+ const publicKeyECDH = await subtle.importKey(
+ "jwk",
+ {
+ ...publicJWK,
+ ext: true,
+ "key_ops": [],
+ },
+ { name: "ECDH", namedCurve: publicJWK.crv },
+ true,
+ [],
+ );
+ const expPublicKeyJWK = await subtle.exportKey(
+ "jwk",
+ publicKeyECDH,
+ );
+ assert(equalJwk(publicJWK, expPublicKeyJWK as JWK));
+
+ const derivedKey = await subtle.deriveBits(
+ {
+ name: "ECDH",
+ public: publicKeyECDH,
+ },
+ privateKeyECDH,
+ size,
+ );
+
+ assert(derivedKey instanceof ArrayBuffer);
+ assertEquals(derivedKey.byteLength, size / 8);
+ }
+});
+
+const ecTestKeys = [
+ {
+ size: 256,
+ namedCurve: "P-256",
+ signatureLength: 64,
+ // deno-fmt-ignore
+ raw: new Uint8Array([
+ 4, 210, 16, 176, 166, 249, 217, 240, 18, 134, 128, 88, 180, 63, 164, 244,
+ 113, 1, 133, 67, 187, 160, 12, 146, 80, 223, 146, 87, 194, 172, 174, 93,
+ 209, 206, 3, 117, 82, 212, 129, 69, 12, 227, 155, 77, 16, 149, 112, 27,
+ 23, 91, 250, 179, 75, 142, 108, 9, 158, 24, 241, 193, 152, 53, 131, 97,
+ 232,
+ ]),
+ // deno-fmt-ignore
+ spki: new Uint8Array([
+ 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206,
+ 61, 3, 1, 7, 3, 66, 0, 4, 210, 16, 176, 166, 249, 217, 240, 18, 134, 128,
+ 88, 180, 63, 164, 244, 113, 1, 133, 67, 187, 160, 12, 146, 80, 223, 146,
+ 87, 194, 172, 174, 93, 209, 206, 3, 117, 82, 212, 129, 69, 12, 227, 155,
+ 77, 16, 149, 112, 27, 23, 91, 250, 179, 75, 142, 108, 9, 158, 24, 241,
+ 193, 152, 53, 131, 97, 232,
+ ]),
+ // deno-fmt-ignore
+ pkcs8: new Uint8Array([
+ 48, 129, 135, 2, 1, 0, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42,
+ 134, 72, 206, 61, 3, 1, 7, 4, 109, 48, 107, 2, 1, 1, 4, 32, 19, 211, 58,
+ 45, 90, 191, 156, 249, 235, 178, 31, 248, 96, 212, 174, 254, 110, 86, 231,
+ 119, 144, 244, 222, 233, 180, 8, 132, 235, 211, 53, 68, 234, 161, 68, 3,
+ 66, 0, 4, 210, 16, 176, 166, 249, 217, 240, 18, 134, 128, 88, 180, 63,
+ 164, 244, 113, 1, 133, 67, 187, 160, 12, 146, 80, 223, 146, 87, 194, 172,
+ 174, 93, 209, 206, 3, 117, 82, 212, 129, 69, 12, 227, 155, 77, 16, 149,
+ 112, 27, 23, 91, 250, 179, 75, 142, 108, 9, 158, 24, 241, 193, 152, 53,
+ 131, 97, 232,
+ ]),
+ },
+ {
+ size: 384,
+ namedCurve: "P-384",
+ signatureLength: 96,
+ // deno-fmt-ignore
+ raw: new Uint8Array([
+ 4, 118, 64, 176, 165, 100, 177, 112, 49, 254, 58, 53, 158, 63, 73, 200,
+ 148, 248, 242, 216, 186, 80, 92, 160, 53, 64, 232, 157, 19, 1, 12, 226,
+ 115, 51, 42, 143, 98, 206, 55, 220, 108, 78, 24, 71, 157, 21, 120, 126,
+ 104, 157, 86, 48, 226, 110, 96, 52, 48, 77, 170, 9, 231, 159, 26, 165,
+ 200, 26, 164, 99, 46, 227, 169, 105, 172, 225, 60, 102, 141, 145, 139,
+ 165, 47, 72, 53, 17, 17, 246, 161, 220, 26, 21, 23, 219, 1, 107, 185,
+ 163, 215,
+ ]),
+ // deno-fmt-ignore
+ spki: new Uint8Array([
+ 48, 118, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0,
+ 34, 3, 98, 0, 4, 118, 64, 176, 165, 100, 177, 112, 49, 254, 58, 53, 158,
+ 63, 73, 200, 148, 248, 242, 216, 186, 80, 92, 160, 53, 64, 232, 157, 19,
+ 1, 12, 226, 115, 51, 42, 143, 98, 206, 55, 220, 108, 78, 24, 71, 157, 21,
+ 120, 126, 104, 157, 86, 48, 226, 110, 96, 52, 48, 77, 170, 9, 231, 159,
+ 26, 165, 200, 26, 164, 99, 46, 227, 169, 105, 172, 225, 60, 102, 141,
+ 145, 139, 165, 47, 72, 53, 17, 17, 246, 161, 220, 26, 21, 23, 219, 1,
+ 107, 185, 163, 215,
+ ]),
+ // deno-fmt-ignore
+ pkcs8: new Uint8Array([
+ 48, 129, 182, 2, 1, 0, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43,
+ 129, 4, 0, 34, 4, 129, 158, 48, 129, 155, 2, 1, 1, 4, 48, 202, 7, 195,
+ 169, 124, 170, 81, 169, 253, 127, 56, 28, 98, 90, 255, 165, 72, 142, 133,
+ 138, 237, 200, 176, 92, 179, 192, 83, 28, 47, 118, 157, 152, 47, 65, 133,
+ 140, 50, 83, 182, 191, 224, 96, 216, 179, 59, 150, 15, 233, 161, 100, 3,
+ 98, 0, 4, 118, 64, 176, 165, 100, 177, 112, 49, 254, 58, 53, 158, 63, 73,
+ 200, 148, 248, 242, 216, 186, 80, 92, 160, 53, 64, 232, 157, 19, 1, 12,
+ 226, 115, 51, 42, 143, 98, 206, 55, 220, 108, 78, 24, 71, 157, 21, 120,
+ 126, 104, 157, 86, 48, 226, 110, 96, 52, 48, 77, 170, 9, 231, 159, 26,
+ 165, 200, 26, 164, 99, 46, 227, 169, 105, 172, 225, 60, 102, 141, 145,
+ 139, 165, 47, 72, 53, 17, 17, 246, 161, 220, 26, 21, 23, 219, 1, 107,
+ 185, 163, 215,
+ ]),
+ },
+];
+
+Deno.test(async function testImportEcSpkiPkcs8() {
+ const subtle = window.crypto.subtle;
+ assert(subtle);
+
+ for (
+ const { namedCurve, raw, spki, pkcs8, signatureLength } of ecTestKeys
+ ) {
+ const rawPublicKeyECDSA = await subtle.importKey(
+ "raw",
+ raw,
+ { name: "ECDSA", namedCurve },
+ true,
+ ["verify"],
+ );
+
+ const expPublicKeyRaw = await subtle.exportKey(
+ "raw",
+ rawPublicKeyECDSA,
+ );
+
+ assertEquals(new Uint8Array(expPublicKeyRaw), raw);
+
+ const privateKeyECDSA = await subtle.importKey(
+ "pkcs8",
+ pkcs8,
+ { name: "ECDSA", namedCurve },
+ true,
+ ["sign"],
+ );
+
+ const expPrivateKeyPKCS8 = await subtle.exportKey(
+ "pkcs8",
+ privateKeyECDSA,
+ );
+
+ assertEquals(new Uint8Array(expPrivateKeyPKCS8), pkcs8);
+
+ const expPrivateKeyJWK = await subtle.exportKey(
+ "jwk",
+ privateKeyECDSA,
+ );
+
+ assertEquals(expPrivateKeyJWK.crv, namedCurve);
+
+ const publicKeyECDSA = await subtle.importKey(
+ "spki",
+ spki,
+ { name: "ECDSA", namedCurve },
+ true,
+ ["verify"],
+ );
+
+ const expPublicKeySPKI = await subtle.exportKey(
+ "spki",
+ publicKeyECDSA,
+ );
+
+ assertEquals(new Uint8Array(expPublicKeySPKI), spki);
+
+ const expPublicKeyJWK = await subtle.exportKey(
+ "jwk",
+ publicKeyECDSA,
+ );
+
+ assertEquals(expPublicKeyJWK.crv, namedCurve);
+
+ for (
+ const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]
+ ) {
+ if (
+ (hash == "SHA-256" && namedCurve == "P-256") ||
+ (hash == "SHA-384" && namedCurve == "P-384")
+ ) {
+ const signatureECDSA = await subtle.sign(
+ { name: "ECDSA", hash },
+ privateKeyECDSA,
+ new Uint8Array([1, 2, 3, 4]),
+ );
+
+ const verifyECDSA = await subtle.verify(
+ { name: "ECDSA", hash },
+ publicKeyECDSA,
+ signatureECDSA,
+ new Uint8Array([1, 2, 3, 4]),
+ );
+ assert(verifyECDSA);
+ } else {
+ await assertRejects(
+ async () => {
+ await subtle.sign(
+ { name: "ECDSA", hash },
+ privateKeyECDSA,
+ new Uint8Array([1, 2, 3, 4]),
+ );
+ },
+ DOMException,
+ "Not implemented",
+ );
+ await assertRejects(
+ async () => {
+ await subtle.verify(
+ { name: "ECDSA", hash },
+ publicKeyECDSA,
+ new Uint8Array(signatureLength),
+ new Uint8Array([1, 2, 3, 4]),
+ );
+ },
+ DOMException,
+ "Not implemented",
+ );
+ }
+ }
+ }
+});
+
+Deno.test(async function testAesGcmEncrypt() {
+ const key = await crypto.subtle.importKey(
+ "raw",
+ new Uint8Array(16),
+ { name: "AES-GCM", length: 256 },
+ true,
+ ["encrypt", "decrypt"],
+ );
+
+ const nonces = [{
+ iv: new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
+ ciphertext: new Uint8Array([
+ 50,
+ 223,
+ 112,
+ 178,
+ 166,
+ 156,
+ 255,
+ 110,
+ 125,
+ 138,
+ 95,
+ 141,
+ 82,
+ 47,
+ 14,
+ 164,
+ 134,
+ 247,
+ 22,
+ ]),
+ }, {
+ iv: new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
+ ciphertext: new Uint8Array([
+ 210,
+ 101,
+ 81,
+ 216,
+ 151,
+ 9,
+ 192,
+ 197,
+ 62,
+ 254,
+ 28,
+ 132,
+ 89,
+ 106,
+ 40,
+ 29,
+ 175,
+ 232,
+ 201,
+ ]),
+ }];
+ for (const { iv, ciphertext: fixture } of nonces) {
+ const data = new Uint8Array([1, 2, 3]);
+
+ const cipherText = await crypto.subtle.encrypt(
+ { name: "AES-GCM", iv },
+ key,
+ data,
+ );
+
+ assert(cipherText instanceof ArrayBuffer);
+ assertEquals(cipherText.byteLength, 19);
+ assertEquals(
+ new Uint8Array(cipherText),
+ fixture,
+ );
+
+ const plainText = await crypto.subtle.decrypt(
+ { name: "AES-GCM", iv },
+ key,
+ cipherText,
+ );
+ assert(plainText instanceof ArrayBuffer);
+ assertEquals(plainText.byteLength, 3);
+ assertEquals(new Uint8Array(plainText), data);
+ }
+});
+
+async function roundTripSecretJwk(
+ jwk: JsonWebKey,
+ algId: AlgorithmIdentifier | HmacImportParams,
+ ops: KeyUsage[],
+ validateKeys: (
+ key: CryptoKey,
+ originalJwk: JsonWebKey,
+ exportedJwk: JsonWebKey,
+ ) => void,
+) {
+ const key = await crypto.subtle.importKey(
+ "jwk",
+ jwk,
+ algId,
+ true,
+ ops,
+ );
+
+ assert(key instanceof CryptoKey);
+ assertEquals(key.type, "secret");
+
+ const exportedKey = await crypto.subtle.exportKey("jwk", key);
+
+ validateKeys(key, jwk, exportedKey);
+}
+
+Deno.test(async function testSecretJwkBase64Url() {
+ // Test 16bits with "overflow" in 3rd pos of 'quartet', no padding
+ const keyData = `{
+ "kty": "oct",
+ "k": "xxx",
+ "alg": "HS512",
+ "key_ops": ["sign", "verify"],
+ "ext": true
+ }`;
+
+ await roundTripSecretJwk(
+ JSON.parse(keyData),
+ { name: "HMAC", hash: "SHA-512" },
+ ["sign", "verify"],
+ (key, _orig, exp) => {
+ assertEquals((key.algorithm as HmacKeyAlgorithm).length, 16);
+
+ assertEquals(exp.k, "xxw");
+ },
+ );
+
+ // HMAC 128bits with base64url characters (-_)
+ await roundTripSecretJwk(
+ {
+ kty: "oct",
+ k: "HnZXRyDKn-_G5Fx4JWR1YA",
+ alg: "HS256",
+ "key_ops": ["sign", "verify"],
+ ext: true,
+ },
+ { name: "HMAC", hash: "SHA-256" },
+ ["sign", "verify"],
+ (key, orig, exp) => {
+ assertEquals((key.algorithm as HmacKeyAlgorithm).length, 128);
+
+ assertEquals(orig.k, exp.k);
+ },
+ );
+
+ // HMAC 104bits/(12+1) bytes with base64url characters (-_), padding and overflow in 2rd pos of "quartet"
+ await roundTripSecretJwk(
+ {
+ kty: "oct",
+ k: "a-_AlFa-2-OmEGa_-z==",
+ alg: "HS384",
+ "key_ops": ["sign", "verify"],
+ ext: true,
+ },
+ { name: "HMAC", hash: "SHA-384" },
+ ["sign", "verify"],
+ (key, _orig, exp) => {
+ assertEquals((key.algorithm as HmacKeyAlgorithm).length, 104);
+
+ assertEquals("a-_AlFa-2-OmEGa_-w", exp.k);
+ },
+ );
+
+ // AES-CBC 128bits with base64url characters (-_) no padding
+ await roundTripSecretJwk(
+ {
+ kty: "oct",
+ k: "_u3K_gEjRWf-7cr-ASNFZw",
+ alg: "A128CBC",
+ "key_ops": ["encrypt", "decrypt"],
+ ext: true,
+ },
+ { name: "AES-CBC" },
+ ["encrypt", "decrypt"],
+ (_key, orig, exp) => {
+ assertEquals(orig.k, exp.k);
+ },
+ );
+
+ // AES-CBC 128bits of '1' with padding chars
+ await roundTripSecretJwk(
+ {
+ kty: "oct",
+ k: "_____________________w==",
+ alg: "A128CBC",
+ "key_ops": ["encrypt", "decrypt"],
+ ext: true,
+ },
+ { name: "AES-CBC" },
+ ["encrypt", "decrypt"],
+ (_key, _orig, exp) => {
+ assertEquals(exp.k, "_____________________w");
+ },
+ );
+});
+
+Deno.test(async function testAESWrapKey() {
+ const key = await crypto.subtle.generateKey(
+ {
+ name: "AES-KW",
+ length: 128,
+ },
+ true,
+ ["wrapKey", "unwrapKey"],
+ );
+
+ const hmacKey = await crypto.subtle.generateKey(
+ {
+ name: "HMAC",
+ hash: "SHA-256",
+ length: 128,
+ },
+ true,
+ ["sign"],
+ );
+
+ //round-trip
+ // wrap-unwrap-export compare
+ const wrappedKey = await crypto.subtle.wrapKey(
+ "raw",
+ hmacKey,
+ key,
+ {
+ name: "AES-KW",
+ },
+ );
+
+ assert(wrappedKey instanceof ArrayBuffer);
+ assertEquals(wrappedKey.byteLength, 16 + 8); // 8 = 'auth tag'
+
+ const unwrappedKey = await crypto.subtle.unwrapKey(
+ "raw",
+ wrappedKey,
+ key,
+ {
+ name: "AES-KW",
+ },
+ {
+ name: "HMAC",
+ hash: "SHA-256",
+ },
+ true,
+ ["sign"],
+ );
+
+ assert(unwrappedKey instanceof CryptoKey);
+ assertEquals((unwrappedKey.algorithm as HmacKeyAlgorithm).length, 128);
+
+ const hmacKeyBytes = await crypto.subtle.exportKey("raw", hmacKey);
+ const unwrappedKeyBytes = await crypto.subtle.exportKey("raw", unwrappedKey);
+
+ assertEquals(new Uint8Array(hmacKeyBytes), new Uint8Array(unwrappedKeyBytes));
+});
+
+// https://github.com/denoland/deno/issues/13534
+Deno.test(async function testAesGcmTagLength() {
+ const key = await crypto.subtle.importKey(
+ "raw",
+ new Uint8Array(32),
+ "AES-GCM",
+ false,
+ ["encrypt", "decrypt"],
+ );
+
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+
+ // encrypt won't fail, it will simply truncate the tag
+ // as expected.
+ const encrypted = await crypto.subtle.encrypt(
+ { name: "AES-GCM", iv, tagLength: 96 },
+ key,
+ new Uint8Array(32),
+ );
+
+ await assertRejects(async () => {
+ await crypto.subtle.decrypt(
+ { name: "AES-GCM", iv, tagLength: 96 },
+ key,
+ encrypted,
+ );
+ });
+});
+
+Deno.test(async function ecPrivateKeyMaterialExportSpki() {
+ // `generateKey` generates a key pair internally stored as "private" key.
+ const keys = await crypto.subtle.generateKey(
+ { name: "ECDSA", namedCurve: "P-256" },
+ true,
+ ["sign", "verify"],
+ );
+
+ assert(keys.privateKey instanceof CryptoKey);
+ assert(keys.publicKey instanceof CryptoKey);
+
+ // `exportKey` should be able to perform necessary conversion to export spki.
+ const spki = await crypto.subtle.exportKey("spki", keys.publicKey);
+ assert(spki instanceof ArrayBuffer);
+});
+
+// https://github.com/denoland/deno/issues/13911
+Deno.test(async function importJwkWithUse() {
+ const jwk = {
+ "kty": "EC",
+ "use": "sig",
+ "crv": "P-256",
+ "x": "FWZ9rSkLt6Dx9E3pxLybhdM6xgR5obGsj5_pqmnz5J4",
+ "y": "_n8G69C-A2Xl4xUW2lF0i8ZGZnk_KPYrhv4GbTGu5G4",
+ };
+
+ const algorithm = { name: "ECDSA", namedCurve: "P-256" };
+
+ const key = await crypto.subtle.importKey(
+ "jwk",
+ jwk,
+ algorithm,
+ true,
+ ["verify"],
+ );
+
+ assert(key instanceof CryptoKey);
+});
+
+// https://github.com/denoland/deno/issues/14215
+Deno.test(async function exportKeyNotExtractable() {
+ const key = await crypto.subtle.generateKey(
+ {
+ name: "HMAC",
+ hash: "SHA-512",
+ },
+ false,
+ ["sign", "verify"],
+ );
+
+ assert(key);
+ assertEquals(key.extractable, false);
+
+ await assertRejects(async () => {
+ // Should fail
+ await crypto.subtle.exportKey("raw", key);
+ }, DOMException);
+});
+
+// https://github.com/denoland/deno/issues/15126
+Deno.test(async function testImportLeadingZeroesKey() {
+ const alg = { name: "ECDSA", namedCurve: "P-256" };
+
+ const jwk = {
+ kty: "EC",
+ crv: "P-256",
+ alg: "ES256",
+ x: "EvidcdFB1xC6tgfakqZsU9aIURxAJkcX62zHe1Nt6xU",
+ y: "AHsk6BioGM7MZWeXOE_49AGmtuaXFT3Ill3DYtz9uYg",
+ d: "WDeYo4o1heCF9l_2VIaClRyIeO16zsMlN8UG6Le9dU8",
+ "key_ops": ["sign"],
+ ext: true,
+ };
+
+ const key = await crypto.subtle.importKey(
+ "jwk",
+ jwk,
+ alg,
+ true,
+ ["sign"],
+ );
+
+ assert(key instanceof CryptoKey);
+ assertEquals(key.type, "private");
+});
+
+// https://github.com/denoland/deno/issues/15523
+Deno.test(async function testECspkiRoundTrip() {
+ const alg = { name: "ECDH", namedCurve: "P-256" };
+ const { publicKey } = await crypto.subtle.generateKey(alg, true, [
+ "deriveBits",
+ ]);
+ const spki = await crypto.subtle.exportKey("spki", publicKey);
+ await crypto.subtle.importKey("spki", spki, alg, true, []);
+});
+
+Deno.test(async function testHmacJwkImport() {
+ await crypto.subtle.importKey(
+ "jwk",
+ {
+ kty: "oct",
+ use: "sig",
+ alg: "HS256",
+ k: "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg",
+ },
+ { name: "HMAC", hash: "SHA-256" },
+ false,
+ ["sign", "verify"],
+ );
+});
+
+Deno.test(async function p521Import() {
+ const jwk = {
+ "crv": "P-521",
+ "ext": true,
+ "key_ops": [
+ "verify",
+ ],
+ "kty": "EC",
+ "x":
+ "AXkSI8nfkc6bu3fifXGuKKbu08g5LKPfxUNQJJYzzPgmN8XLDzx0C9Sdeejl1XoWGrheKPHl0k4tUmHw0cdInpfj",
+ "y":
+ "AT4vjsO0bzVRlN3Wthv9DewncDXS2tlTob5QojV8WX1GzOAikRfWFEP3nspoSv88U447acZAsk5IvgGJuVjgMDlx",
+ };
+ const algorithm = { name: "ECDSA", namedCurve: "P-521" };
+
+ const key = await crypto.subtle.importKey(
+ "jwk",
+ jwk,
+ algorithm,
+ true,
+ ["verify"],
+ );
+
+ assert(key instanceof CryptoKey);
+});
+
+Deno.test(async function p521Generate() {
+ const algorithm = { name: "ECDSA", namedCurve: "P-521" };
+
+ const key = await crypto.subtle.generateKey(
+ algorithm,
+ true,
+ ["sign", "verify"],
+ );
+
+ assert(key.privateKey instanceof CryptoKey);
+ assert(key.publicKey instanceof CryptoKey);
+});
diff --git a/tests/unit/webgpu_test.ts b/tests/unit/webgpu_test.ts
new file mode 100644
index 000000000..517c75f9e
--- /dev/null
+++ b/tests/unit/webgpu_test.ts
@@ -0,0 +1,267 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assert, assertEquals, assertThrows } from "./test_util.ts";
+
+let isCI: boolean;
+try {
+ isCI = (Deno.env.get("CI")?.length ?? 0) > 0;
+} catch {
+ isCI = true;
+}
+
+// Skip these tests on linux CI, because the vulkan emulator is not good enough
+// yet, and skip on macOS CI because these do not have virtual GPUs.
+const isLinuxOrMacCI =
+ (Deno.build.os === "linux" || Deno.build.os === "darwin") && isCI;
+// Skip these tests in WSL because it doesn't have good GPU support.
+const isWsl = await checkIsWsl();
+
+Deno.test({
+ permissions: { read: true, env: true },
+ ignore: isWsl || isLinuxOrMacCI,
+}, async function webgpuComputePass() {
+ const adapter = await navigator.gpu.requestAdapter();
+ assert(adapter);
+
+ const numbers = [1, 4, 3, 295];
+
+ const device = await adapter.requestDevice();
+ assert(device);
+
+ const shaderCode = await Deno.readTextFile(
+ "tests/testdata/webgpu/computepass_shader.wgsl",
+ );
+
+ const shaderModule = device.createShaderModule({
+ code: shaderCode,
+ });
+
+ const size = new Uint32Array(numbers).byteLength;
+
+ const stagingBuffer = device.createBuffer({
+ size: size,
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+ });
+
+ const storageBuffer = device.createBuffer({
+ label: "Storage Buffer",
+ size: size,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST |
+ GPUBufferUsage.COPY_SRC,
+ mappedAtCreation: true,
+ });
+
+ const buf = new Uint32Array(storageBuffer.getMappedRange());
+
+ buf.set(numbers);
+
+ storageBuffer.unmap();
+
+ const computePipeline = device.createComputePipeline({
+ layout: "auto",
+ compute: {
+ module: shaderModule,
+ entryPoint: "main",
+ },
+ });
+ const bindGroupLayout = computePipeline.getBindGroupLayout(0);
+
+ const bindGroup = device.createBindGroup({
+ layout: bindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: storageBuffer,
+ },
+ },
+ ],
+ });
+
+ const encoder = device.createCommandEncoder();
+
+ const computePass = encoder.beginComputePass();
+ computePass.setPipeline(computePipeline);
+ computePass.setBindGroup(0, bindGroup);
+ computePass.insertDebugMarker("compute collatz iterations");
+ computePass.dispatchWorkgroups(numbers.length);
+ computePass.end();
+
+ encoder.copyBufferToBuffer(storageBuffer, 0, stagingBuffer, 0, size);
+
+ device.queue.submit([encoder.finish()]);
+
+ await stagingBuffer.mapAsync(1);
+
+ const data = stagingBuffer.getMappedRange();
+
+ assertEquals(new Uint32Array(data), new Uint32Array([0, 2, 7, 55]));
+
+ stagingBuffer.unmap();
+
+ device.destroy();
+
+ // TODO(lucacasonato): webgpu spec should add a explicit destroy method for
+ // adapters.
+ const resources = Object.keys(Deno.resources());
+ Deno.close(Number(resources[resources.length - 1]));
+});
+
+Deno.test({
+ permissions: { read: true, env: true },
+ ignore: isWsl || isLinuxOrMacCI,
+}, async function webgpuHelloTriangle() {
+ const adapter = await navigator.gpu.requestAdapter();
+ assert(adapter);
+
+ const device = await adapter.requestDevice();
+ assert(device);
+
+ const shaderCode = await Deno.readTextFile(
+ "tests/testdata/webgpu/hellotriangle_shader.wgsl",
+ );
+
+ const shaderModule = device.createShaderModule({
+ code: shaderCode,
+ });
+
+ const pipelineLayout = device.createPipelineLayout({
+ bindGroupLayouts: [],
+ });
+
+ const renderPipeline = device.createRenderPipeline({
+ layout: pipelineLayout,
+ vertex: {
+ module: shaderModule,
+ entryPoint: "vs_main",
+ },
+ fragment: {
+ module: shaderModule,
+ entryPoint: "fs_main",
+ targets: [
+ {
+ format: "rgba8unorm-srgb",
+ },
+ ],
+ },
+ });
+
+ const dimensions = {
+ width: 200,
+ height: 200,
+ };
+ const unpaddedBytesPerRow = dimensions.width * 4;
+ const align = 256;
+ const paddedBytesPerRowPadding = (align - unpaddedBytesPerRow % align) %
+ align;
+ const paddedBytesPerRow = unpaddedBytesPerRow + paddedBytesPerRowPadding;
+
+ const outputBuffer = device.createBuffer({
+ label: "Capture",
+ size: paddedBytesPerRow * dimensions.height,
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
+ });
+ const texture = device.createTexture({
+ label: "Capture",
+ size: dimensions,
+ format: "rgba8unorm-srgb",
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
+ });
+
+ const encoder = device.createCommandEncoder();
+ const view = texture.createView();
+ const renderPass = encoder.beginRenderPass({
+ colorAttachments: [
+ {
+ view,
+ storeOp: "store",
+ loadOp: "clear",
+ clearValue: [0, 1, 0, 1],
+ },
+ ],
+ });
+ renderPass.setPipeline(renderPipeline);
+ renderPass.draw(3, 1);
+ renderPass.end();
+
+ encoder.copyTextureToBuffer(
+ {
+ texture,
+ },
+ {
+ buffer: outputBuffer,
+ bytesPerRow: paddedBytesPerRow,
+ rowsPerImage: 0,
+ },
+ dimensions,
+ );
+
+ const bundle = encoder.finish();
+ device.queue.submit([bundle]);
+
+ await outputBuffer.mapAsync(1);
+ const data = new Uint8Array(outputBuffer.getMappedRange());
+
+ assertEquals(
+ data,
+ await Deno.readFile("tests/testdata/webgpu/hellotriangle.out"),
+ );
+
+ outputBuffer.unmap();
+
+ device.destroy();
+
+ // TODO(lucacasonato): webgpu spec should add a explicit destroy method for
+ // adapters.
+ const resources = Object.keys(Deno.resources());
+ Deno.close(Number(resources[resources.length - 1]));
+});
+
+Deno.test({
+ ignore: isWsl || isLinuxOrMacCI,
+}, async function webgpuAdapterHasFeatures() {
+ const adapter = await navigator.gpu.requestAdapter();
+ assert(adapter);
+ assert(adapter.features);
+ const resources = Object.keys(Deno.resources());
+ Deno.close(Number(resources[resources.length - 1]));
+});
+
+Deno.test({
+ ignore: isWsl || isLinuxOrMacCI,
+}, async function webgpuNullWindowSurfaceThrows() {
+ const adapter = await navigator.gpu.requestAdapter();
+ assert(adapter);
+
+ const device = await adapter.requestDevice();
+ assert(device);
+
+ assertThrows(
+ () => {
+ new Deno.UnsafeWindowSurface("cocoa", null, null);
+ },
+ );
+
+ device.destroy();
+ const resources = Object.keys(Deno.resources());
+ Deno.close(Number(resources[resources.length - 1]));
+});
+
+Deno.test(function getPreferredCanvasFormat() {
+ const preferredFormat = navigator.gpu.getPreferredCanvasFormat();
+ assert(preferredFormat === "bgra8unorm" || preferredFormat === "rgba8unorm");
+});
+
+async function checkIsWsl() {
+ return Deno.build.os === "linux" && await hasMicrosoftProcVersion();
+
+ async function hasMicrosoftProcVersion() {
+ // https://github.com/microsoft/WSL/issues/423#issuecomment-221627364
+ try {
+ const procVersion = await Deno.readTextFile("/proc/version");
+ return /microsoft/i.test(procVersion);
+ } catch {
+ return false;
+ }
+ }
+}
diff --git a/tests/unit/websocket_test.ts b/tests/unit/websocket_test.ts
new file mode 100644
index 000000000..223b13404
--- /dev/null
+++ b/tests/unit/websocket_test.ts
@@ -0,0 +1,738 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals, assertThrows, fail } from "./test_util.ts";
+
+const servePort = 4248;
+const serveUrl = `ws://localhost:${servePort}/`;
+
+Deno.test({ permissions: "none" }, function websocketPermissionless() {
+ assertThrows(
+ () => new WebSocket("ws://localhost"),
+ Deno.errors.PermissionDenied,
+ );
+});
+
+Deno.test(async function websocketConstructorTakeURLObjectAsParameter() {
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
+ const ws = new WebSocket(new URL("ws://localhost:4242/"));
+ assertEquals(ws.url, "ws://localhost:4242/");
+ ws.onerror = (e) => reject(e);
+ ws.onopen = () => ws.close();
+ ws.onclose = () => {
+ resolve();
+ };
+ await promise;
+});
+
+Deno.test(async function websocketH2SendSmallPacket() {
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
+ const ws = new WebSocket(new URL("wss://localhost:4249/"));
+ assertEquals(ws.url, "wss://localhost:4249/");
+ let messageCount = 0;
+ ws.onerror = (e) => reject(e);
+ ws.onopen = () => {
+ ws.send("a".repeat(16));
+ ws.send("a".repeat(16));
+ ws.send("a".repeat(16));
+ };
+ ws.onmessage = () => {
+ if (++messageCount == 3) {
+ ws.close();
+ }
+ };
+ ws.onclose = () => {
+ resolve();
+ };
+ await promise;
+});
+
+Deno.test(async function websocketH2SendLargePacket() {
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
+ const ws = new WebSocket(new URL("wss://localhost:4249/"));
+ assertEquals(ws.url, "wss://localhost:4249/");
+ let messageCount = 0;
+ ws.onerror = (e) => reject(e);
+ ws.onopen = () => {
+ ws.send("a".repeat(65000));
+ ws.send("a".repeat(65000));
+ ws.send("a".repeat(65000));
+ };
+ ws.onmessage = () => {
+ if (++messageCount == 3) {
+ ws.close();
+ }
+ };
+ ws.onclose = () => {
+ resolve();
+ };
+ await promise;
+});
+
+Deno.test(async function websocketSendLargePacket() {
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
+ const ws = new WebSocket(new URL("wss://localhost:4243/"));
+ assertEquals(ws.url, "wss://localhost:4243/");
+ ws.onerror = (e) => reject(e);
+ ws.onopen = () => {
+ ws.send("a".repeat(65000));
+ };
+ ws.onmessage = () => {
+ ws.close();
+ };
+ ws.onclose = () => {
+ resolve();
+ };
+ await promise;
+});
+
+Deno.test(async function websocketSendLargeBinaryPacket() {
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
+ const ws = new WebSocket(new URL("wss://localhost:4243/"));
+ ws.binaryType = "arraybuffer";
+ assertEquals(ws.url, "wss://localhost:4243/");
+ ws.onerror = (e) => reject(e);
+ ws.onopen = () => {
+ ws.send(new Uint8Array(65000));
+ };
+ ws.onmessage = (msg: MessageEvent) => {
+ assertEquals(msg.data.byteLength, 65000);
+ ws.close();
+ };
+ ws.onclose = () => {
+ resolve();
+ };
+ await promise;
+});
+
+Deno.test(async function websocketSendLargeBlobPacket() {
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
+ const ws = new WebSocket(new URL("wss://localhost:4243/"));
+ ws.binaryType = "arraybuffer";
+ assertEquals(ws.url, "wss://localhost:4243/");
+ ws.onerror = (e) => reject(e);
+ ws.onopen = () => {
+ ws.send(new Blob(["a".repeat(65000)]));
+ };
+ ws.onmessage = (msg: MessageEvent) => {
+ assertEquals(msg.data.byteLength, 65000);
+ ws.close();
+ };
+ ws.onclose = () => {
+ resolve();
+ };
+ await promise;
+});
+
+// https://github.com/denoland/deno/pull/17762
+// https://github.com/denoland/deno/issues/17761
+Deno.test(async function websocketPingPong() {
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
+ const ws = new WebSocket("ws://localhost:4245/");
+ assertEquals(ws.url, "ws://localhost:4245/");
+ ws.onerror = (e) => reject(e);
+ ws.onmessage = (e) => {
+ ws.send(e.data);
+ };
+ ws.onclose = () => {
+ resolve();
+ };
+ await promise;
+ ws.close();
+});
+
+// TODO(mmastrac): This requires us to ignore bad certs
+// Deno.test(async function websocketSecureConnect() {
+// const { promise, resolve } = Promise.withResolvers<void>();
+// const ws = new WebSocket("wss://localhost:4243/");
+// assertEquals(ws.url, "wss://localhost:4243/");
+// ws.onerror = (error) => {
+// console.log(error);
+// fail();
+// };
+// ws.onopen = () => ws.close();
+// ws.onclose = () => {
+// resolve();
+// };
+// await promise;
+// });
+
+// https://github.com/denoland/deno/issues/18700
+Deno.test(
+ { sanitizeOps: false, sanitizeResources: false },
+ async function websocketWriteLock() {
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: (req) => {
+ const { socket, response } = Deno.upgradeWebSocket(req);
+ socket.onopen = function () {
+ setTimeout(() => socket.send("Hello"), 500);
+ };
+ socket.onmessage = function (e) {
+ assertEquals(e.data, "Hello");
+ ac.abort();
+ };
+ return response;
+ },
+ signal: ac.signal,
+ onListen: () => listeningDeferred.resolve(),
+ hostname: "localhost",
+ port: servePort,
+ });
+
+ await listeningDeferred.promise;
+ const deferred = Promise.withResolvers<void>();
+ const ws = new WebSocket(serveUrl);
+ assertEquals(ws.url, serveUrl);
+ ws.onerror = () => fail();
+ ws.onmessage = (e) => {
+ assertEquals(e.data, "Hello");
+ setTimeout(() => {
+ ws.send(e.data);
+ }, 1000);
+ deferred.resolve();
+ };
+ ws.onclose = () => {
+ deferred.resolve();
+ };
+
+ await Promise.all([deferred.promise, server.finished]);
+ ws.close();
+ },
+);
+
+// https://github.com/denoland/deno/issues/18775
+Deno.test({
+ sanitizeOps: false,
+ sanitizeResources: false,
+}, async function websocketDoubleClose() {
+ const deferred = Promise.withResolvers<void>();
+
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: (req) => {
+ const { response, socket } = Deno.upgradeWebSocket(req);
+ let called = false;
+ socket.onopen = () => socket.send("Hello");
+ socket.onmessage = () => {
+ assert(!called);
+ called = true;
+ socket.send("bye");
+ socket.close();
+ };
+ socket.onclose = () => ac.abort();
+ socket.onerror = () => fail();
+ return response;
+ },
+ signal: ac.signal,
+ onListen: () => listeningDeferred.resolve(),
+ hostname: "localhost",
+ port: servePort,
+ });
+
+ await listeningDeferred.promise;
+
+ const ws = new WebSocket(serveUrl);
+ assertEquals(ws.url, serveUrl);
+ ws.onerror = () => fail();
+ ws.onmessage = (m: MessageEvent) => {
+ if (m.data == "Hello") ws.send("bye");
+ };
+ ws.onclose = () => {
+ deferred.resolve();
+ };
+ await Promise.all([deferred.promise, server.finished]);
+});
+
+// https://github.com/denoland/deno/issues/19483
+Deno.test({
+ sanitizeOps: false,
+ sanitizeResources: false,
+}, async function websocketCloseFlushes() {
+ const deferred = Promise.withResolvers<void>();
+
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: (req) => {
+ const { response, socket } = Deno.upgradeWebSocket(req);
+ socket.onopen = () => socket.send("Hello");
+ socket.onmessage = () => {
+ socket.send("Bye");
+ socket.close();
+ };
+ socket.onclose = () => ac.abort();
+ socket.onerror = () => fail();
+ return response;
+ },
+ signal: ac.signal,
+ onListen: () => listeningDeferred.resolve(),
+ hostname: "localhost",
+ port: servePort,
+ });
+
+ await listeningDeferred.promise;
+
+ const ws = new WebSocket(serveUrl);
+ assertEquals(ws.url, serveUrl);
+ let seenBye = false;
+ ws.onerror = () => fail();
+ ws.onmessage = ({ data }) => {
+ if (data == "Hello") {
+ ws.send("Hello!");
+ } else {
+ assertEquals(data, "Bye");
+ seenBye = true;
+ }
+ };
+ ws.onclose = () => {
+ deferred.resolve();
+ };
+ await Promise.all([deferred.promise, server.finished]);
+
+ assert(seenBye);
+});
+
+Deno.test(
+ { sanitizeOps: false },
+ function websocketConstructorWithPrototypePollution() {
+ const originalSymbolIterator = Array.prototype[Symbol.iterator];
+ try {
+ Array.prototype[Symbol.iterator] = () => {
+ throw Error("unreachable");
+ };
+ assertThrows(() => {
+ new WebSocket(
+ new URL("ws://localhost:4242/"),
+ // Allow `Symbol.iterator` to be called in WebIDL conversion to `sequence<DOMString>`
+ // deno-lint-ignore no-explicit-any
+ ["soap", "soap"].values() as any,
+ );
+ }, DOMException);
+ } finally {
+ Array.prototype[Symbol.iterator] = originalSymbolIterator;
+ }
+ },
+);
+
+Deno.test(async function websocketTlsSocketWorks() {
+ const cert = await Deno.readTextFile("tests/testdata/tls/localhost.crt");
+ const key = await Deno.readTextFile("tests/testdata/tls/localhost.key");
+
+ const messages: string[] = [],
+ errors: { server?: Event; client?: Event }[] = [];
+ const promise = new Promise((okay, nope) => {
+ const ac = new AbortController();
+ const server = Deno.serve({
+ handler: (req) => {
+ const { response, socket } = Deno.upgradeWebSocket(req);
+ socket.onopen = () => socket.send("ping");
+ socket.onmessage = (e) => {
+ messages.push(e.data);
+ socket.close();
+ };
+ socket.onerror = (e) => errors.push({ server: e });
+ socket.onclose = () => ac.abort();
+ return response;
+ },
+ signal: ac.signal,
+ hostname: "localhost",
+ port: servePort,
+ cert,
+ key,
+ });
+ setTimeout(() => {
+ const ws = new WebSocket(`wss://localhost:${servePort}`);
+ ws.onmessage = (e) => {
+ messages.push(e.data);
+ ws.send("pong");
+ };
+ ws.onerror = (e) => {
+ errors.push({ client: e });
+ nope();
+ };
+ ws.onclose = () => okay(server.finished);
+ }, 1000);
+ });
+
+ const finished = await promise;
+
+ assertEquals(errors, []);
+ assertEquals(messages, ["ping", "pong"]);
+
+ await finished;
+});
+
+// https://github.com/denoland/deno/issues/15340
+Deno.test(
+ async function websocketServerFieldInit() {
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: (req) => {
+ const { socket, response } = Deno.upgradeWebSocket(req, {
+ idleTimeout: 0,
+ });
+ socket.onopen = function () {
+ assert(typeof socket.url == "string");
+ assert(socket.readyState == WebSocket.OPEN);
+ assert(socket.protocol == "");
+ assert(socket.binaryType == "arraybuffer");
+ socket.close();
+ };
+ socket.onclose = () => ac.abort();
+ return response;
+ },
+ signal: ac.signal,
+ onListen: () => listeningDeferred.resolve(),
+ hostname: "localhost",
+ port: servePort,
+ });
+
+ await listeningDeferred.promise;
+ const deferred = Promise.withResolvers<void>();
+ const ws = new WebSocket(serveUrl);
+ assertEquals(ws.url, serveUrl);
+ ws.onerror = () => fail();
+ ws.onclose = () => {
+ deferred.resolve();
+ };
+
+ await Promise.all([deferred.promise, server.finished]);
+ },
+);
+
+Deno.test(
+ { sanitizeOps: false },
+ async function websocketServerGetsGhosted() {
+ const ac = new AbortController();
+ const listeningDeferred = Promise.withResolvers<void>();
+
+ const server = Deno.serve({
+ handler: (req) => {
+ const { socket, response } = Deno.upgradeWebSocket(req, {
+ idleTimeout: 2,
+ });
+ socket.onerror = () => socket.close();
+ socket.onclose = () => ac.abort();
+ return response;
+ },
+ signal: ac.signal,
+ onListen: () => listeningDeferred.resolve(),
+ hostname: "localhost",
+ port: servePort,
+ });
+
+ await listeningDeferred.promise;
+ const r = await fetch("http://localhost:4545/ghost_ws_client");
+ assertEquals(r.status, 200);
+ await r.body?.cancel();
+
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ const deferred2 = Promise.withResolvers<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ const ws = new WebSocket("ws://localhost:4244");
+ ws.onerror = () => fail();
+ ws.onclose = (e) => {
+ assertEquals(e.code, 1005);
+ resolve();
+ };
+ await promise;
+});
diff --git a/tests/unit/websocketstream_test.ts.disabled b/tests/unit/websocketstream_test.ts.disabled
new file mode 100644
index 000000000..eaedb71bd
--- /dev/null
+++ b/tests/unit/websocketstream_test.ts.disabled
@@ -0,0 +1,359 @@
+// 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", { sanitizeOps: false }, 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<void>((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<void>((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<void>((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<void>((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();
+ },
+);
+
+// TODO(mmastrac): Failed on CI, disabled
+Deno.test("async close with empty stream", { ignore: true }, 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<void>((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();
+});
+
+// TODO(mmastrac): Failed on CI, disabled
+Deno.test(
+ "async close with unread messages in stream",
+ { ignore: true },
+ 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<void>((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/tests/unit/webstorage_test.ts b/tests/unit/webstorage_test.ts
new file mode 100644
index 000000000..9dc560af1
--- /dev/null
+++ b/tests/unit/webstorage_test.ts
@@ -0,0 +1,52 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+// deno-lint-ignore-file no-explicit-any
+
+import { assert, assertEquals, assertThrows } from "./test_util.ts";
+
+Deno.test({ permissions: "none" }, function webStoragesReassignable() {
+ // Can reassign to web storages
+ globalThis.localStorage = 1 as any;
+ globalThis.sessionStorage = 1 as any;
+ // The actual values don't change
+ assert(globalThis.localStorage instanceof globalThis.Storage);
+ assert(globalThis.sessionStorage instanceof globalThis.Storage);
+});
+
+Deno.test(function webstorageSizeLimit() {
+ localStorage.clear();
+ assertThrows(
+ () => {
+ localStorage.setItem("k", "v".repeat(15 * 1024 * 1024));
+ },
+ Error,
+ "Exceeded maximum storage size",
+ );
+ assertEquals(localStorage.getItem("k"), null);
+ assertThrows(
+ () => {
+ localStorage.setItem("k".repeat(15 * 1024 * 1024), "v");
+ },
+ Error,
+ "Exceeded maximum storage size",
+ );
+ assertThrows(
+ () => {
+ localStorage.setItem(
+ "k".repeat(5 * 1024 * 1024),
+ "v".repeat(5 * 1024 * 1024),
+ );
+ },
+ Error,
+ "Exceeded maximum storage size",
+ );
+});
+
+Deno.test(function webstorageProxy() {
+ localStorage.clear();
+ localStorage.foo = "foo";
+ assertEquals(localStorage.foo, "foo");
+ const symbol = Symbol("bar");
+ localStorage[symbol as any] = "bar";
+ assertEquals(localStorage[symbol as any], "bar");
+ assertEquals(symbol in localStorage, true);
+});
diff --git a/tests/unit/worker_permissions_test.ts b/tests/unit/worker_permissions_test.ts
new file mode 100644
index 000000000..28bf9f92a
--- /dev/null
+++ b/tests/unit/worker_permissions_test.ts
@@ -0,0 +1,34 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "./test_util.ts";
+
+Deno.test(
+ { permissions: { env: true, read: true } },
+ async function workerEnvArrayPermissions() {
+ const { promise, resolve } = Promise.withResolvers<boolean[]>();
+
+ const worker = new Worker(
+ import.meta.resolve(
+ "../testdata/workers/env_read_check_worker.js",
+ ),
+ { type: "module", deno: { permissions: { env: ["test", "OTHER"] } } },
+ );
+
+ worker.onmessage = ({ data }) => {
+ resolve(data.permissions);
+ };
+
+ worker.postMessage({
+ names: ["test", "TEST", "asdf", "OTHER"],
+ });
+
+ const permissions = await promise;
+ worker.terminate();
+
+ if (Deno.build.os === "windows") {
+ // windows ignores case
+ assertEquals(permissions, [true, true, false, true]);
+ } else {
+ assertEquals(permissions, [true, false, false, true]);
+ }
+ },
+);
diff --git a/tests/unit/worker_test.ts b/tests/unit/worker_test.ts
new file mode 100644
index 000000000..eea0e8106
--- /dev/null
+++ b/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<string>();
+ jsWorker.onmessage = (e) => {
+ deferred1.resolve(e.data);
+ };
+
+ const deferred2 = Promise.withResolvers<string>();
+ 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<string>();
+ 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<any>();
+ 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<string>();
+ // 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<string>();
+ 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<string>();
+ 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<string>();
+ // 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<number>();
+
+ 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<void>();
+
+ 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<void>();
+ const deferred2 = Promise.withResolvers<void>();
+
+ 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<any>();
+ 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<string>();
+ 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<boolean>();
+ 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<void>();
+ 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<void>();
+ 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<void>();
+ 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<boolean>();
+ 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<boolean>();
+ 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<any>();
+ 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<any>();
+ 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<boolean>();
+ 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<any>();
+ 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<any>();
+ 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<string>();
+ 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<void>();
+ 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<void>();
+ 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<any>();
+ 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<string>();
+ 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<void>();
+ 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<any>();
+ const deferred2 = Promise.withResolvers<boolean>();
+ const deferred3 = Promise.withResolvers<boolean>();
+ const result = Promise.withResolvers<void>();
+ 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<any>();
+ w.onmessage = function (evt) {
+ resolve(evt.data);
+ };
+
+ assertEquals(
+ Object.keys(
+ await promise as unknown as Record<string, number>,
+ ),
+ ["rss", "heapTotal", "heapUsed", "external"],
+ );
+ w.terminate();
+ },
+});
diff --git a/tests/unit/write_file_test.ts b/tests/unit/write_file_test.ts
new file mode 100644
index 000000000..6cd08e2d1
--- /dev/null
+++ b/tests/unit/write_file_test.ts
@@ -0,0 +1,427 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+ unreachable,
+} from "./test_util.ts";
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function writeFileSyncSuccess() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data);
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ const actual = dec.decode(dataRead);
+ assertEquals(actual, "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function writeFileSyncUrl() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+ const fileUrl = new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/test.txt`,
+ );
+ Deno.writeFileSync(fileUrl, data);
+ const dataRead = Deno.readFileSync(fileUrl);
+ const dec = new TextDecoder("utf-8");
+ const actual = dec.decode(dataRead);
+ assertEquals(actual, "Hello");
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test({ permissions: { write: true } }, function writeFileSyncFail() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = "/baddir/test.txt";
+ // The following should fail because /baddir doesn't exist (hopefully).
+ assertThrows(() => {
+ Deno.writeFileSync(filename, data);
+ }, Deno.errors.NotFound);
+});
+
+Deno.test({ permissions: { write: false } }, function writeFileSyncPerm() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = "/baddir/test.txt";
+ // The following should fail due to no write permission
+ assertThrows(() => {
+ Deno.writeFileSync(filename, data);
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function writeFileSyncUpdateMode() {
+ if (Deno.build.os !== "windows") {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data, { mode: 0o755 });
+ assertEquals(Deno.statSync(filename).mode! & 0o777, 0o755);
+ Deno.writeFileSync(filename, data, { mode: 0o666 });
+ assertEquals(Deno.statSync(filename).mode! & 0o777, 0o666);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function writeFileSyncCreate() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ // if create turned off, the file won't be created
+ assertThrows(() => {
+ Deno.writeFileSync(filename, data, { create: false });
+ }, Deno.errors.NotFound);
+
+ // Turn on create, should have no error
+ Deno.writeFileSync(filename, data, { create: true });
+ Deno.writeFileSync(filename, data, { create: false });
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ const actual = dec.decode(dataRead);
+ assertEquals(actual, "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function writeFileSyncCreateNew() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data, { createNew: true });
+
+ assertThrows(() => {
+ Deno.writeFileSync(filename, data, { createNew: true });
+ }, Deno.errors.AlreadyExists);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function writeFileSyncAppend() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data);
+ Deno.writeFileSync(filename, data, { append: true });
+ let dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ let actual = dec.decode(dataRead);
+ assertEquals(actual, "HelloHello");
+ // Now attempt overwrite
+ Deno.writeFileSync(filename, data, { append: false });
+ dataRead = Deno.readFileSync(filename);
+ actual = dec.decode(dataRead);
+ assertEquals(actual, "Hello");
+ // append not set should also overwrite
+ Deno.writeFileSync(filename, data);
+ dataRead = Deno.readFileSync(filename);
+ actual = dec.decode(dataRead);
+ assertEquals(actual, "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileSuccess() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeFile(filename, data);
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ const actual = dec.decode(dataRead);
+ assertEquals(actual, "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileUrl() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = await Deno.makeTempDir();
+ const fileUrl = new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/test.txt`,
+ );
+ await Deno.writeFile(fileUrl, data);
+ const dataRead = Deno.readFileSync(fileUrl);
+ const dec = new TextDecoder("utf-8");
+ const actual = dec.decode(dataRead);
+ assertEquals(actual, "Hello");
+
+ Deno.removeSync(tempDir, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileNotFound() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = "/baddir/test.txt";
+ // The following should fail because /baddir doesn't exist (hopefully).
+ await assertRejects(async () => {
+ await Deno.writeFile(filename, data);
+ }, Deno.errors.NotFound);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: false } },
+ async function writeFilePerm() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = "/baddir/test.txt";
+ // The following should fail due to no write permission
+ await assertRejects(async () => {
+ await Deno.writeFile(filename, data);
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileUpdateMode() {
+ if (Deno.build.os !== "windows") {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeFile(filename, data, { mode: 0o755 });
+ assertEquals(Deno.statSync(filename).mode! & 0o777, 0o755);
+ await Deno.writeFile(filename, data, { mode: 0o666 });
+ assertEquals(Deno.statSync(filename).mode! & 0o777, 0o666);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileCreate() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ // if create turned off, the file won't be created
+ await assertRejects(async () => {
+ await Deno.writeFile(filename, data, { create: false });
+ }, Deno.errors.NotFound);
+
+ // Turn on create, should have no error
+ await Deno.writeFile(filename, data, { create: true });
+ await Deno.writeFile(filename, data, { create: false });
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ const actual = dec.decode(dataRead);
+ assertEquals(actual, "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileCreateNew() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeFile(filename, data, { createNew: true });
+ await assertRejects(async () => {
+ await Deno.writeFile(filename, data, { createNew: true });
+ }, Deno.errors.AlreadyExists);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileAppend() {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeFile(filename, data);
+ await Deno.writeFile(filename, data, { append: true });
+ let dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ let actual = dec.decode(dataRead);
+ assertEquals(actual, "HelloHello");
+ // Now attempt overwrite
+ await Deno.writeFile(filename, data, { append: false });
+ dataRead = Deno.readFileSync(filename);
+ actual = dec.decode(dataRead);
+ assertEquals(actual, "Hello");
+ // append not set should also overwrite
+ await Deno.writeFile(filename, data);
+ dataRead = Deno.readFileSync(filename);
+ actual = dec.decode(dataRead);
+ assertEquals(actual, "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileAbortSignal(): Promise<void> {
+ const ac = new AbortController();
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ queueMicrotask(() => ac.abort());
+ try {
+ await Deno.writeFile(filename, data, { signal: ac.signal });
+ unreachable();
+ } catch (e) {
+ assert(e instanceof Error);
+ assertEquals(e.name, "AbortError");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileAbortSignalReason(): Promise<void> {
+ const ac = new AbortController();
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ const abortReason = new Error();
+ queueMicrotask(() => ac.abort(abortReason));
+ try {
+ await Deno.writeFile(filename, data, { signal: ac.signal });
+ unreachable();
+ } catch (e) {
+ assertEquals(e, abortReason);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileAbortSignalPrimitiveReason(): Promise<void> {
+ const ac = new AbortController();
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ queueMicrotask(() => ac.abort("Some string"));
+ try {
+ await Deno.writeFile(filename, data, { signal: ac.signal });
+ unreachable();
+ } catch (e) {
+ assertEquals(e, "Some string");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileAbortSignalPreAborted(): Promise<void> {
+ const ac = new AbortController();
+ ac.abort();
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ try {
+ await Deno.writeFile(filename, data, { signal: ac.signal });
+ unreachable();
+ } catch (e) {
+ assert(e instanceof Error);
+ assertEquals(e.name, "AbortError");
+ }
+ assertNotExists(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileAbortSignalReasonPreAborted(): Promise<void> {
+ const ac = new AbortController();
+ const abortReason = new Error();
+ ac.abort(abortReason);
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ try {
+ await Deno.writeFile(filename, data, { signal: ac.signal });
+ unreachable();
+ } catch (e) {
+ assertEquals(e, abortReason);
+ }
+ assertNotExists(filename);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileAbortSignalPrimitiveReasonPreAborted(): Promise<
+ void
+ > {
+ const ac = new AbortController();
+ ac.abort("Some string");
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ try {
+ await Deno.writeFile(filename, data, { signal: ac.signal });
+ unreachable();
+ } catch (e) {
+ assertEquals(e, "Some string");
+ }
+ assertNotExists(filename);
+ },
+);
+
+// Test that AbortController's cancel handle is cleaned-up correctly, and do not leak resources.
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileWithAbortSignalNotCalled() {
+ const ac = new AbortController();
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeFile(filename, data, { signal: ac.signal });
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ const actual = dec.decode(dataRead);
+ assertEquals(actual, "Hello");
+ },
+);
+
+function assertNotExists(filename: string | URL) {
+ if (pathExists(filename)) {
+ throw new Error(`The file ${filename} exists.`);
+ }
+}
+
+function pathExists(path: string | URL) {
+ try {
+ Deno.statSync(path);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeFileStream() {
+ const stream = new ReadableStream({
+ pull(controller) {
+ controller.enqueue(new Uint8Array([1]));
+ controller.enqueue(new Uint8Array([2]));
+ controller.close();
+ },
+ });
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeFile(filename, stream);
+ assertEquals(Deno.readFileSync(filename), new Uint8Array([1, 2]));
+ },
+);
diff --git a/tests/unit/write_text_file_test.ts b/tests/unit/write_text_file_test.ts
new file mode 100644
index 000000000..a58d91997
--- /dev/null
+++ b/tests/unit/write_text_file_test.ts
@@ -0,0 +1,218 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+} from "./test_util.ts";
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function writeTextFileSyncSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeTextFileSync(filename, "Hello");
+ const dataRead = Deno.readTextFileSync(filename);
+ assertEquals(dataRead, "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function writeTextFileSyncByUrl() {
+ const tempDir = Deno.makeTempDirSync();
+ const fileUrl = new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/test.txt`,
+ );
+ Deno.writeTextFileSync(fileUrl, "Hello");
+ const dataRead = Deno.readTextFileSync(fileUrl);
+ assertEquals(dataRead, "Hello");
+
+ Deno.removeSync(fileUrl, { recursive: true });
+ },
+);
+
+Deno.test({ permissions: { write: true } }, function writeTextFileSyncFail() {
+ const filename = "/baddir/test.txt";
+ // The following should fail because /baddir doesn't exist (hopefully).
+ assertThrows(() => {
+ Deno.writeTextFileSync(filename, "hello");
+ }, Deno.errors.NotFound);
+});
+
+Deno.test({ permissions: { write: false } }, function writeTextFileSyncPerm() {
+ const filename = "/baddir/test.txt";
+ // The following should fail due to no write permission
+ assertThrows(() => {
+ Deno.writeTextFileSync(filename, "Hello");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function writeTextFileSyncUpdateMode() {
+ if (Deno.build.os !== "windows") {
+ const data = "Hello";
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeTextFileSync(filename, data, { mode: 0o755 });
+ assertEquals(Deno.statSync(filename).mode! & 0o777, 0o755);
+ Deno.writeTextFileSync(filename, data, { mode: 0o666 });
+ assertEquals(Deno.statSync(filename).mode! & 0o777, 0o666);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function writeTextFileSyncCreate() {
+ const data = "Hello";
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ let caughtError = false;
+ // if create turned off, the file won't be created
+ try {
+ Deno.writeTextFileSync(filename, data, { create: false });
+ } catch (e) {
+ caughtError = true;
+ assert(e instanceof Deno.errors.NotFound);
+ }
+ assert(caughtError);
+
+ // Turn on create, should have no error
+ Deno.writeTextFileSync(filename, data, { create: true });
+ Deno.writeTextFileSync(filename, data, { create: false });
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ function writeTextFileSyncAppend() {
+ const data = "Hello";
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeTextFileSync(filename, data);
+ Deno.writeTextFileSync(filename, data, { append: true });
+ assertEquals(Deno.readTextFileSync(filename), "HelloHello");
+ // Now attempt overwrite
+ Deno.writeTextFileSync(filename, data, { append: false });
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
+ // append not set should also overwrite
+ Deno.writeTextFileSync(filename, data);
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeTextFileSuccess() {
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeTextFile(filename, "Hello");
+ const dataRead = Deno.readTextFileSync(filename);
+ assertEquals(dataRead, "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeTextFileByUrl() {
+ const tempDir = Deno.makeTempDirSync();
+ const fileUrl = new URL(
+ `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/test.txt`,
+ );
+ await Deno.writeTextFile(fileUrl, "Hello");
+ const dataRead = Deno.readTextFileSync(fileUrl);
+ assertEquals(dataRead, "Hello");
+
+ Deno.removeSync(fileUrl, { recursive: true });
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeTextFileNotFound() {
+ const filename = "/baddir/test.txt";
+ // The following should fail because /baddir doesn't exist (hopefully).
+ await assertRejects(async () => {
+ await Deno.writeTextFile(filename, "Hello");
+ }, Deno.errors.NotFound);
+ },
+);
+
+Deno.test(
+ { permissions: { write: false } },
+ async function writeTextFilePerm() {
+ const filename = "/baddir/test.txt";
+ // The following should fail due to no write permission
+ await assertRejects(async () => {
+ await Deno.writeTextFile(filename, "Hello");
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeTextFileUpdateMode() {
+ if (Deno.build.os !== "windows") {
+ const data = "Hello";
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeTextFile(filename, data, { mode: 0o755 });
+ assertEquals(Deno.statSync(filename).mode! & 0o777, 0o755);
+ await Deno.writeTextFile(filename, data, { mode: 0o666 });
+ assertEquals(Deno.statSync(filename).mode! & 0o777, 0o666);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeTextFileCreate() {
+ const data = "Hello";
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ let caughtError = false;
+ // if create turned off, the file won't be created
+ try {
+ await Deno.writeTextFile(filename, data, { create: false });
+ } catch (e) {
+ caughtError = true;
+ assert(e instanceof Deno.errors.NotFound);
+ }
+ assert(caughtError);
+
+ // Turn on create, should have no error
+ await Deno.writeTextFile(filename, data, { create: true });
+ await Deno.writeTextFile(filename, data, { create: false });
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeTextFileAppend() {
+ const data = "Hello";
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeTextFile(filename, data);
+ await Deno.writeTextFile(filename, data, { append: true });
+ assertEquals(Deno.readTextFileSync(filename), "HelloHello");
+ // Now attempt overwrite
+ await Deno.writeTextFile(filename, data, { append: false });
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
+ // append not set should also overwrite
+ await Deno.writeTextFile(filename, data);
+ assertEquals(Deno.readTextFileSync(filename), "Hello");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, write: true } },
+ async function writeTextFileStream() {
+ const stream = new ReadableStream({
+ pull(controller) {
+ controller.enqueue("Hello");
+ controller.enqueue("World");
+ controller.close();
+ },
+ });
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeTextFile(filename, stream);
+ assertEquals(Deno.readTextFileSync(filename), "HelloWorld");
+ },
+);