// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { unitTest, assert, assertEquals, assertThrows, assertThrowsAsync, fail, } from "./test_util.ts"; unitTest({ perms: { net: true } }, async function fetchProtocolError(): Promise< void > { await assertThrowsAsync( async (): Promise => { await fetch("file:///"); }, TypeError, "not supported" ); }); unitTest( { perms: { net: true } }, async function fetchConnectionError(): Promise { await assertThrowsAsync( async (): Promise => { await fetch("http://localhost:4000"); }, Deno.errors.Http, "error trying to connect" ); } ); unitTest({ perms: { net: true } }, async function fetchJsonSuccess(): Promise< void > { const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); const json = await response.json(); assertEquals(json.name, "deno"); }); unitTest(async function fetchPerm(): Promise { await assertThrowsAsync(async () => { await fetch("http://localhost:4545/cli/tests/fixture.json"); }, Deno.errors.PermissionDenied); }); unitTest({ perms: { net: true } }, async function fetchUrl(): Promise { const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); assertEquals(response.url, "http://localhost:4545/cli/tests/fixture.json"); const _json = await response.json(); }); unitTest({ perms: { net: true } }, async function fetchURL(): Promise { const response = await fetch( new URL("http://localhost:4545/cli/tests/fixture.json") ); assertEquals(response.url, "http://localhost:4545/cli/tests/fixture.json"); const _json = await response.json(); }); unitTest({ perms: { net: true } }, async function fetchHeaders(): Promise< void > { const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); const headers = response.headers; assertEquals(headers.get("Content-Type"), "application/json"); assert(headers.get("Server")!.startsWith("SimpleHTTP")); const _json = await response.json(); }); unitTest({ perms: { net: true } }, async function fetchBlob(): Promise { const response = await fetch("http://localhost:4545/cli/tests/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"))); }); unitTest({ perms: { net: true } }, async function fetchBodyUsed(): Promise< void > { const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); assertEquals(response.bodyUsed, false); assertThrows((): void => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (response as any).bodyUsed = true; }); await response.blob(); assertEquals(response.bodyUsed, true); }); unitTest( { perms: { net: true } }, async function fetchBodyUsedReader(): Promise { const response = await fetch( "http://localhost:4545/cli/tests/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); } ); unitTest( { perms: { net: true } }, async function fetchBodyUsedCancelStream(): Promise { const response = await fetch( "http://localhost:4545/cli/tests/fixture.json" ); assert(response.body !== null); assertEquals(response.bodyUsed, false); const promise = response.body.cancel(); assertEquals(response.bodyUsed, true); await promise; } ); unitTest({ perms: { net: true } }, async function fetchAsyncIterator(): Promise< void > { const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); const headers = response.headers; assert(response.body !== null); let total = 0; for await (const chunk of response.body) { total += chunk.length; } assertEquals(total, Number(headers.get("Content-Length"))); }); unitTest({ perms: { net: true } }, async function fetchBodyReader(): Promise< void > { const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); const headers = response.headers; 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, Number(headers.get("Content-Length"))); }); unitTest( { perms: { net: true } }, async function fetchBodyReaderBigBody(): Promise { const data = "a".repeat(10 << 10); // 10mb const response = await fetch( "http://localhost:4545/cli/tests/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); } ); unitTest({ perms: { net: true } }, async function responseClone(): Promise< void > { const response = await fetch("http://localhost:4545/cli/tests/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]); } }); unitTest({ perms: { net: true } }, async function fetchEmptyInvalid(): Promise< void > { await assertThrowsAsync(async () => { await fetch(""); }, URIError); }); unitTest( { perms: { net: true } }, async function fetchMultipartFormDataSuccess(): Promise { const response = await fetch( "http://localhost:4545/cli/tests/subdir/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")`); } ); unitTest( { perms: { net: true } }, async function fetchURLEncodedFormDataSuccess(): Promise { const response = await fetch( "http://localhost:4545/cli/tests/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(), ""); } ); unitTest( { perms: { net: true } }, async function fetchInitFormDataBinaryFileBody(): Promise { // Some random bytes // prettier-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); } ); unitTest( { perms: { net: true } }, async function fetchInitFormDataMultipleFilesBody(): Promise { const files = [ { // prettier-ignore content: new Uint8Array([137,80,78,71,13,10,26,10, 137, 1, 25]), type: "image/png", name: "image", fileName: "some-image.png", }, { // prettier-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 ); } } ); unitTest( { perms: { net: true }, }, async function fetchWithRedirection(): Promise { const response = await fetch("http://localhost:4546/"); // will redirect to http://localhost:4545/ assertEquals(response.status, 200); assertEquals(response.statusText, "OK"); assertEquals(response.url, "http://localhost:4545/"); const body = await response.text(); assert(body.includes("Directory listing for /")); } ); unitTest( { perms: { net: true }, }, async function fetchWithRelativeRedirection(): Promise { const response = await fetch("http://localhost:4545/cli/tests"); // will redirect to /cli/tests/ assertEquals(response.status, 200); assertEquals(response.statusText, "OK"); const body = await response.text(); assert(body.includes("Directory listing for /cli/tests/")); } ); unitTest( { perms: { net: true }, }, async function fetchWithInfRedirection(): Promise { const response = await fetch("http://localhost:4549/cli/tests"); // will redirect to the same place assertEquals(response.status, 0); // network error assertEquals(response.type, "error"); assertEquals(response.ok, false); } ); unitTest( { perms: { net: true } }, async function fetchInitStringBody(): Promise { 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")); } ); unitTest( { perms: { net: true } }, async function fetchRequestInitStringBody(): Promise { 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); } ); unitTest( { perms: { net: true } }, async function fetchInitTypedArrayBody(): Promise { 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); } ); unitTest( { perms: { net: true } }, async function fetchInitArrayBufferBody(): Promise { 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); } ); unitTest( { perms: { net: true } }, async function fetchInitURLSearchParamsBody(): Promise { 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") ); } ); unitTest({ perms: { net: true } }, async function fetchInitBlobBody(): Promise< void > { 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")); }); unitTest( { perms: { net: true } }, async function fetchInitFormDataBody(): Promise { 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")); } ); unitTest( { perms: { net: true } }, async function fetchInitFormDataBlobFilenameBody(): Promise { const form = new FormData(); form.append("field", "value"); form.append("file", new Blob([new TextEncoder().encode("deno")])); 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, "blob"); } ); unitTest( { perms: { net: true } }, async function fetchInitFormDataTextFileBody(): Promise { 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()); } ); unitTest({ perms: { net: true } }, async function fetchUserAgent(): Promise< void > { 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(); }); // TODO(ry) The following tests work but are flaky. There's a race condition // somewhere. Here is what one of these flaky failures looks like: // // unitTest fetchPostBodyString_permW0N1E0R0 // assertEquals failed. actual = expected = POST /blah HTTP/1.1 // hello: World // foo: Bar // host: 127.0.0.1:4502 // content-length: 11 // hello world // Error: actual: expected: POST /blah HTTP/1.1 // hello: World // foo: Bar // host: 127.0.0.1:4502 // content-length: 11 // hello world // at Object.assertEquals (file:///C:/deno/js/testing/util.ts:29:11) // at fetchPostBodyString (file function bufferServer(addr: string): Deno.Buffer { const [hostname, port] = addr.split(":"); const listener = Deno.listen({ hostname, port: Number(port), }) as Deno.Listener; const buf = new Deno.Buffer(); listener.accept().then(async (conn: Deno.Conn) => { 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; } unitTest( { perms: { net: true }, }, async function fetchRequest(): Promise { const addr = "127.0.0.1:4501"; const buf = 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(buf.bytes()); const expected = [ "POST /blah HTTP/1.1\r\n", "hello: World\r\n", "foo: Bar\r\n", "accept: */*\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); } ); unitTest( { perms: { net: true }, }, async function fetchPostBodyString(): Promise { const addr = "127.0.0.1:4502"; const buf = 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(buf.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", `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); } ); unitTest( { perms: { net: true }, }, async function fetchPostBodyTypedArray(): Promise { const addr = "127.0.0.1:4503"; const buf = 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(buf.bytes()); const expected = [ "POST /blah HTTP/1.1\r\n", "hello: World\r\n", "foo: Bar\r\n", "accept: */*\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); } ); unitTest( { perms: { net: true }, }, async function fetchWithManualRedirection(): Promise { const response = await fetch("http://localhost:4546/", { redirect: "manual", }); // will redirect to http://localhost:4545/ assertEquals(response.status, 0); assertEquals(response.statusText, ""); assertEquals(response.url, ""); assertEquals(response.type, "opaqueredirect"); try { await response.text(); fail( "Reponse.text() didn't throw on a filtered response without a body (type opaqueredirect)" ); } catch (e) { return; } } ); unitTest( { perms: { net: true }, }, async function fetchWithErrorRedirection(): Promise { const response = await fetch("http://localhost:4546/", { redirect: "error", }); // will redirect to http://localhost:4545/ assertEquals(response.status, 0); assertEquals(response.statusText, ""); assertEquals(response.url, ""); assertEquals(response.type, "error"); try { await response.text(); fail( "Reponse.text() didn't throw on a filtered response without a body (type error)" ); } catch (e) { return; } } ); unitTest(function responseRedirect(): void { const redir = Response.redirect("example.com/newLocation", 301); assertEquals(redir.status, 301); assertEquals(redir.statusText, ""); assertEquals(redir.url, ""); assertEquals(redir.headers.get("Location"), "example.com/newLocation"); assertEquals(redir.type, "default"); }); unitTest({ perms: { net: true } }, async function fetchBodyReadTwice(): Promise< void > { const response = await fetch("http://localhost:4545/cli/tests/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 } } }); unitTest( { perms: { net: true } }, async function fetchBodyReaderAfterRead(): Promise { const response = await fetch( "http://localhost:4545/cli/tests/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 } } ); unitTest( { perms: { net: true } }, async function fetchBodyReaderWithCancelAndNewReader(): Promise { const data = "a".repeat(1 << 10); const response = await fetch( "http://localhost:4545/cli/tests/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); } ); unitTest( { perms: { net: true } }, async function fetchBodyReaderWithReadCancelAndNewReader(): Promise { const data = "a".repeat(1 << 10); const response = await fetch( "http://localhost:4545/cli/tests/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); } ); unitTest( { perms: { net: true } }, async function fetchResourceCloseAfterStreamCancel(): Promise { const res = await fetch("http://localhost:4545/cli/tests/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(); } ); unitTest( { perms: { net: true } }, async function fetchNullBodyStatus(): Promise { 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/cli/tests/echo_server", { body: "deno", method: "POST", headers, }); assertEquals(res.body, null); assertEquals(res.status, status); } } ); unitTest( { perms: { net: true } }, function fetchResponseConstructorNullBody(): void { 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" ); } } } ); unitTest( { perms: { net: true } }, function fetchResponseConstructorInvalidStatus(): void { const invalidStatus = [101, 600, 199]; for (const status of invalidStatus) { try { new Response("deno", { status }); fail("Invalid status"); } catch (e) { assert(e instanceof RangeError); assertEquals( e.message, `The status provided (${status}) is outside the range [200, 599]` ); } } } );