diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 24982e5..498bf37 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,4 +24,4 @@ jobs: uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: ./coverage.lcov \ No newline at end of file + path-to-lcov: ./coverage.lcov diff --git a/README.md b/README.md index a1e62ed..249e271 100644 --- a/README.md +++ b/README.md @@ -6,53 +6,141 @@ HTTP Server testing library inspired by [node-supertest-fetch](https://github.com/jwalton/node-supertest-fetch). +## Install + +```sh +deno add jsr:@deno-libs/superfetch +``` + ## Example ### Basic testing +Pass a `Deno.ServeHandler` to `makeFetch` and it returns a `fetch` function +bound to a throwaway server hosting that handler. + ```ts import { describe, it } from 'jsr:@std/testing/bdd' import { makeFetch } from 'jsr:@deno-libs/superfetch' describe('makeFetch', () => { - it('should work with HTTP handler', async () => { - const handler = (res) => new Response('Hello World') + it('should work with an HTTP handler', async () => { + const handler: Deno.ServeHandler = () => new Response('Hello World') - const fetch = makeFetch(s) + const fetch = makeFetch(handler) const res = await fetch('/') - + res.expect('Hello World') }) +}) +``` + +### Assertions + +`expectStatus`, `expectHeader` and `expect` are chainable. `expectBody` ends a +chain. + +```ts +const handler: Deno.ServeHandler = () => + new Response('teapot', { + status: 418, + headers: { 'Content-Type': 'text/plain' }, + }) + +const fetch = makeFetch(handler) +const res = await fetch('/') + +res + .expectStatus(418) + .expectHeader('Content-Type', /text/) + .expectHeader('Coffee-Allowed', null) // assert a header is absent + .expectBody('teapot') +``` + +`expect` dispatches on its arguments: a number checks the status, two arguments +check a header, anything else checks the body. + +```ts +res.expect(418) // same as expectStatus(418) +res.expect('Content-Type', 'text/plain') // same as expectHeader(...) +res.expect('teapot') // same as expectBody('teapot') +``` + +The body is decoded from the response `Content-Type`: `application/json` is +parsed as JSON, `text/*` as text, anything else as an `ArrayBuffer`. + +```ts +const handler: Deno.ServeHandler = () => + new Response(JSON.stringify({ hello: 'world' }), { + headers: { 'Content-Type': 'application/json' }, + }) + +const res = await makeFetch(handler)('/') -run() +res.expectBody({ hello: 'world' }) +``` + +### Requests + +The first argument is a path, and the second is a regular +[`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). + +```ts +const res = await fetch('/users?limit=10', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'deno' }), +}) +``` + +A single `fetch` can be reused for as many requests as you like. + +```ts +const fetch = makeFetch(handler) + +for (const path of ['/', '/status', '/header']) { + const res = await fetch(path) + res.expectStatus(200) +} ``` ### Full access to Response and port +The returned object is a real `Response` with the assertion helpers and the port +attached. + ```ts -import { describe, it } from 'jsr:@std/testing/bdd' -import { makeFetch } from 'jsr:@deno-libs/superfetch' +const res = await fetch('/') -describe('makeFetch', () => { - it('should work with HTTP handler', async () => { - const handler = (res) => new Response('Hello World') +res.expect('Hello World') +console.log(res.status) // 200 +console.log(res.headers.get('Content-Type')) +console.log(res.port) // port the handler was served on +``` - const fetch = makeFetch(s) +Note that the body has already been read in order to run the assertions, so +`res.json()` / `res.text()` cannot be called again. - const res = await fetch('/') - - res.expect('Hello World') - console.log(res) // server response object - console.log(res.port) // port on which the server was listening - }) +### Testing an existing server + +`makeFetch` also accepts a `Deno.HttpServer`. The server is yours, so superfetch +leaves it running — shut it down when you are done. + +```ts +const server = Deno.serve({ port: 0, onListen: () => {} }, handler) + +const fetch = makeFetch(server) +const res = await fetch('/') + +res.expectStatus(200).expectBody('Hello World') -run() +await server.shutdown() ``` [gh-actions-img]: https://img.shields.io/github/actions/workflow/status/deno-libs/superfetch/main.yml?branch=master&style=for-the-badge&logo=github [cov]: https://coveralls.io/github/deno-libs/superfetch [github-actions]: https://github.com/deno-libs/superfetch/actions [cov-badge]: https://img.shields.io/coverallsCoverage/github/deno-libs/superfetch?style=for-the-badge -[docs-badge]: https://img.shields.io/github/v/release/deno-libs/superfetch?color=yellow&label=Docs&logo=deno&style=for-the-badge -[docs]: https://doc.deno.land/https/deno.land/x/superfetch/mod.ts +[docs-badge]: https://img.shields.io/jsr/v/@deno-libs/superfetch?color=yellow&label=Docs&logo=deno&style=for-the-badge +[docs]: https://jsr.io/@deno-libs/superfetch/doc diff --git a/deno.json b/deno.json index cf4fe9e..8a1bee4 100644 --- a/deno.json +++ b/deno.json @@ -10,9 +10,25 @@ "test": "deno test --allow-net --allow-read --allow-write --allow-env --coverage=coverage" }, "name": "@deno-libs/superfetch", - "version": "3.0.0", + "version": "4.0.0", "exports": { ".": "./mod.ts", "./types": "./types.ts" + }, + "publish": { + "exclude": [ + ".claude", + ".github", + ".vscode", + "**/*_test.ts" + ] + }, + "imports": { + "@deno-libs/gql": "jsr:@deno-libs/gql@^4.1.0", + "@std/assert": "jsr:@std/assert@^1.0.19", + "@std/expect": "jsr:@std/expect@^1.0.20", + "@std/media-types": "jsr:@std/media-types@^1.1.0", + "@std/testing": "jsr:@std/testing@^1.0.20", + "graphql": "npm:graphql@^16.10.0" } } diff --git a/deno.lock b/deno.lock index c02e83e..bfb2e1c 100644 --- a/deno.lock +++ b/deno.lock @@ -1,49 +1,65 @@ { - "version": "4", + "version": "5", "specifiers": { - "jsr:@std/assert@1.0.0": "1.0.0", - "jsr:@std/assert@1.0.0-rc.2": "1.0.0-rc.2", - "jsr:@std/expect@0.224.5": "0.224.5", + "jsr:@deno-libs/gql@^4.1.0": "4.1.0", + "jsr:@std/assert@^1.0.19": "1.0.19", + "jsr:@std/expect@^1.0.20": "1.0.20", "jsr:@std/http@0.224.5": "0.224.5", - "jsr:@std/internal@1": "1.0.5", - "jsr:@std/internal@^1.0.1": "1.0.5", - "jsr:@std/media-types@1.0.1": "1.0.1", - "jsr:@std/testing@0.225.3": "0.225.3", - "npm:graphql-http@1.22.0": "1.22.0_graphql@16.8.1", - "npm:graphql@16.8.1": "16.8.1", + "jsr:@std/internal@^1.0.12": "1.0.14", + "jsr:@std/internal@^1.0.14": "1.0.14", + "jsr:@std/media-types@^1.1.0": "1.1.0", + "jsr:@std/path@^1.1.6": "1.1.6", + "jsr:@std/testing@^1.0.20": "1.0.20", + "npm:graphql-http@1.22.4": "1.22.4_graphql@16.10.0", + "npm:graphql@16.10.0": "16.10.0", + "npm:graphql@^16.10.0": "16.10.0", "npm:xss@1.0.15": "1.0.15" }, "jsr": { - "@std/assert@1.0.0-rc.2": { - "integrity": "0484eab1d76b55fca1c3beaff485a274e67dd3b9f065edcbe70030dfc0b964d3", + "@deno-libs/gql@4.1.0": { + "integrity": "71900f98c278e84057abb877c1ec3f2a7ab957174443dee1bed533b86e21c412", "dependencies": [ - "jsr:@std/internal@1" + "jsr:@std/http", + "npm:graphql-http", + "npm:graphql@16.10.0", + "npm:xss" ] }, - "@std/assert@1.0.0": { - "integrity": "0e4f6d873f7f35e2a1e6194ceee39686c996b9e5d134948e644d35d4c4df2008", + "@std/assert@1.0.19": { + "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", "dependencies": [ - "jsr:@std/internal@^1.0.1" + "jsr:@std/internal@^1.0.12" ] }, - "@std/expect@0.224.5": { - "integrity": "ae6e57ee6f23a2fd5ae130cb84448eef182354cb902e1a2a98d64c72ac3a5a1c", + "@std/expect@1.0.20": { + "integrity": "ffc4f3c33f732417e3e9acf3d995818c89984313c37d933b9ebbb4571d2887e9", "dependencies": [ - "jsr:@std/assert@1.0.0-rc.2", - "jsr:@std/internal@1" + "jsr:@std/assert", + "jsr:@std/internal@^1.0.14", + "jsr:@std/path" ] }, "@std/http@0.224.5": { "integrity": "b03b5d1529f6c423badfb82f6640f9f2557b4034cd7c30655ba5bb447ff750a4" }, - "@std/internal@1.0.5": { - "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" + "@std/internal@1.0.14": { + "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" }, - "@std/media-types@1.0.1": { - "integrity": "f2ddc3497be0bd87ac0c9b9b26bb454f76bdc45e1b9a12146af47fab3ba2828c" + "@std/media-types@1.1.0": { + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" }, - "@std/testing@0.225.3": { - "integrity": "348c24d0479d44ab3dbb4f26170f242e19f24051b45935d4a9e7ca0ab7e37780" + "@std/path@1.1.6": { + "integrity": "c68485c2a4dfbb5ae3cc74fae4e8c4e5d874cf8a8ed12927917235c758b46cbe", + "dependencies": [ + "jsr:@std/internal@^1.0.14" + ] + }, + "@std/testing@1.0.20": { + "integrity": "21380ed438672762e4ec549cbf4fe41c5b68f5598773a30b64abe7375513e721", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/internal@^1.0.14" + ] } }, "npm": { @@ -53,28 +69,32 @@ "cssfilter@0.0.10": { "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" }, - "graphql-http@1.22.0_graphql@16.8.1": { - "integrity": "sha512-9RBUlGJWBFqz9LwfpmAbjJL/8j/HCNkZwPBU5+Bfmwez+1Ay43DocMNQYpIWsWqH0Ftv6PTNAh2aRnnMCBJgLw==", + "graphql-http@1.22.4_graphql@16.10.0": { + "integrity": "sha512-OC3ucK988teMf+Ak/O+ZJ0N2ukcgrEurypp8ePyJFWq83VzwRAmHxxr+XxrMpxO/FIwI4a7m/Fzv3tWGJv0wPA==", "dependencies": [ "graphql" ] }, - "graphql@16.8.1": { - "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==" + "graphql@16.10.0": { + "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==" }, "xss@1.0.15": { "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", "dependencies": [ "commander", "cssfilter" - ] + ], + "bin": true } }, - "remote": { - "https://deno.land/x/gql@3.0.1/deps.ts": "4daa527e6f98a6e2edb048d079a3a51f016db209497509343c3aed7448ceecd4", - "https://deno.land/x/gql@3.0.1/graphiql/markup.ts": "825b76ac088b3813c85da2061e4b4e59ae5f57e38761f66e8c941bbd65999afa", - "https://deno.land/x/gql@3.0.1/graphiql/render.ts": "d01d9462f392f84fe264feb88d00eecb5913802d01ae2db6dfd03514d62f4935", - "https://deno.land/x/gql@3.0.1/mod.ts": "ff13be68e3ad1170f987a4b0b3c1c506d2aaf688afad79d16a2ad55e4f8899c7", - "https://deno.land/x/gql@3.0.1/types.ts": "c9b933567b747ce041f6f0d3e9bba32d102af62ae17785fa0b97310407af8481" + "workspace": { + "dependencies": [ + "jsr:@deno-libs/gql@^4.1.0", + "jsr:@std/assert@^1.0.19", + "jsr:@std/expect@^1.0.20", + "jsr:@std/media-types@^1.1.0", + "jsr:@std/testing@^1.0.20", + "npm:graphql@^16.10.0" + ] } } diff --git a/deps.ts b/deps.ts index 03f95bc..45febe7 100644 --- a/deps.ts +++ b/deps.ts @@ -1,3 +1,3 @@ -export { assertEquals } from 'jsr:@std/assert@1.0.0/equals' -export { assertMatch } from 'jsr:@std/assert@1.0.0/match' -export { parseMediaType } from 'jsr:@std/media-types@1.0.1/parse-media-type' +export { assertEquals } from '@std/assert/equals' +export { assertMatch } from '@std/assert/match' +export { parseMediaType } from '@std/media-types/parse-media-type' diff --git a/e2e_test.ts b/e2e_test.ts index 7b376f3..80ec8cb 100644 --- a/e2e_test.ts +++ b/e2e_test.ts @@ -1,7 +1,7 @@ -import { describe, it } from 'jsr:@std/testing@0.225.3/bdd' +import { describe, it } from '@std/testing/bdd' import { makeFetch } from './mod.ts' -import { GraphQLHTTP } from 'https://deno.land/x/gql@3.0.1/mod.ts' -import { buildSchema } from 'npm:graphql@16.8.1' +import { GraphQLHTTP } from '@deno-libs/gql' +import { buildSchema } from 'graphql' describe('e2e', () => { it('should work with GraphQL', async () => { diff --git a/egg.json b/egg.json deleted file mode 100644 index bf9c368..0000000 --- a/egg.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://x.nest.land/eggs@0.3.10/src/schema.json", - "name": "superfetch", - "entry": "./mod.ts", - "description": "🐕 Superdeno-like superagent testing library based on Fetch API. Ported from node-supertest-fetch.", - "homepage": "https://github.com/deno-libs/superfetch", - "version": "1.0.2", - "releaseType": "patch", - "unstable": false, - "unlisted": false, - "files": [ - "./**/*.ts", - "README.md", - "LICENSE" - ], - "ignore": [], - "checkFormat": true, - "checkTests": "deno test --allow-net --allow-read", - "check": true, - "checkAll": true, - "repository": "https://github.com/deno-libs/superfetch" -} diff --git a/mod.ts b/mod.ts index 43279ef..2b3d4f9 100644 --- a/mod.ts +++ b/mod.ts @@ -5,12 +5,27 @@ import type { MakeFetchResponse, } from './types.ts' +/** + * Resolve a caller-supplied path or URL against the test server's origin. + * + * Only the path, query and hash are taken from `url` — the origin is always + * forced to the local test server, so `'/users?a=1'` and + * `new URL('http://example.com/users?a=1')` both reach the handler under test. + */ +const toLocalUrl = (port: number, url: string | URL) => { + const resolved = new URL(url, `http://localhost:${port}`) + resolved.protocol = 'http:' + resolved.hostname = 'localhost' + resolved.port = `${port}` + return resolved +} + const fetchEndpoint = async ( port: number, url: string | URL, params?: RequestInit, ) => { - const res = await fetch(`http://localhost:${port}${url}`, params) + const res = await fetch(toLocalUrl(port, url), params) let data: unknown const ct = res.headers.get('Content-Type') if (ct === null) return { data: await res.text(), res } @@ -21,35 +36,44 @@ const fetchEndpoint = async ( else data = await res.arrayBuffer() return { res, data } } -const makeFetchPromise = (handlerOrListener: HandlerOrListener) => { + +type Responder = ( + url?: URL | string, + params?: RequestInit, +) => Promise<{ res: Response; data?: unknown; port: number }> + +const makeFetchPromise = (handlerOrListener: HandlerOrListener): Responder => { + // The caller created this server and owns its lifecycle, so we only borrow + // it — shutting it down here would break any further use of it. if ('addr' in handlerOrListener) { - const port = handlerOrListener.addr.port - - const resp = (url: URL | string = '', params?: RequestInit) => { - return new Promise<{ res: Response; data?: unknown }>((resolve) => { - setTimeout(async () => { - const { res, data } = await fetchEndpoint(port, url, params) - resolve({ res, data }) - return handlerOrListener.shutdown() - }) - }) - } - return { resp, port } - } else { - const server = Deno.serve({ port: 0 }, handlerOrListener) - const resp = async (url: URL | string = '', params?: RequestInit) => { - const res = await fetchEndpoint(server.addr.port, url, params) + const { port } = handlerOrListener.addr + return async (url = '', params?: RequestInit) => ({ + ...await fetchEndpoint(port, url, params), + port, + }) + } + + // A bare handler is ours to host. Serve it per request rather than once per + // `makeFetch()`, so a single instance can be reused for more than one + // request and an unused instance never leaves a server listening. + return async (url = '', params?: RequestInit) => { + const server = Deno.serve( + { port: 0, onListen: () => {} }, + handlerOrListener, + ) + const { port } = server.addr + try { + return { ...await fetchEndpoint(port, url, params), port } + } finally { await server.shutdown() - return res } - return { resp, port: server.addr.port } } } export const makeFetch = (h: HandlerOrListener): FetchFunction => { - const { resp, port } = makeFetchPromise(h) + const resp = makeFetchPromise(h) async function fetch(url: string | URL, options?: RequestInit) { - const { res, data } = await resp(url, options) + const { res, data, port } = await resp(url, options) const expectStatus = (a: number, b?: string) => { assertEquals( diff --git a/mod_test.ts b/mod_test.ts index afd0b58..3388f43 100644 --- a/mod_test.ts +++ b/mod_test.ts @@ -1,7 +1,7 @@ -import { describe, it } from 'jsr:@std/testing@0.225.3/bdd' -import { expect } from 'jsr:@std/expect@0.224.5/expect' +import { describe, it } from '@std/testing/bdd' +import { expect } from '@std/expect/expect' import { makeFetch } from './mod.ts' -import { AssertionError } from 'jsr:@std/assert@1.0.0/assertion-error' +import { AssertionError } from '@std/assert/assertion-error' const tw = new TextDecoder() @@ -92,16 +92,10 @@ describe('expectStatus', () => { const fetch = makeFetch(handler) const res = await fetch('/') - try { - res.expectStatus(404) - } catch (e) { - expect(e instanceof AssertionError).toBe(true) - expect((e as Error).message).toMatch( - new RegExp( - 'Values are not equal: expected to have status code 404 but was 200', - ), - ) - } + expect(() => res.expectStatus(404)).toThrow(AssertionError) + expect(() => res.expectStatus(404)).toThrow( + 'expected to have status code 404 but was 200', + ) }) }) @@ -125,55 +119,39 @@ describe('expectHeader', () => { new Response('Hello World', { headers: { 'Content-Type': 'text/html' } }) const fetch = makeFetch(handler) const res = await fetch('/') - try { - res.expectHeader('Content-Type', 'text/plain') - } catch (e) { - expect(e instanceof AssertionError).toBe(true) - expect((e as Error).message).toMatch( - new RegExp( - 'Values are not equal: expected to have header Content-Type with value text/plain, got text/html', - ), - ) - } + expect(() => res.expectHeader('Content-Type', 'text/plain')).toThrow( + AssertionError, + ) + expect(() => res.expectHeader('Content-Type', 'text/plain')).toThrow( + 'expected to have header Content-Type with value text/plain, got text/html', + ) }) it('throws if does not match regex', async () => { const handler: Deno.ServeHandler = () => new Response('Hello World', { headers: { 'Content-Type': 'text/html' } }) const fetch = makeFetch(handler) const res = await fetch('/') - try { - res.expectHeader('Content-Type', /image/) - } catch (e) { - expect((e as Error).message).toEqual( - 'Expected actual: "text/html" to match: "/image/": expected header Content-Type to match regexp /image/, got text/html', - ) - } + expect(() => res.expectHeader('Content-Type', /image/)).toThrow( + 'expected header Content-Type to match regexp /image/, got text/html', + ) }) it('throws if header does not exist and regex is passed', async () => { const handler: Deno.ServeHandler = () => new Response('Hello World', { headers: undefined }) const fetch = makeFetch(handler) const res = await fetch('/') - try { - res.expectHeader('garbage-header', /image/) - } catch (e) { - expect((e as Error).message).toMatch( - new RegExp('expected header null to not be empty'), - ) - } + expect(() => res.expectHeader('garbage-header', /image/)).toThrow( + 'expected header null to not be empty', + ) }) it('throws if header does not exist and array is passed', async () => { const handler: Deno.ServeHandler = () => new Response('Hello World', { headers: undefined }) const fetch = makeFetch(handler) const res = await fetch('/') - try { + expect(() => res.expectHeader('garbage-header', ['content-type', 'content-length']) - } catch (e) { - expect((e as Error).message).toMatch( - new RegExp('expected header null to not be empty'), - ) - } + ).toThrow('expected header null to not be empty') }) it('can expect array of header values', async () => { const handler: Deno.ServeHandler = () => @@ -210,11 +188,7 @@ describe('expectBody', () => { const handler: Deno.ServeHandler = () => new Response('Hello World') const fetch = makeFetch(handler) const res = await fetch('/') - try { - res.expectBody('Hello World?') - } catch (e) { - expect(e).toBeDefined() - } + expect(() => res.expectBody('Hello World?')).toThrow(AssertionError) }) it('supports regex', async () => { const handler: Deno.ServeHandler = () => new Response('Hello World') @@ -250,11 +224,63 @@ describe('expect', () => { describe('Deno.HttpServer', () => { it('should accept an HTTP server', async () => { - const fetch = makeFetch( - Deno.serve({ port: 0 }, () => new Response('hello')), + const server = Deno.serve( + { port: 0, onListen: () => {} }, + () => new Response('hello'), ) + const fetch = makeFetch(server) const res = await fetch('/') res.expectStatus(200).expectBody('hello') + await server.shutdown() + }) + it('should leave the caller-owned server running between requests', async () => { + const server = Deno.serve( + { port: 0, onListen: () => {} }, + () => new Response('hello'), + ) + const fetch = makeFetch(server) + + for (const _ of [1, 2, 3]) { + const res = await fetch('/') + res.expectStatus(200).expectBody('hello') + expect(res.port).toBe(server.addr.port) + } + + await server.shutdown() + }) +}) + +describe('reuse', () => { + it('should serve more than one request from a single makeFetch', async () => { + const handler: Deno.ServeHandler = (req) => + new Response(new URL(req.url).pathname) + const fetch = makeFetch(handler) + + for (const path of ['/', '/status', '/header']) { + const res = await fetch(path) + res.expectStatus(200).expectBody(path) + } + }) + it('should not leave a server listening if never called', () => { + const handler: Deno.ServeHandler = () => new Response('hello') + makeFetch(handler) + }) +}) + +describe('url handling', () => { + it('should accept a URL object', async () => { + const handler: Deno.ServeHandler = (req) => + new Response(new URL(req.url).pathname) + const fetch = makeFetch(handler) + const res = await fetch(new URL('http://example.com/hello')) + res.expectStatus(200).expectBody('/hello') + }) + it('should preserve the query string and hash', async () => { + const handler: Deno.ServeHandler = (req) => + new Response(new URL(req.url).search) + const fetch = makeFetch(handler) + const res = await fetch('/search?q=deno&page=2') + res.expectStatus(200).expectBody('?q=deno&page=2') }) }) @@ -270,19 +296,4 @@ describe('Port randomness', () => { expect(res.port).not.toBe(port) l.close() }) - it('should throw error if free port cannot be found', async () => { - globalThis.Deno.listen = () => { - throw new Error('bad error!') - } - const handler: Deno.ServeHandler = () => - new Response('Hello World', { status: 200 }) - try { - const fetch = makeFetch(handler) - await fetch('/') - } catch (e) { - expect((e as Error).message).toMatch( - new RegExp('Unable to get free port'), - ) - } - }) })