diff options
author | Matt Mastracci <matthew@mastracci.com> | 2024-02-10 13:22:13 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-10 20:22:13 +0000 |
commit | f5e46c9bf2f50d66a953fa133161fc829cecff06 (patch) | |
tree | 8faf2f5831c1c7b11d842cd9908d141082c869a5 /tests/unit | |
parent | d2477f780630a812bfd65e3987b70c0d309385bb (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')
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¶m2=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¶m2"); + 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"); + }, +); |