From 6b85164848463c5092c662074c7bddd57bb627d6 Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Tue, 4 Aug 2026 15:31:02 -0400 Subject: [PATCH] chore: convert to ESM Adopts the toolchain the other conversions use: src/ layout, conditional exports, vitest + @hapi/oxc-plugin, Node 22 baseline. @hapi/hoek moves to ^12.0.0-rc.0 and @hapi/bourne to ^4.0.1; @hapi/boom stays at ^10.0.1 until its own conversion lands. src/index.d.ts changes export = to export default -- the former is a TS1203 error under NodeNext in a type: module package, and the runtime has always default-exported. The entry keeps its default export so consumers are unaffected. API.md is added to files and its examples converted from require(), which would throw ERR_REQUIRE_ESM against the published package. One added test covers a payload.js branch that Node 22's raised default stream highWaterMark left unreachable. --- .github/workflows/ci-module.yml | 16 +- .gitignore | 2 +- API.md | 127 +- lib/recorder.js | 40 - oxfmt.config.ts | 8 + oxlint.config.ts | 11 + package.json | 55 +- {lib => src}/index.d.ts | 243 ++-- {lib => src}/index.js | 308 ++--- {lib => src}/payload.js | 15 +- src/recorder.js | 36 + {lib => src}/tap.js | 18 +- test/index.js | 2162 ++++++++++++++----------------- test/index.ts | 134 +- tsconfig.json | 25 + vitest.config.ts | 24 + 16 files changed, 1556 insertions(+), 1668 deletions(-) delete mode 100755 lib/recorder.js create mode 100644 oxfmt.config.ts create mode 100644 oxlint.config.ts rename {lib => src}/index.d.ts (58%) rename {lib => src}/index.js (70%) rename {lib => src}/payload.js (83%) create mode 100755 src/recorder.js rename {lib => src}/tap.js (56%) create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 44369c8..49bcabb 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -1,13 +1,13 @@ name: ci on: - push: - branches: - - master - - next - pull_request: - workflow_dispatch: + push: + branches: + - master + - next + pull_request: + workflow_dispatch: jobs: - test: - uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-18-hapi-21 + test: + uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-22-hapi-21 diff --git a/.gitignore b/.gitignore index 8f679c9..af5a347 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ **/node_modules **/package-lock.json -coverage.* +coverage/ **/.DS_Store **/._* diff --git a/API.md b/API.md index 83db929..5690b71 100755 --- a/API.md +++ b/API.md @@ -1,18 +1,16 @@ ## Usage ```javascript -const Wreck = require('@hapi/wreck'); +import Wreck from '@hapi/wreck'; const example = async function () { - const { res, payload } = await Wreck.get('http://example.com'); console.log(payload.toString()); }; try { example(); -} -catch (ex) { +} catch (ex) { console.error(ex); } ``` @@ -20,7 +18,10 @@ catch (ex) { ### Advanced ```javascript -const Wreck = require('@hapi/wreck'); +import Http from 'node:http'; +import Https from 'node:https'; + +import Wreck from '@hapi/wreck'; const method = 'GET'; // GET, POST, PUT, DELETE const uri = '/'; @@ -31,41 +32,39 @@ const wreck = Wreck.defaults({ agents: { https: new Https.Agent({ maxSockets: 100 }), http: new Http.Agent({ maxSockets: 1000 }), - httpsAllowUnauthorized: new Https.Agent({ maxSockets: 100, rejectUnauthorized: false }) - } + httpsAllowUnauthorized: new Https.Agent({ maxSockets: 100, rejectUnauthorized: false }), + }, }); // cascading example -- does not alter `wreck` // inherits `headers` and `agents` specified above const wreckWithTimeout = wreck.defaults({ - timeout: 5 + timeout: 5, }); // all attributes are optional const options = { baseUrl: 'https://www.example.com', payload: readableStream || 'foo=bar' || Buffer.from('foo=bar'), - headers: { /* http headers */ }, + headers: {/* http headers */}, redirects: 3, beforeRedirect: (redirectMethod, statusCode, location, resHeaders, redirectOptions, next) => next(), redirected: function (statusCode, location, req) {}, - timeout: 1000, // 1 second, default: unlimited + timeout: 1000, // 1 second, default: unlimited maxBytes: 1048576, // 1 MB, default: unlimited rejectUnauthorized: true || false, - agent: null, // Node Core http.Agent + agent: null, // Node Core http.Agent secureProtocol: 'SSLv3_method', // The SSL method to use - ciphers: 'DES-CBC3-SHA' // The TLS ciphers to support + ciphers: 'DES-CBC3-SHA', // The TLS ciphers to support }; const example = async function () { - const promise = wreck.request(method, uri, options); try { const res = await promise; const body = await Wreck.read(res, options); console.log(body.toString()); - } - catch (err) { + } catch (err) { // Handle errors } }; @@ -76,7 +75,8 @@ If the request was already redirected, aborting the original request will not ab ### `defaults(options)` -Returns a *new* instance of Wreck which merges the provided `options` with those provided on a per-request basis. You can call defaults repeatedly to build up multiple clients. +Returns a _new_ instance of Wreck which merges the provided `options` with those provided on a per-request basis. You can call defaults repeatedly to build up multiple clients. + - `options` - Config object containing settings for both `request` and `read` operations as well as: - `agents` - an object that contains the agents for pooling connections with the following required keys: - `http` - an [HTTP Agent](http://nodejs.org/api/http.html#http_class_http_agent) instance. @@ -87,12 +87,13 @@ Returns a *new* instance of Wreck which merges the provided `options` with those ### `request(method, uri, [options])` Initiate an HTTP request. + - `method` - a string specifying the HTTP request method. Defaults to `'GET'`. - `uri` - the URI of the requested resource. - `options` - optional configuration object with the following keys: - - `agent` - Node Core [http.Agent](http://nodejs.org/api/http.html#http_class_http_agent). Defaults to either `wreck.agents.http` or `wreck.agents.https`. Setting to `false` disables agent pooling. + - `agent` - Node Core [http.Agent](http://nodejs.org/api/http.html#http_class_http_agent). Defaults to either `wreck.agents.http` or `wreck.agents.https`. Setting to `false` disables agent pooling. - `baseUrl` - fully qualified URL string used as the base URL. Most useful with `Wreck.defaults()` when making multiple requests to the same domain. For example, if `baseUrl` is `https://example.com/api/`, then requesting `/end/point?test=true` will fetch `https://example.com/end/point?test=true`. Any query string in the `baseUrl` will be overwritten with the query string in the `uri` When `baseUrl` is given, `uri` must also be a string. In order to retain the `/api/` portion of the `baseUrl` in the example, the `path` must not start with a leading `/` and the `baseUrl` must end with a trailing `/`. @@ -103,7 +104,6 @@ Initiate an HTTP request. - `resHeaders` - An object with the headers received as part of the redirection response. - `redirectOptions` - Options that will be applied to the redirect request. Changes to this object are applied to the redirection request. - `next` - the callback function called to perform the redirection using signature `function(err)`. Passing an error into callback will stop the redirect request. - - `ciphers` - [TLS](https://nodejs.org/api/tls.html#tls_modifying_the_default_tls_cipher_suite) list of TLS ciphers to override node's default. The possible values depend on your installation of OpenSSL. Read the official OpenSSL docs for possible [TLS_CIPHERS](https://www.openssl.org/docs/man1.0.2/apps/ciphers.html#CIPHER-LIST-FORMAT). - `headers` - an object containing the request headers. @@ -114,9 +114,9 @@ Initiate an HTTP request. - `redirected` - a function to call when a redirect was triggered, using the signature `function(statusCode, location, req)` where: - - `statusCode` - HTTP status code of the response that triggered the redirect. - - `location` - The redirected location string. - - `req` - The new [ClientRequest](http://nodejs.org/api/http.html#http_class_http_clientrequest) object which replaces the one initially returned. + - `statusCode` - HTTP status code of the response that triggered the redirect. + - `location` - The redirected location string. + - `req` - The new [ClientRequest](http://nodejs.org/api/http.html#http_class_http_clientrequest) object which replaces the one initially returned. - `redirectMethod` - override the HTTP method used when following 301 and 302 redirections. Defaults to the original method. @@ -136,7 +136,6 @@ Initiate an HTTP request. - `hints` - Optional `dns.lookup()` hints. - Returns a promise that resolves into a node response object. The promise has a `req` property which is the instance of the node.js [ClientRequest](http://nodejs.org/api/http.html#http_class_http_clientrequest) object. ### `read(response, options)` @@ -168,18 +167,21 @@ When using gunzip, HTTP headers `Content-Encoding`, `Content-Length`, `Content-R ### `get(uri, [options])` Convenience method for GET operations. + - `uri` - The URI of the requested resource. - `options` - Optional config object containing settings for both `request` and `read` operations. Returns a promise that resolves into an object with the following properties: + - `res` - The [HTTP Incoming Message](https://nodejs.org/api/http.html#http_class_http_incomingmessage) - object, which is a readable stream that has "ended" and contains no more data to read. + object, which is a readable stream that has "ended" and contains no more data to read. - `payload` - The payload in the form of a Buffer or (optionally) parsed JavaScript object (JSON). Throws any error that may have occurred during handling of the request or a Boom error object if the response has an error status code (i.e. 4xx or 5xx). If the error is a boom error object it will have the following properties in addition to the standard boom properties: + - `data.isResponseError` - boolean, indicates if the error is a result of an error response status code - `data.headers` - object containing the response headers - `data.payload` - the payload in the form of a Buffer or as a parsed object @@ -188,18 +190,21 @@ properties: ### `post(uri, [options])` Convenience method for POST operations. + - `uri` - The URI of the requested resource. - `options` - Optional config object containing settings for both `request` and `read` operations. Returns a promise that resolves into an object with the following properties: + - `res` - The [HTTP Incoming Message](https://nodejs.org/api/http.html#http_class_http_incomingmessage) - object, which is a readable stream that has "ended" and contains no more data to read. + object, which is a readable stream that has "ended" and contains no more data to read. - `payload` - The payload in the form of a Buffer or (optionally) parsed JavaScript object (JSON). Throws any error that may have occurred during handling of the request or a Boom error object if the response has an error status code (i.e. 4xx or 5xx). If the error is a boom error object it will have the following properties in addition to the standard boom properties: + - `data.isResponseError` - boolean, indicates if the error is a result of an error response status code - `data.headers` - object containing the response headers - `data.payload` - the payload in the form of a Buffer or as a parsed object @@ -208,18 +213,21 @@ properties: ### `patch(uri, [options])` Convenience method for PATCH operations. + - `uri` - The URI of the requested resource. - `options` - Optional config object containing settings for both `request` and `read` operations. Returns a promise that resolves into an object with the following properties: + - `res` - The [HTTP Incoming Message](https://nodejs.org/api/http.html#http_class_http_incomingmessage) - object, which is a readable stream that has "ended" and contains no more data to read. + object, which is a readable stream that has "ended" and contains no more data to read. - `payload` - The payload in the form of a Buffer or (optionally) parsed JavaScript object (JSON). Throws any error that may have occurred during handling of the request or a Boom error object if the response has an error status code (i.e. 4xx or 5xx). If the error is a boom error object it will have the following properties in addition to the standard boom properties: + - `data.isResponseError` - boolean, indicates if the error is a result of an error response status code - `data.headers` - object containing the response headers - `data.payload` - the payload in the form of a Buffer or as a parsed object @@ -228,18 +236,21 @@ properties: ### `put(uri, [options])` Convenience method for PUT operations. + - `uri` - The URI of the requested resource. - `options` - Optional config object containing settings for both `request` and `read` operations. Returns a promise that resolves into an object with the following properties: + - `res` - The [HTTP Incoming Message](https://nodejs.org/api/http.html#http_class_http_incomingmessage) - object, which is a readable stream that has "ended" and contains no more data to read. + object, which is a readable stream that has "ended" and contains no more data to read. - `payload` - The payload in the form of a Buffer or (optionally) parsed JavaScript object (JSON). Throws any error that may have occurred during handling of the request or a Boom error object if the response has an error status code (i.e. 4xx or 5xx). If the error is a boom error object it will have the following properties in addition to the standard boom properties: + - `data.isResponseError` - boolean, indicates if the error is a result of an error response status code - `data.headers` - object containing the response headers - `data.payload` - the payload in the form of a Buffer or as a parsed object @@ -248,18 +259,21 @@ properties: ### `delete(uri, [options])` Convenience method for DELETE operations. + - `uri` - The URI of the requested resource. - `options` - Optional config object containing settings for both `request` and `read` operations. Returns a promise that resolves into an object with the following properties: + - `res` - The [HTTP Incoming Message](https://nodejs.org/api/http.html#http_class_http_incomingmessage) - object, which is a readable stream that has "ended" and contains no more data to read. + object, which is a readable stream that has "ended" and contains no more data to read. - `payload` - The payload in the form of a Buffer or (optionally) parsed JavaScript object (JSON). Throws any error that may have occurred during handling of the request or a Boom error object if the response has an error status code (i.e. 4xx or 5xx). If the error is a boom error object it will have the following properties in addition to the standard boom properties: + - `data.isResponseError` - boolean, indicates if the error is a result of an error response status code - `data.headers` - object containing the response headers - `data.payload` - the payload in the form of a Buffer or as a parsed object @@ -269,11 +283,13 @@ properties: Creates a [readable stream](http://nodejs.org/api/stream.html#stream_class_stream_readable) for the provided payload and encoding. + - `payload` - The Buffer or string to be wrapped in a readable stream. - `encoding` - The encoding to use. Must be a valid Buffer encoding, such as 'utf8' or 'ascii'. + ```javascript const stream = Wreck.toReadableStream(Buffer.from('Hello', 'ascii'), 'ascii'); const read = stream.read(); @@ -282,13 +298,15 @@ const read = stream.read(); ### `parseCacheControl(field)` -Parses the provided *cache-control* request header value into an object containing +Parses the provided _cache-control_ request header value into an object containing a property for each directive and it's value. Boolean directives, such as "private" or "no-cache" will be set to the boolean `true`. + - `field` - The header cache control value to be parsed. + ```javascript const result = Wreck.parseCacheControl('private, max-age=0, no-cache'); // result.private -> true @@ -298,26 +316,27 @@ const result = Wreck.parseCacheControl('private, max-age=0, no-cache'); ### `agents` -An object containing the node agents used for pooling connections for `http` and `https`. The properties are `http`, `https`, and `httpsAllowUnauthorized` which is an `https` agent with `rejectUnauthorized` set to false. All agents have `maxSockets` configured to `Infinity`. They are each instances of the Node.js [Agent](http://nodejs.org/api/http.html#http_class_http_agent) and expose the standard properties. +An object containing the node agents used for pooling connections for `http` and `https`. The properties are `http`, `https`, and `httpsAllowUnauthorized` which is an `https` agent with `rejectUnauthorized` set to false. All agents have `maxSockets` configured to `Infinity`. They are each instances of the Node.js [Agent](http://nodejs.org/api/http.html#http_class_http_agent) and expose the standard properties. For example, the following code demonstrates changing `maxSockets` on the `http` agent. - ```js - const Wreck = require('@hapi/wreck'); +```js +import Wreck from '@hapi/wreck'; - Wreck.agents.http.maxSockets = 20; - ``` +Wreck.agents.http.maxSockets = 20; +``` Below is another example that sets the certificate details for all HTTPS requests. ```js -const HTTPS = require('https'); -const Wreck = require('@hapi/wreck'); +import Https from 'node:https'; + +import Wreck from '@hapi/wreck'; -Wreck.agents.https = new HTTPS.Agent({ +Wreck.agents.https = new Https.Agent({ cert, key, - ca + ca, }); ``` @@ -327,12 +346,12 @@ To enable events, use `Wreck.defaults({ events: true })`. Events are available v #### `preRequest` -The request event is emitted just before *wreck* creates a request. The +The request event is emitted just before _wreck_ creates a request. The handler should accept the following arguments `(uri, options)` where: - - `uri` - the result of `new URL(uri)`. This will provide information about - the resource requested. Also includes the headers and method. - - `options` - the options passed into the request function. This will include +- `uri` - the result of `new URL(uri)`. This will provide information about + the resource requested. Also includes the headers and method. +- `options` - the options passed into the request function. This will include a payload if there is one. Since the `preRequest` event executes on a global event handler, you can intercept @@ -340,30 +359,30 @@ and decorate `uri` and `options` before a request is created. #### `request` -The request event is emitted just after *wreck* creates a request. The handler should accept the following arguments `(req)` where: +The request event is emitted just after _wreck_ creates a request. The handler should accept the following arguments `(req)` where: - - `req` - the raw [`ClientRequest`](https://nodejs.org/api/http.html#http_class_http_clientrequest) object created from the `uri`, before `end` has been called. +- `req` - the raw [`ClientRequest`](https://nodejs.org/api/http.html#http_class_http_clientrequest) object created from the `uri`, before `end` has been called. Since the `request` event executes on a global event handler, you can intercept and add listeners to a request. #### `response` -The response event is always emitted for any request that *wreck* makes. The +The response event is always emitted for any request that _wreck_ makes. The handler should accept the following arguments `(err, details)` where: - - `err` - a Boom error - - `details` - object with the following properties +- `err` - a Boom error +- `details` - object with the following properties - `req` - the raw `ClientHttp` request object - `res` - the raw `IncomingMessage` response object - `start` - the time that the request was initiated - `uri` - the result of `new URL(uri)`. This will provide information about - the resource requested. Also includes the headers and method. - -This event is useful for logging all requests that go through *wreck*. The `err` -and `res` arguments can be undefined depending on if an error occurs. Please -be aware that if multiple modules are depending on the same cached *wreck* -module that this event can fire for each request made across all modules. The -`start` property is the timestamp when the request was started. This can be -useful for determining how long it takes *wreck* to get a response back and + the resource requested. Also includes the headers and method. + +This event is useful for logging all requests that go through _wreck_. The `err` +and `res` arguments can be undefined depending on if an error occurs. Please +be aware that if multiple modules are depending on the same cached _wreck_ +module that this event can fire for each request made across all modules. The +`start` property is the timestamp when the request was started. This can be +useful for determining how long it takes _wreck_ to get a response back and processed. diff --git a/lib/recorder.js b/lib/recorder.js deleted file mode 100755 index 5b67417..0000000 --- a/lib/recorder.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -const Stream = require('stream'); - -const Boom = require('@hapi/boom'); - - -const internals = {}; - - -module.exports = internals.Recorder = class extends Stream.Writable { - - constructor(options) { - - super(); - - this.settings = options; // No need to clone since called internally with new object - this.buffers = []; - this.length = 0; - } - - _write(chunk, encoding, next) { - - if (this.settings.maxBytes && - this.length + chunk.length > this.settings.maxBytes) { - - return this.emit('error', Boom.entityTooLarge('Payload content length greater than maximum allowed: ' + this.settings.maxBytes)); - } - - this.length = this.length + chunk.length; - this.buffers.push(chunk); - next(); - } - - collect() { - - const buffer = (this.buffers.length === 0 ? Buffer.alloc(0) : (this.buffers.length === 1 ? this.buffers[0] : Buffer.concat(this.buffers, this.length))); - return buffer; - } -}; diff --git a/oxfmt.config.ts b/oxfmt.config.ts new file mode 100644 index 0000000..12e357b --- /dev/null +++ b/oxfmt.config.ts @@ -0,0 +1,8 @@ +import DefaultOxfmtConfig from '@hapi/oxc-plugin/oxfmt'; +import { defineConfig } from 'oxfmt'; + +import type { OxfmtConfig } from 'oxfmt'; + +export default defineConfig({ + ...DefaultOxfmtConfig, +}) as OxfmtConfig; diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 0000000..fd4548e --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,11 @@ +import HapiRecommended from '@hapi/oxc-plugin/oxlint'; +import { defineConfig } from 'oxlint'; + +import type { OxlintConfig } from 'oxlint'; + +export default defineConfig({ + extends: [HapiRecommended], + env: { + ...HapiRecommended.env, + }, +}) as OxlintConfig; diff --git a/package.json b/package.json index e01bbc3..0a4d988 100755 --- a/package.json +++ b/package.json @@ -1,32 +1,51 @@ { "name": "@hapi/wreck", - "description": "HTTP Client Utilities", "version": "18.1.2", - "repository": "git://github.com/hapijs/wreck", - "main": "lib/index", - "types": "lib/index.d.ts", + "description": "HTTP Client Utilities", "keywords": [ - "utilities", + "client", "http", - "client" + "utilities" ], + "license": "BSD-3-Clause", + "repository": "git://github.com/hapijs/wreck", "files": [ - "lib" + "src", + "API.md", + "README.md" ], + "type": "module", + "types": "src/index.d.ts", + "exports": { + ".": { + "types": "./src/index.d.ts", + "default": "./src/index.js" + } + }, + "scripts": { + "test": "vitest run --coverage", + "typecheck": "tsc --noEmit", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "fmt": "oxfmt --check", + "fmt:fix": "oxfmt", + "check": "npm run lint && npm run fmt && npm run typecheck && npm test" + }, "dependencies": { "@hapi/boom": "^10.0.1", - "@hapi/bourne": "^3.0.0", - "@hapi/hoek": "^11.0.2" + "@hapi/bourne": "^4.0.1", + "@hapi/hoek": "^12.0.0-rc.0" }, "devDependencies": { - "@hapi/code": "^9.0.3", - "@hapi/lab": "^25.1.2", - "@types/node": "^17.0.31", - "typescript": "~4.6.4" - }, - "scripts": { - "test": "lab -t 100 -L -a @hapi/code -m 10000 -Y", - "test-cov-html": "lab -r html -o coverage.html -a @hapi/code -m 10000" + "@hapi/oxc-plugin": "^1.0.4", + "@types/node": "^22", + "@vitest/coverage-v8": "^4.1.10", + "oxfmt": "^0.61.0", + "oxlint": "^1.76.0", + "typescript": "^6.0.3", + "vitest": "^4.1.10" }, - "license": "BSD-3-Clause" + "engines": { + "node": ">=22" + } } diff --git a/lib/index.d.ts b/src/index.d.ts similarity index 58% rename from lib/index.d.ts rename to src/index.d.ts index 7f808be..453dce6 100755 --- a/lib/index.d.ts +++ b/src/index.d.ts @@ -3,173 +3,147 @@ import { EventEmitter } from 'events'; import * as Http from 'http'; import * as Https from 'https'; +import { LookupFunction } from 'node:net'; import * as Stream from 'stream'; import * as Url from 'url'; -import { LookupFunction } from "node:net" import { Boom } from '@hapi/boom'; - -/** - * An HTTP request client. - */ +/** An HTTP request client. */ declare class Client { - - /** - * An object containing the node agents used for pooling connections for `http` and `https`. - */ + /** An object containing the node agents used for pooling connections for `http` and `https`. */ agents: Client.Agents; - /** - * An event emitter used to deliver events when the `events` option is set. - */ + /** An event emitter used to deliver events when the `events` option is set. */ events?: Client.Events; /** * Creates a new client. - * - * @param options - the client default options. + * + * @param options - The client default options. */ constructor(options?: Client.Options); /** * Creates a new client using the current client options as defaults and the provided options as override. - * - * @param options - the client override options. - * - * @returns a new client. + * + * @param options - The client override options. + * @returns A new client. */ defaults(options: Client.Options): Client; /** * Request an HTTP resource. - * - * @param method - a string specifying the HTTP request method. Defaults to 'GET'. - * @param url - the URI of the requested resource. - * @param options - default options override. - * - * @returns a promise resolving into an HTTP response object with a 'req' property holding a reference to the HTTP request object. + * + * @param method - A string specifying the HTTP request method. Defaults to 'GET'. + * @param url - The URI of the requested resource. + * @param options - Default options override. + * @returns A promise resolving into an HTTP response object with a 'req' property holding a reference to the HTTP + * request object. */ - request(method: string, url: string, options?: Client.request.Options): Promise & { req: Http.ClientRequest }; + request( + method: string, + url: string, + options?: Client.request.Options, + ): Promise & { req: Http.ClientRequest }; /** * Reads a readable stream and returns the parsed payload. - * - * @param res - the readable stream. - * @param options - default options override. - * - * @returns the parsed payload based on the provided options. + * + * @param res - The readable stream. + * @param options - Default options override. + * @returns The parsed payload based on the provided options. */ read(res: Stream.Readable | Http.IncomingMessage, options?: Client.read.Options): Promise; /** * Converts a buffer, string, or an array of them into a readable stream. - * - * @param payload - a string, buffer, or an array of them. - * @param encoding - the payload encoding. - * - * @returns a readable stream. + * + * @param payload - A string, buffer, or an array of them. + * @param encoding - The payload encoding. + * @returns A readable stream. */ toReadableStream(payload: Client.toReadableStream.Payload, encoding?: string): Stream.Readable; /** * Parses the HTTP Cache-Control header. - * - * @param field - the header content. - * - * @returns an object with the header parameters or null if invalid. + * + * @param field - The header content. + * @returns An object with the header parameters or null if invalid. */ parseCacheControl(field: string): Client.parseCacheControl.Parameters | null; /** * Performs an HTTP GET request. - * - * @param uri - the resource URI. - * @param options - default options override. - * - * @returns the received payload Buffer or parsed payload based on the options. + * + * @param uri - The resource URI. + * @param options - Default options override. + * @returns The received payload Buffer or parsed payload based on the options. */ get(uri: string, options?: Client.request.Options & Client.read.Options): Promise>; /** * Performs an HTTP POST request. * - * @param uri - the resource URI. - * @param options - default options override. - * - * @returns the received payload Buffer or parsed payload based on the options. + * @param uri - The resource URI. + * @param options - Default options override. + * @returns The received payload Buffer or parsed payload based on the options. */ post(uri: string, options?: Client.request.Options & Client.read.Options): Promise>; /** * Performs an HTTP PATCH request. * - * @param uri - the resource URI. - * @param options - default options override. - * - * @returns the received payload Buffer or parsed payload based on the options. + * @param uri - The resource URI. + * @param options - Default options override. + * @returns The received payload Buffer or parsed payload based on the options. */ patch(uri: string, options?: Client.request.Options & Client.read.Options): Promise>; /** * Performs an HTTP PUT request. * - * @param uri - the resource URI. - * @param options - default options override. - * - * @returns the received payload Buffer or parsed payload based on the options. + * @param uri - The resource URI. + * @param options - Default options override. + * @returns The received payload Buffer or parsed payload based on the options. */ put(uri: string, options?: Client.request.Options & Client.read.Options): Promise>; /** * Performs an HTTP DELETE request. * - * @param uri - the resource URI. - * @param options - default options override. - * - * @returns the received payload Buffer or parsed payload based on the options. + * @param uri - The resource URI. + * @param options - Default options override. + * @returns The received payload Buffer or parsed payload based on the options. */ delete(uri: string, options?: Client.request.Options & Client.read.Options): Promise>; } - declare namespace Client { - interface Options extends request.Options, read.Options { - - /** - * An object containing the node agents used for pooling connections for `http` and `https`. - */ + /** An object containing the node agents used for pooling connections for `http` and `https`. */ readonly agents?: Agents; /** * Enables events. - * + * * @default false */ readonly events?: boolean; } interface Agents { - - /** - * The agent used for HTTP requests. - */ + /** The agent used for HTTP requests. */ readonly http: Http.Agent; - /** - * The agent used for HTTPS requests. - */ + /** The agent used for HTTPS requests. */ readonly https: Https.Agent; - /** - * The agent used for HTTPS requests which ignores unauthorized requests. - */ + /** The agent used for HTTPS requests which ignores unauthorized requests. */ readonly httpsAllowUnauthorized: Https.Agent; } class Events extends EventEmitter { - on(event: 'preRequest', litener: Events.preRequest): this; once(event: 'preRequest', litener: Events.preRequest): this; addListener(event: 'preRequest', litener: Events.preRequest): this; @@ -184,121 +158,111 @@ declare namespace Client { } namespace Events { - type preRequest = (uri: string, options: Client.Options) => void; type request = (req: Http.ClientRequest) => void; - type response = (err: Boom | undefined, details: { req: Http.ClientRequest, res: Http.IncomingMessage | undefined, start: number, url: Url.URL }) => void; + type response = ( + err: Boom | undefined, + details: { req: Http.ClientRequest; res: Http.IncomingMessage | undefined; start: number; url: Url.URL }, + ) => void; } namespace request { - interface Options { - - /** - * Node HTTP or HTTPS Agent object (false disables agent pooling). - */ + /** Node HTTP or HTTPS Agent object (false disables agent pooling). */ readonly agent?: Http.Agent | Https.Agent | false; - /** - * Custom lookup function. Default: dns.lookup(). - */ + /** Custom lookup function. Default: dns.lookup(). */ readonly lookup?: LookupFunction; /** - * IP address family to use when resolving host or hostname. Valid values are 4 or 6. When unspecified, both IP v4 and v6 will be used. + * IP address family to use when resolving host or hostname. Valid values are 4 or 6. When unspecified, both + * IP v4 and v6 will be used. */ readonly family?: number; - /** - * Optional dns.lookup() hints. - */ + /** Optional dns.lookup() hints. */ readonly hints?: number; - /** - * Fully qualified URL string used as the base URL. - */ + /** Fully qualified URL string used as the base URL. */ readonly baseUrl?: string; /** * A function to call before a redirect is triggered. - * - * @param redirectMethod - a string specifying the redirect method. + * + * @param redirectMethod - A string specifying the redirect method. * @param statusCode - HTTP status code of the response that triggered the redirect. * @param location - The redirect location string. * @param resHeaders - An object with the headers received as part of the redirection response. - * @param redirectOptions - Options that will be applied to the redirect request. Changes to this object are applied to the redirection request. - * @param next - the callback function called to perform the redirection. - */ - readonly beforeRedirect?: (redirectMethod: string, statusCode: number, location: string, resHeaders: Record, redirectOptions: Client.request.Options, next: () => void) => void; - - /** - * TLS list of TLS ciphers to override node's default. + * @param redirectOptions - Options that will be applied to the redirect request. Changes to this object are + * applied to the redirection request. + * @param next - The callback function called to perform the redirection. */ + readonly beforeRedirect?: ( + redirectMethod: string, + statusCode: number, + location: string, + resHeaders: Record, + redirectOptions: Client.request.Options, + next: () => void, + ) => void; + + /** TLS list of TLS ciphers to override node's default. */ readonly ciphers?: string; - /** - * An object containing the request headers. - */ + /** An object containing the request headers. */ readonly headers?: Record; /** * Determines how to handle gzipped payloads. - * + * * @default false */ readonly gunzip?: boolean | 'force'; /** - * The request body as a string, Buffer, readable stream, or an object that can be serialized using `JSON.stringify()`. + * The request body as a string, Buffer, readable stream, or an object that can be serialized using + * `JSON.stringify()`. */ readonly payload?: Payload; /** * Enables redirects on 303 responses (using GET). - * + * * @default false */ readonly redirect303?: boolean; - /** - * Overrides the HTTP method used when following 301 and 302 redirections. Defaults to the original method. - */ + /** Overrides the HTTP method used when following 301 and 302 redirections. Defaults to the original method. */ readonly redirectMethod?: string; /** * The maximum number of redirects to follow. - * + * * @default false */ readonly redirects?: number | false; /** * A function to call when a redirect was triggered. - * + * * @param statusCode - HTTP status code of the response that triggered the redirect. - * @param location - the redirected location string. - * @param req - the new ClientRequest object which replaces the one initially returned. + * @param location - The redirected location string. + * @param req - The new ClientRequest object which replaces the one initially returned. */ readonly redirected?: (statusCode: number, location: string, req: Http.ClientRequest) => void; - /** - * TLS flag indicating whether the client should reject a response from a server with invalid certificates. - */ + /** TLS flag indicating whether the client should reject a response from a server with invalid certificates. */ readonly rejectUnauthorized?: boolean; - /** - * TLS flag indicating the SSL method to use, e.g. `SSLv3_method` to force SSL version 3. - */ + /** TLS flag indicating the SSL method to use, e.g. `SSLv3_method` to force SSL version 3. */ readonly secureProtocol?: string; - /** - * A UNIX socket path string for direct server connection. - */ + /** A UNIX socket path string for direct server connection. */ readonly socketPath?: string; /** * Number of milliseconds to wait without receiving a response before aborting the request. - * + * * @default 0 */ readonly timeout?: number; @@ -307,38 +271,33 @@ declare namespace Client { type Payload = string | Buffer | Stream.Readable | object; interface Response { - res: Http.IncomingMessage; payload: T; } } namespace read { - interface Options { - /** - * Determines how to handle gzipped payloads. - * - * @default false - */ + * Determines how to handle gzipped payloads. + * + * @default false + */ readonly gunzip?: boolean | 'force'; - /** - * Determines how to parse the payload as JSON. - */ + /** Determines how to parse the payload as JSON. */ readonly json?: boolean | 'strict' | 'force'; /** * The maximum allowed response payload size. - * + * * @default 0 */ readonly maxBytes?: number; /** * The number of milliseconds to wait while reading data before aborting handling of the response. - * + * * @default 0 */ readonly timeout?: number; @@ -346,21 +305,17 @@ declare namespace Client { } namespace toReadableStream { - type Item = string | Buffer; type Payload = Item | Item[]; } namespace parseCacheControl { - interface Parameters { - 'max-age'?: number; [key: string]: string | number | undefined; } } } - declare const client: Client; -export = client; +export default client; diff --git a/lib/index.js b/src/index.js similarity index 70% rename from lib/index.js rename to src/index.js index 369d951..b3cdae7 100755 --- a/lib/index.js +++ b/src/index.js @@ -1,43 +1,38 @@ -'use strict'; +import * as Events from 'node:events'; +import * as Http from 'node:http'; +import * as Https from 'node:https'; +import * as Stream from 'node:stream'; +import * as Url from 'node:url'; +import * as Zlib from 'node:zlib'; -const Events = require('events'); -const Http = require('http'); -const Https = require('https'); -const Stream = require('stream'); -const Url = require('url'); -const Zlib = require('zlib'); +import * as Boom from '@hapi/boom'; +import * as Bourne from '@hapi/bourne'; +import * as Hoek from '@hapi/hoek'; -const Boom = require('@hapi/boom'); -const Bourne = require('@hapi/bourne'); -const Hoek = require('@hapi/hoek'); +import { Payload } from './payload.js'; +import { Recorder } from './recorder.js'; +import { Tap } from './tap.js'; -const Payload = require('./payload'); -const Recorder = require('./recorder'); -const Tap = require('./tap'); +const jsonRegex = /^application\/([a-z0-9.]*[+-]json|json)$/; +const shallowOptions = ['agent', 'agents', 'beforeRedirect', 'payload', 'redirected']; +const httpOptions = ['secureProtocol', 'ciphers', 'lookup', 'family', 'hints']; +const sensitiveCrossHostHeaders = new Set(['authorization', 'cookie', 'proxy-authorization']); +// New instance is exported as default export -const internals = { - jsonRegex: /^application\/([a-z0-9.]*[+-]json|json)$/, - shallowOptions: ['agent', 'agents', 'beforeRedirect', 'payload', 'redirected'], - httpOptions: ['secureProtocol', 'ciphers', 'lookup', 'family', 'hints'], - sensitiveCrossHostHeaders: new Set(['authorization', 'cookie', 'proxy-authorization']) -}; - - -// New instance is exported as module.exports - -internals.Client = class { - +class Client { constructor(options = {}) { + Hoek.assert( + !options.agents || (options.agents.https && options.agents.http && options.agents.httpsAllowUnauthorized), + 'Option agents must include "http", "https", and "httpsAllowUnauthorized"', + ); - Hoek.assert(!options.agents || options.agents.https && options.agents.http && options.agents.httpsAllowUnauthorized, 'Option agents must include "http", "https", and "httpsAllowUnauthorized"'); - - this._defaults = Hoek.clone(options, { shallow: internals.shallowOptions }); + this._defaults = Hoek.clone(options, { shallow: shallowOptions }); this.agents = this._defaults.agents || { https: new Https.Agent({ maxSockets: Infinity }), http: new Http.Agent({ maxSockets: Infinity }), - httpsAllowUnauthorized: new Https.Agent({ maxSockets: Infinity, rejectUnauthorized: false }) + httpsAllowUnauthorized: new Https.Agent({ maxSockets: Infinity, rejectUnauthorized: false }), }; if (this._defaults.events) { @@ -46,74 +41,83 @@ internals.Client = class { } defaults(options) { - Hoek.assert(options && typeof options === 'object', 'options must be provided to defaults'); - options = Hoek.applyToDefaults(this._defaults, options, { shallow: internals.shallowOptions }); - return new internals.Client(options); + options = Hoek.applyToDefaults(this._defaults, options, { shallow: shallowOptions }); + return new Client(options); } request(method, url, options = {}) { - try { - options = Hoek.applyToDefaults(this._defaults, options, { shallow: internals.shallowOptions }); - - Hoek.assert(options.payload === undefined || typeof options.payload === 'string' || typeof options.payload === 'object', 'options.payload must be a string, a Buffer, a Stream, or an Object'); - Hoek.assert(internals.isNullOrUndefined(options.agent) || typeof options.rejectUnauthorized !== 'boolean', 'options.agent cannot be set to an Agent at the same time as options.rejectUnauthorized is set'); - Hoek.assert(internals.isNullOrUndefined(options.beforeRedirect) || typeof options.beforeRedirect === 'function', 'options.beforeRedirect must be a function'); - Hoek.assert(internals.isNullOrUndefined(options.redirected) || typeof options.redirected === 'function', 'options.redirected must be a function'); - Hoek.assert(options.gunzip === undefined || typeof options.gunzip === 'boolean' || options.gunzip === 'force', 'options.gunzip must be a boolean or "force"'); - } - catch (err) { + options = Hoek.applyToDefaults(this._defaults, options, { shallow: shallowOptions }); + + Hoek.assert( + options.payload === undefined || + typeof options.payload === 'string' || + typeof options.payload === 'object', + 'options.payload must be a string, a Buffer, a Stream, or an Object', + ); + Hoek.assert( + isNullOrUndefined(options.agent) || typeof options.rejectUnauthorized !== 'boolean', + 'options.agent cannot be set to an Agent at the same time as options.rejectUnauthorized is set', + ); + Hoek.assert( + isNullOrUndefined(options.beforeRedirect) || typeof options.beforeRedirect === 'function', + 'options.beforeRedirect must be a function', + ); + Hoek.assert( + isNullOrUndefined(options.redirected) || typeof options.redirected === 'function', + 'options.redirected must be a function', + ); + Hoek.assert( + options.gunzip === undefined || typeof options.gunzip === 'boolean' || options.gunzip === 'force', + 'options.gunzip must be a boolean or "force"', + ); + } catch (err) { return Promise.reject(err); } if (options.baseUrl) { - url = internals.resolveUrl(options.baseUrl, url); + url = resolveUrl(options.baseUrl, url); delete options.baseUrl; } const relay = {}; const req = this._request(method, url, options, relay); - const promise = new Promise((resolve, reject) => { - - relay.callback = (err, res) => { + const { promise, resolve, reject } = Promise.withResolvers(); - if (err) { - reject(err); - return; - } - - resolve(res); + relay.callback = (err, res) => { + if (err) { + reject(err); return; - }; - }); + } + + resolve(res); + }; promise.req = req; return promise; } _request(method, url, options, relay, _trace) { - const uri = {}; if (options.socketPath) { uri.socketPath = options.socketPath; const parsedUri = new Url.URL(url, `unix://${options.socketPath}`); - internals.applyUrlToOptions(uri, { - host: '', // host must be empty according to https://tools.ietf.org/html/rfc2616#section-14.23 + applyUrlToOptions(uri, { + host: '', // host must be empty according to https://tools.ietf.org/html/rfc2616#section-14.23 protocol: 'http:', hash: parsedUri.hash, search: parsedUri.search, searchParams: parsedUri.searchParams, pathname: parsedUri.pathname, - href: parsedUri.href + href: parsedUri.href, }); - } - else { + } else { uri.setHost = false; const parsedUri = new Url.URL(url); - internals.applyUrlToOptions(uri, parsedUri); + applyUrlToOptions(uri, parsedUri); } uri.method = method.toUpperCase(); @@ -133,51 +137,51 @@ internals.Client = class { uri.headers.host = uri.host; } - if (options.payload && typeof options.payload === 'object' && !(options.payload instanceof Stream) && !Buffer.isBuffer(options.payload)) { + if ( + options.payload && + typeof options.payload === 'object' && + !(options.payload instanceof Stream.default) && + !Buffer.isBuffer(options.payload) + ) { options.payload = JSON.stringify(options.payload); if (!usedHeaders.has('content-type')) { uri.headers['content-type'] = 'application/json'; } } - if (options.gunzip && - !usedHeaders.has('accept-encoding')) { - + if (options.gunzip && !usedHeaders.has('accept-encoding')) { uri.headers['accept-encoding'] = 'gzip'; } - const payloadSupported = uri.method !== 'GET' && uri.method !== 'HEAD' && !internals.isNullOrUndefined(options.payload); - if (payloadSupported && + const payloadSupported = uri.method !== 'GET' && uri.method !== 'HEAD' && !isNullOrUndefined(options.payload); + if ( + payloadSupported && (typeof options.payload === 'string' || Buffer.isBuffer(options.payload)) && - !usedHeaders.has('content-length')) { - - uri.headers['content-length'] = Buffer.isBuffer(options.payload) ? options.payload.length : Buffer.byteLength(options.payload); + !usedHeaders.has('content-length') + ) { + uri.headers['content-length'] = Buffer.isBuffer(options.payload) + ? options.payload.length + : Buffer.byteLength(options.payload); } - let redirects = options.hasOwnProperty('redirects') ? options.redirects : false; // Needed to allow 0 as valid value when passed recursively + let redirects = Object.hasOwn(options, 'redirects') ? options.redirects : false; // Needed to allow 0 as valid value when passed recursively _trace = _trace ?? []; _trace.push({ method: uri.method, url }); const client = uri.protocol === 'https:' ? Https : Http; - for (const option of internals.httpOptions) { + for (const option of httpOptions) { if (options[option] !== undefined) { uri[option] = options[option]; } } - if (options.rejectUnauthorized !== undefined && - uri.protocol === 'https:') { - + if (options.rejectUnauthorized !== undefined && uri.protocol === 'https:') { uri.agent = options.rejectUnauthorized ? this.agents.https : this.agents.httpsAllowUnauthorized; - } - else if (options.agent || - options.agent === false) { - + } else if (options.agent || options.agent === false) { uri.agent = options.agent; - } - else { + } else { uri.agent = uri.protocol === 'https:' ? this.agents.https : this.agents.http; } @@ -188,17 +192,15 @@ internals.Client = class { this._emit('request', req); - let shadow = null; // A copy of the streamed request payload when redirects are enabled + let shadow = null; // A copy of the streamed request payload when redirects are enabled let timeoutId; const onError = (err) => { - err.trace = _trace; return finishOnce(Boom.badGateway('Client request error', err)); }; const onAbort = () => { - if (!req.socket) { // Fake an ECONNRESET error on early abort @@ -211,15 +213,12 @@ internals.Client = class { req.once('error', onError); const onResponse = (res) => { - // Pass-through response const statusCode = res.statusCode; - const redirectMethod = internals.redirectMethod(statusCode, uri.method, options); - - if (redirects === false || - !redirectMethod) { + const redirectMethod = resolveRedirectMethod(statusCode, uri.method, options); + if (redirects === false || !redirectMethod) { return finishOnce(null, res); } @@ -240,13 +239,13 @@ internals.Client = class { location = new Url.URL(location, uri.href).href; } - const redirectOptions = Hoek.clone(options, { shallow: internals.shallowOptions }); - redirectOptions.payload = shadow ?? options.payload; // shadow must be ready at this point if set + const redirectOptions = Hoek.clone(options, { shallow: shallowOptions }); + redirectOptions.payload = shadow ?? options.payload; // shadow must be ready at this point if set redirectOptions.redirects = --redirects; if (timeoutId) { clearTimeout(timeoutId); const elapsed = Date.now() - start; - redirectOptions.timeout = (redirectOptions.timeout - elapsed).toString(); // stringify to not drop timeout when === 0 + redirectOptions.timeout = (redirectOptions.timeout - elapsed).toString(); // stringify to not drop timeout when === 0 } // When redirecting cross-origin (scheme, host, or port differs), remove sensitive credential headers @@ -254,7 +253,7 @@ internals.Client = class { const parsedLocation = new URL(location); if (uri.origin !== parsedLocation.origin) { for (const header of Object.keys(redirectOptions.headers)) { - if (internals.sensitiveCrossHostHeaders.has(header.toLowerCase())) { + if (sensitiveCrossHostHeaders.has(header.toLowerCase())) { delete redirectOptions.headers[header]; } } @@ -262,13 +261,18 @@ internals.Client = class { } const followRedirect = (err) => { - if (err) { err.trace = _trace; return finishOnce(Boom.badGateway('Invalid redirect', err)); } - const redirectReq = this._request(redirectMethod, location, redirectOptions, { callback: finishOnce }, _trace); + const redirectReq = this._request( + redirectMethod, + location, + redirectOptions, + { callback: finishOnce }, + _trace, + ); if (options.redirected) { options.redirected(statusCode, location, redirectReq); } @@ -278,13 +282,19 @@ internals.Client = class { return followRedirect(); } - return options.beforeRedirect(redirectMethod, statusCode, location, res.headers, redirectOptions, followRedirect); + return options.beforeRedirect( + redirectMethod, + statusCode, + location, + res.headers, + redirectOptions, + followRedirect, + ); }; // Register handlers const finish = (err, res) => { - if (err) { req.abort(); } @@ -314,20 +324,19 @@ internals.Client = class { // Write payload if (payloadSupported) { - if (options.payload instanceof Stream) { + if (options.payload instanceof Stream.default) { let stream = options.payload; if (redirects) { const collector = new Tap(); collector.once('finish', () => { - shadow = collector.collect(); }); stream = options.payload.pipe(collector); } - internals.deferPipeUntilSocketConnects(req, stream); + deferPipeUntilSocketConnects(req, stream); return req; } @@ -341,18 +350,14 @@ internals.Client = class { } _emit(...args) { - if (this.events) { this.events.emit(...args); } } read(res, options = {}) { - return new Promise((resolve, reject) => { - this._read(res, options, (err, payload) => { - if (err) { reject(err); return; @@ -365,15 +370,13 @@ internals.Client = class { } _read(res, options, callback) { - - options = Hoek.applyToDefaults(this._defaults, options, { shallow: internals.shallowOptions }); + options = Hoek.applyToDefaults(this._defaults, options, { shallow: shallowOptions }); // Finish once let clientTimeoutId = null; const finish = (err, buffer) => { - clearTimeout(clientTimeoutId); reader.removeListener('error', onReaderError); reader.removeListener('finish', onReaderFinish); @@ -393,7 +396,7 @@ internals.Client = class { // Parse JSON if (options.json === 'force') { - return internals.tryParseBuffer(buffer, callback); + return tryParseBuffer(buffer, callback); } // 'strict' or true @@ -401,7 +404,7 @@ internals.Client = class { const contentType = res.headers?.['content-type'] ?? ''; const mime = contentType.split(';')[0].trim().toLowerCase(); - if (!internals.jsonRegex.test(mime)) { + if (!jsonRegex.test(mime)) { if (options.json === 'strict') { return callback(Boom.notAcceptable('The content-type is not JSON compatible')); } @@ -409,27 +412,23 @@ internals.Client = class { return callback(null, buffer); } - return internals.tryParseBuffer(buffer, callback); + return tryParseBuffer(buffer, callback); }; const finishOnce = Hoek.once(finish); const clientTimeout = options.timeout; - if (clientTimeout && - clientTimeout > 0) { - + if (clientTimeout && clientTimeout > 0) { clientTimeoutId = setTimeout(() => finishOnce(Boom.clientTimeout()), clientTimeout); } // Hander errors const onResError = (err) => { - return finishOnce(err.isBoom ? err : Boom.internal('Payload stream error', err)); }; const onResAborted = () => { - if (!res.complete) { finishOnce(Boom.internal('Payload stream closed prematurely')); } @@ -444,8 +443,8 @@ internals.Client = class { const reader = new Recorder({ maxBytes: options.maxBytes }); const onReaderError = (err) => { - - if (res.destroy) { // GZip stream has no destroy() method + // GZip stream has no destroy() method + if (res.destroy) { res.destroy(); } @@ -455,16 +454,13 @@ internals.Client = class { reader.once('error', onReaderError); const onReaderFinish = () => { - return finishOnce(null, reader.collect()); }; reader.once('finish', onReaderFinish); if (options.gunzip) { - const contentEncoding = options.gunzip === 'force' ? - 'gzip' : - res.headers?.['content-encoding'] ?? ''; + const contentEncoding = options.gunzip === 'force' ? 'gzip' : (res.headers?.['content-encoding'] ?? ''); if (/^(x-)?gzip(\s*,\s*identity)?$/.test(contentEncoding)) { const gunzip = Zlib.createGunzip(); @@ -478,12 +474,10 @@ internals.Client = class { } toReadableStream(payload, encoding) { - return new Payload(payload, encoding); } parseCacheControl(field) { - /* Cache-Control = 1#cache-directive cache-directive = token [ "=" ( token / quoted-string ) ] @@ -492,11 +486,11 @@ internals.Client = class { */ // 1: directive = 2: token 3: quoted-string - const regex = /(?:^|(?:\s*\,\s*))([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\]|\\.)*)\")))?/g; + const regex = + /(?:^|(?:\s*\,\s*))([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\]|\\.)*)\")))?/g; const header = {}; const error = field.replace(regex, ($0, $1, $2, $3) => { - const value = $2 || $3; header[$1] = value ? value.toLowerCase() : true; return ''; @@ -510,8 +504,7 @@ internals.Client = class { } header['max-age'] = maxAge; - } - catch (err) { } + } catch (err) {} } return error ? null : header; @@ -520,39 +513,32 @@ internals.Client = class { // Shortcuts get(uri, options) { - return this._shortcut('GET', uri, options); } post(uri, options) { - return this._shortcut('POST', uri, options); } patch(uri, options) { - return this._shortcut('PATCH', uri, options); } put(uri, options) { - return this._shortcut('PUT', uri, options); } delete(uri, options) { - return this._shortcut('DELETE', uri, options); } async _shortcut(method, uri, options = {}) { - const res = await this.request(method, uri, options); let payload; try { payload = await this.read(res, options); - } - catch (err) { + } catch (err) { err.data = err.data ?? {}; err.data.res = res; throw err; @@ -568,18 +554,19 @@ internals.Client = class { isResponseError: true, headers: res.headers, res, - payload + payload, }; - throw new Boom.Boom(`Response Error: ${res.statusCode} ${res.statusMessage}`, { statusCode: res.statusCode, data }); + throw new Boom.Boom(`Response Error: ${res.statusCode} ${res.statusMessage}`, { + statusCode: res.statusCode, + data, + }); } -}; - +} // baseUrl needs to end in a trailing / if it contains paths that need to be preserved -internals.resolveUrl = function (baseUrl, path) { - +function resolveUrl(baseUrl, path) { if (!path) { return baseUrl; } @@ -587,13 +574,10 @@ internals.resolveUrl = function (baseUrl, path) { // Will default to path if it's not a relative URL const url = new Url.URL(path, baseUrl); return Url.format(url); -}; - - -internals.deferPipeUntilSocketConnects = function (req, stream) { +} +function deferPipeUntilSocketConnects(req, stream) { const onSocket = (socket) => { - if (!socket.connecting) { return onSocketConnect(); } @@ -602,23 +586,19 @@ internals.deferPipeUntilSocketConnects = function (req, stream) { }; const onSocketConnect = () => { - stream.pipe(req); stream.removeListener('error', onStreamError); }; const onStreamError = (err) => { - req.emit('error', err); }; req.once('socket', onSocket); stream.on('error', onStreamError); -}; - - -internals.redirectMethod = function (code, method, options) { +} +function resolveRedirectMethod(code, method, options) { switch (code) { case 301: case 302: @@ -637,11 +617,9 @@ internals.redirectMethod = function (code, method, options) { } return null; -}; - - -internals.tryParseBuffer = function (buffer, next) { +} +function tryParseBuffer(buffer, next) { if (buffer.length === 0) { return next(null, null); } @@ -649,22 +627,20 @@ internals.tryParseBuffer = function (buffer, next) { let payload; try { payload = Bourne.parse(buffer.toString()); - } - catch (err) { + } catch (err) { return next(Boom.badGateway(err.message, { payload: buffer })); } return next(null, payload); -}; - - -internals.applyUrlToOptions = (options, url) => { +} +function applyUrlToOptions(options, url) { options.host = url.host; options.origin = url.origin; options.searchParams = url.searchParams; options.protocol = url.protocol; - options.hostname = typeof url.hostname === 'string' && url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname; + options.hostname = + typeof url.hostname === 'string' && url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname; options.hash = url.hash; options.search = url.search; options.pathname = url.pathname; @@ -681,8 +657,8 @@ internals.applyUrlToOptions = (options, url) => { } return options; -}; +} -internals.isNullOrUndefined = (val) => [null, undefined].includes(val); +const isNullOrUndefined = (val) => [null, undefined].includes(val); -module.exports = new internals.Client(); +export default new Client(); diff --git a/lib/payload.js b/src/payload.js similarity index 83% rename from lib/payload.js rename to src/payload.js index 44d5dda..f2578a5 100755 --- a/lib/payload.js +++ b/src/payload.js @@ -1,15 +1,7 @@ -'use strict'; - -const Stream = require('stream'); - - -const internals = {}; - - -module.exports = internals.Payload = class extends Stream.Readable { +import * as Stream from 'node:stream'; +export class Payload extends Stream.Readable { constructor(payload, encoding) { - super(); const data = [].concat(payload || ''); @@ -26,7 +18,6 @@ module.exports = internals.Payload = class extends Stream.Readable { } _read(size) { - const chunk = this._data.slice(this._position, this._position + size); this.push(chunk, this._encoding); this._position = this._position + chunk.length; @@ -35,4 +26,4 @@ module.exports = internals.Payload = class extends Stream.Readable { this.push(null); } } -}; +} diff --git a/src/recorder.js b/src/recorder.js new file mode 100755 index 0000000..0ed68c8 --- /dev/null +++ b/src/recorder.js @@ -0,0 +1,36 @@ +import * as Stream from 'node:stream'; + +import * as Boom from '@hapi/boom'; + +export class Recorder extends Stream.Writable { + constructor(options) { + super(); + + this.settings = options; // No need to clone since called internally with new object + this.buffers = []; + this.length = 0; + } + + _write(chunk, encoding, next) { + if (this.settings.maxBytes && this.length + chunk.length > this.settings.maxBytes) { + return this.emit( + 'error', + Boom.entityTooLarge('Payload content length greater than maximum allowed: ' + this.settings.maxBytes), + ); + } + + this.length = this.length + chunk.length; + this.buffers.push(chunk); + next(); + } + + collect() { + const buffer = + this.buffers.length === 0 + ? Buffer.alloc(0) + : this.buffers.length === 1 + ? this.buffers[0] + : Buffer.concat(this.buffers, this.length); + return buffer; + } +} diff --git a/lib/tap.js b/src/tap.js similarity index 56% rename from lib/tap.js rename to src/tap.js index d4932a5..fa73570 100755 --- a/lib/tap.js +++ b/src/tap.js @@ -1,29 +1,19 @@ -'use strict'; +import * as Stream from 'node:stream'; -const Stream = require('stream'); - -const Payload = require('./payload'); - - -const internals = {}; - - -module.exports = internals.Tap = class extends Stream.Transform { +import { Payload } from './payload.js'; +export class Tap extends Stream.Transform { constructor() { - super(); this.buffers = []; } _transform(chunk, encoding, next) { - this.buffers.push(chunk); next(null, chunk); } collect() { - return new Payload(this.buffers); } -}; +} diff --git a/test/index.js b/test/index.js index 8341538..b7f11ac 100755 --- a/test/index.js +++ b/test/index.js @@ -1,425 +1,408 @@ -'use strict'; +import * as Dns from 'node:dns'; +import * as Events from 'node:events'; +import * as Fs from 'node:fs'; +import * as Http from 'node:http'; +import * as Https from 'node:https'; +import * as Path from 'node:path'; +import * as Stream from 'node:stream'; +import { fileURLToPath } from 'node:url'; +import * as Zlib from 'node:zlib'; -const Http = require('http'); -const Https = require('https'); -const Path = require('path'); -const Fs = require('fs'); -const Events = require('events'); -const Stream = require('stream'); -const Zlib = require('zlib'); -const Dns = require('dns'); +import * as Boom from '@hapi/boom'; +import * as Hoek from '@hapi/hoek'; +import { describe, expect, it, onTestFinished } from 'vitest'; -const Boom = require('@hapi/boom'); -const Code = require('@hapi/code'); -const Hoek = require('@hapi/hoek'); -const Lab = require('@hapi/lab'); -const Wreck = require('..'); +import Wreck from '../src/index.js'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = Path.dirname(__filename); const internals = { payload: new Array(1640).join('0123456789'), // make sure we have a payload larger than 16384 bytes for chunking coverage gzippedPayload: Zlib.gzipSync(new Array(1640).join('0123456789')), socket: __dirname + '/server.sock', emitSymbol: Symbol.for('wreck'), - refusePort: ['win19', 'win22'].includes(process.env.ImageOS) ? 777 : 0 + refusePort: ['win19', 'win22'].includes(process.env.ImageOS) ? 777 : 0, }; - -const { it, describe } = exports.lab = Lab.script(); -const expect = Code.expect; - - describe('request()', () => { - - it('requests a resource', async (flags) => { - + it('requests a resource', async () => { const server = await internals.server(); + onTestFinished(() => server.close()); const res = await Wreck.request('get', `http://localhost:${server.address().port}`); const body = await Wreck.read(res); - expect(Buffer.isBuffer(body)).to.equal(true); - expect(body.toString()).to.equal(internals.payload); - flags.onCleanup = () => server.close(); + expect(Buffer.isBuffer(body)).toBe(true); + expect(body.toString()).toBe(internals.payload); }); - it('requests a resource with IPv6', { skip: !process.features.ipv6 }, async (flags) => { - + it.skipIf(!process.features.ipv6)('requests a resource with IPv6', async () => { const server = await internals.server(); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const res = await Wreck.request('get', 'http://[::1]:' + server.address().port); const body = await Wreck.read(res); - expect(Buffer.isBuffer(body)).to.equal(true); - expect(body.toString()).to.equal(internals.payload); + expect(Buffer.isBuffer(body)).toBe(true); + expect(body.toString()).toBe(internals.payload); }); - it('requests a DELETE resource with payload', async (flags) => { - + it('requests a DELETE resource with payload', async () => { const handler = (req, res) => { - - expect(req.headers['content-length']).to.equal('16390'); + expect(req.headers['content-length']).toBe('16390'); res.writeHead(200, { 'Content-Type': 'text/plain' }); req.pipe(res); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - const res = await Wreck.request('delete', `http://localhost:${server.address().port}`, { payload: internals.payload }); + onTestFinished(() => server.close()); + const res = await Wreck.request('delete', `http://localhost:${server.address().port}`, { + payload: internals.payload, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); + expect(body.toString()).toBe(internals.payload); }); - it('requests a POST resource', async (flags) => { - + it('requests a POST resource', async () => { const handler = (req, res) => { - - expect(req.headers['content-length']).to.equal('16390'); + expect(req.headers['content-length']).toBe('16390'); res.writeHead(200, { 'Content-Type': 'text/plain' }); req.pipe(res); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { payload: internals.payload }); + onTestFinished(() => server.close()); + const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { + payload: internals.payload, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); + expect(body.toString()).toBe(internals.payload); }); - it('requests a POST resource with unicode characters in payload', async (flags) => { - + it('requests a POST resource with unicode characters in payload', async () => { const handler = (req, res) => { - - expect(req.headers['content-length']).to.equal('14'); + expect(req.headers['content-length']).toBe('14'); res.writeHead(200, { 'Content-Type': 'text/plain' }); req.pipe(res); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const unicodePayload = JSON.stringify({ field: 'ć' }); - const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { payload: unicodePayload }); + const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { + payload: unicodePayload, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(unicodePayload); + expect(body.toString()).toBe(unicodePayload); }); - it('requests a POST resource with a JSON payload', async (flags) => { - + it('requests a POST resource with a JSON payload', async () => { const handler = (req, res) => { - - expect(req.headers['content-type']).to.equal('application/json'); + expect(req.headers['content-type']).toBe('application/json'); res.writeHead(200, { 'Content-Type': 'application/json' }); req.pipe(res); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const payload = { my: 'object' }; const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { payload }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(JSON.stringify(payload)); + expect(body.toString()).toBe(JSON.stringify(payload)); }); - it('requests a POST resource with a JSON payload and custom content-type header', async (flags) => { - + it('requests a POST resource with a JSON payload and custom content-type header', async () => { const handler = (req, res) => { - - expect(req.headers['content-type']).to.equal('application/json-patch+json'); + expect(req.headers['content-type']).toBe('application/json-patch+json'); res.writeHead(200, { 'Content-Type': 'application/json' }); req.pipe(res); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const payload = [{ op: 'remove', path: '/test' }]; const headers = {}; headers['content-type'] = 'application/json-patch+json'; const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { payload, headers }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(JSON.stringify(payload)); + expect(body.toString()).toBe(JSON.stringify(payload)); }); - it('should not overwrite content-length if it is already in the headers', async (flags) => { - + it('should not overwrite content-length if it is already in the headers', async () => { const handler = (req, res) => { - - expect(req.headers['content-length']).to.equal('16390'); + expect(req.headers['content-length']).toBe('16390'); res.writeHead(200, { 'Content-Type': 'text/plain' }); req.pipe(res); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const options = { payload: internals.payload, headers: { 'Content-Length': '16390' } }; const res = await Wreck.request('post', `http://localhost:${server.address().port}`, options); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); + expect(body.toString()).toBe(internals.payload); }); - it('should not add content-type if it is already in the headers but not lower cased', async (flags) => { - + it('should not add content-type if it is already in the headers but not lower cased', async () => { const handler = (req, res) => { - - expect(req.headers['content-type']).to.equal('application/json-patch+json'); + expect(req.headers['content-type']).toBe('application/json-patch+json'); res.writeHead(200, { 'Content-Type': 'application/json' }); req.pipe(res); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const payload = [{ op: 'remove', path: '/test' }]; const headers = {}; headers['Content-Type'] = 'application/json-patch+json'; const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { payload, headers }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(JSON.stringify(payload)); + expect(body.toString()).toBe(JSON.stringify(payload)); }); - it('requests a POST resource with headers', async (flags) => { - + it('requests a POST resource with headers', async () => { const server = await internals.server('echo'); - flags.onCleanup = () => server.close(); - - const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { headers: { 'user-agent': 'wreck' }, payload: internals.payload }); + onTestFinished(() => server.close()); + const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { + headers: { 'user-agent': 'wreck' }, + payload: internals.payload, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); + expect(body.toString()).toBe(internals.payload); }); - it('requests a POST resource with stream payload', async (flags) => { - + it('requests a POST resource with stream payload', async () => { const server = await internals.server('echo'); - flags.onCleanup = () => server.close(); - - const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { payload: Wreck.toReadableStream(internals.payload) }); + onTestFinished(() => server.close()); + const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { + payload: Wreck.toReadableStream(internals.payload), + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); + expect(body.toString()).toBe(internals.payload); }); - it('cannot set agent and rejectUnauthorized at the same time', async (flags) => { - + it('cannot set agent and rejectUnauthorized at the same time', async () => { const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); - - await expect(Wreck.request('get', `http://localhost:${server.address().port}`, { rejectUnauthorized: true, agent: new Https.Agent() })).to.reject(); + onTestFinished(() => server.close()); + await expect( + Wreck.request('get', `http://localhost:${server.address().port}`, { + rejectUnauthorized: true, + agent: new Https.Agent(), + }), + ).rejects.toThrow(); }); - it('cannot set a false agent and rejectUnauthorized at the same time', async (flags) => { - + it('cannot set a false agent and rejectUnauthorized at the same time', async () => { const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); - - await expect(Wreck.request('get', `http://localhost:${server.address().port}`, { rejectUnauthorized: false, agent: false })).to.reject(); + onTestFinished(() => server.close()); + await expect( + Wreck.request('get', `http://localhost:${server.address().port}`, { + rejectUnauthorized: false, + agent: false, + }), + ).rejects.toThrow(); }); - it('can set a null agent and rejectUnauthorized at the same time', async (flags) => { - + it('can set a null agent and rejectUnauthorized at the same time', async () => { const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); - - await expect(Wreck.request('get', `http://localhost:${server.address().port}`, { rejectUnauthorized: false, agent: null })).to.not.reject(); + onTestFinished(() => server.close()); + await expect( + Wreck.request('get', `http://localhost:${server.address().port}`, { + rejectUnauthorized: false, + agent: null, + }), + ).resolves.not.toThrow(); }); - it('requests an https resource', async (flags) => { - + it('requests an https resource', async () => { const res = await Wreck.request('get', 'https://google.com', { rejectUnauthorized: true }); const body = await Wreck.read(res); - expect(body.toString()).to.contain(''); + expect(body.toString()).toContain(''); }); - it('requests an https resource with secure protocol set', async (flags) => { - - const res = await Wreck.request('get', 'https://google.com', { rejectUnauthorized: true, secureProtocol: 'SSLv23_method' }); + it('requests an https resource with secure protocol set', async () => { + const res = await Wreck.request('get', 'https://google.com', { + rejectUnauthorized: true, + secureProtocol: 'SSLv23_method', + }); const body = await Wreck.read(res); - expect(body.toString()).to.contain(''); + expect(body.toString()).toContain(''); }); - it('requests an https resource with TLS ciphers set', async (flags) => { - + it('requests an https resource with TLS ciphers set', async () => { const res = await Wreck.request('get', 'https://google.com', { rejectUnauthorized: true, ciphers: 'HIGH' }); const body = await Wreck.read(res); - expect(body.toString()).to.contain(''); + expect(body.toString()).toContain(''); }); - it('fails when an https resource has invalid certs and the default rejectUnauthorized', async (flags) => { - + it('fails when an https resource has invalid certs and the default rejectUnauthorized', async () => { const server = await internals.https(); - flags.onCleanup = () => server.close(); - - await expect(Wreck.request('get', 'https://localhost:' + server.address().port)).to.reject(); + onTestFinished(() => server.close()); + await expect(Wreck.request('get', 'https://localhost:' + server.address().port)).rejects.toThrow(); }); - it('succeeds when an https resource has unauthorized certs and rejectUnauthorized is false', async (flags) => { - + it('succeeds when an https resource has unauthorized certs and rejectUnauthorized is false', async () => { const server = await internals.https(); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); await Wreck.request('get', 'https://localhost:' + server.address().port, { rejectUnauthorized: false }); }); - it('applies rejectUnauthorized when redirected', async (flags) => { - + it('applies rejectUnauthorized when redirected', async () => { let gen = 0; const handler = (req, res) => { - if (!gen++) { - res.writeHead(301, { 'Location': '/' }); + res.writeHead(301, { Location: '/' }); res.end(); - } - else { + } else { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(); } }; const server = await internals.https(handler); - flags.onCleanup = () => server.close(); - - const res = await Wreck.request('get', 'https://localhost:' + server.address().port, { redirects: 1, rejectUnauthorized: false }); - expect(res.statusCode).to.equal(200); + onTestFinished(() => server.close()); + const res = await Wreck.request('get', 'https://localhost:' + server.address().port, { + redirects: 1, + rejectUnauthorized: false, + }); + expect(res.statusCode).toBe(200); }); - it('does not follow redirections by default', async (flags) => { - + it('does not follow redirections by default', async () => { let gen = 0; const handler = (req, res) => { - if (!gen++) { - res.writeHead(301, { 'Location': `http://localhost:${server.address().port}` }); + res.writeHead(301, { Location: `http://localhost:${server.address().port}` }); res.end(); - } - else { + } else { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(internals.payload); } }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const res = await Wreck.request('get', `http://localhost:${server.address().port}`); await Wreck.read(res); - expect(res.statusCode).to.equal(301); + expect(res.statusCode).toBe(301); }); - it('handles redirections', async (flags) => { - + it('handles redirections', async () => { let gen = 0; const handler = (req, res) => { - if (!gen++) { - res.writeHead(301, { 'Location': `http://localhost:${server.address().port}` }); + res.writeHead(301, { Location: `http://localhost:${server.address().port}` }); res.end(); - } - else { + } else { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(internals.payload); } }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 1, beforeRedirect: null, redirected: null }); + onTestFinished(() => server.close()); + const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { + redirects: 1, + beforeRedirect: null, + redirected: null, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); + expect(body.toString()).toBe(internals.payload); }); - it('handles 301 redirections without overriding the HTTP method', async (flags) => { - + it('handles 301 redirections without overriding the HTTP method', async () => { const payload = 'HELLO POST'; let gen = 0; const handler = async (req, res) => { - - expect(req.method).to.equal('POST'); + expect(req.method).toBe('POST'); const res2 = await Wreck.read(req); - expect(res2.toString()).to.equal(payload); + expect(res2.toString()).toBe(payload); if (!gen++) { - res.writeHead(301, { 'Location': `http://localhost:${server.address().port}` }); + res.writeHead(301, { Location: `http://localhost:${server.address().port}` }); res.end(); - } - else { + } else { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(internals.payload); } }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - const res = await Wreck.request('POST', `http://localhost:${server.address().port}`, { redirects: 1, beforeRedirect: null, redirected: null, payload }); + onTestFinished(() => server.close()); + const res = await Wreck.request('POST', `http://localhost:${server.address().port}`, { + redirects: 1, + beforeRedirect: null, + redirected: null, + payload, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); + expect(body.toString()).toBe(internals.payload); }); - it('overrides 301 redirection method', async (flags) => { - + it('overrides 301 redirection method', async () => { const payload = 'HELLO POST'; let gen = 0; const handler = async (req, res) => { - const res2 = await Wreck.read(req); if (!gen++) { - expect(req.method).to.equal('POST'); - expect(res2.toString()).to.equal(payload); - res.writeHead(301, { 'Location': `http://localhost:${server.address().port}` }); + expect(req.method).toBe('POST'); + expect(res2.toString()).toBe(payload); + res.writeHead(301, { Location: `http://localhost:${server.address().port}` }); res.end(); - } - else { - expect(req.method).to.equal('GET'); - expect(res2.toString()).to.equal(''); + } else { + expect(req.method).toBe('GET'); + expect(res2.toString()).toBe(''); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(internals.payload); } }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - const res = await Wreck.request('POST', `http://localhost:${server.address().port}`, { redirectMethod: 'GET', redirects: 1, beforeRedirect: null, redirected: null, payload }); + onTestFinished(() => server.close()); + const res = await Wreck.request('POST', `http://localhost:${server.address().port}`, { + redirectMethod: 'GET', + redirects: 1, + beforeRedirect: null, + redirected: null, + payload, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); + expect(body.toString()).toBe(internals.payload); }); - it('handles redirections with new host', async (flags) => { - + it('handles redirections with new host', async () => { const handler = (req, res) => { - - res.writeHead(302, { 'Location': 'http://localhost:' + http2.address().port }); + res.writeHead(302, { Location: 'http://localhost:' + http2.address().port }); res.end(); }; const http1 = await internals.server(handler); const http2 = await internals.server(); - const headers = {}; // Headers object is needed to trigger bug + const headers = {}; // Headers object is needed to trigger bug const res = await Wreck.request('get', 'http://localhost:' + http1.address().port, { redirects: 1, headers }); - expect(res.statusCode).to.equal(200); + expect(res.statusCode).toBe(200); http1.close(); http2.close(); }); - it('handles redirections with new hostname, removing authorization, cookie, and proxy-authorization headers', async (flags) => { - + it('handles redirections with new hostname, removing authorization, cookie, and proxy-authorization headers', async () => { const handler1 = (req, res) => { - - res.writeHead(302, { 'Location': 'http://127.0.0.1:' + http2.address().port }); + res.writeHead(302, { Location: 'http://127.0.0.1:' + http2.address().port }); res.end(); }; const handler2 = (req, res) => { - // request must have 'x-foo' header, but must not have 'authorization', 'cookie' or 'proxy-authorization' - if (req.headers.authorization || req.headers.cookie || req.headers['proxy-authorization'] || !req.headers['x-foo']) { + if ( + req.headers.authorization || + req.headers.cookie || + req.headers['proxy-authorization'] || + !req.headers['x-foo'] + ) { res.writeHead(500); } @@ -433,19 +416,17 @@ describe('request()', () => { authorization: 'some-auth-key', cookie: 'some-cookie', 'proxy-authorization': 'some-proxy-auth', - 'x-foo': 'something-else' + 'x-foo': 'something-else', }; const res = await Wreck.request('get', 'http://localhost:' + http1.address().port, { redirects: 1, headers }); - expect(res.statusCode).to.equal(200); + expect(res.statusCode).toBe(200); http1.close(); http2.close(); }); - it('removes sensitive headers when redirected to a different port on the same hostname', async (flags) => { - + it('removes sensitive headers when redirected to a different port on the same hostname', async () => { const handler2 = (req, res) => { - if (req.headers.authorization || req.headers.cookie || req.headers['proxy-authorization']) { res.writeHead(500); return res.end(); @@ -458,8 +439,7 @@ describe('request()', () => { const http2 = await internals.server(handler2); const handler1 = (req, res) => { - - res.writeHead(302, { 'Location': 'http://localhost:' + http2.address().port }); + res.writeHead(302, { Location: 'http://localhost:' + http2.address().port }); res.end(); }; @@ -468,25 +448,22 @@ describe('request()', () => { const headers = { authorization: 'some-auth-key', cookie: 'some-cookie', - 'proxy-authorization': 'some-proxy-auth' + 'proxy-authorization': 'some-proxy-auth', }; const res = await Wreck.request('get', 'http://localhost:' + http1.address().port, { redirects: 1, headers }); - expect(res.statusCode).to.equal(200); + expect(res.statusCode).toBe(200); http1.close(); http2.close(); }); - it('removes sensitive headers when redirected to a different scheme on the same hostname', async (flags) => { - + it('removes sensitive headers when redirected to a different scheme on the same hostname', async () => { const handler1 = (req, res) => { - - res.writeHead(302, { 'Location': 'https://127.0.0.1:' + https.address().port }); + res.writeHead(302, { Location: 'https://127.0.0.1:' + https.address().port }); res.end(); }; const handler2 = (req, res) => { - if (req.headers.authorization || req.headers.cookie || req.headers['proxy-authorization']) { res.writeHead(500); return res.end(); @@ -502,22 +479,24 @@ describe('request()', () => { const headers = { authorization: 'some-auth-key', cookie: 'some-cookie', - 'proxy-authorization': 'some-proxy-auth' + 'proxy-authorization': 'some-proxy-auth', }; - const res = await Wreck.request('get', 'http://localhost:' + http.address().port, { redirects: 1, rejectUnauthorized: false, headers }); - expect(res.statusCode).to.equal(200); + const res = await Wreck.request('get', 'http://localhost:' + http.address().port, { + redirects: 1, + rejectUnauthorized: false, + headers, + }); + expect(res.statusCode).toBe(200); http.close(); https.close(); }); - it('preserves proxy-authorization header on same-hostname redirect', async (flags) => { - + it('preserves proxy-authorization header on same-hostname redirect', async () => { let gen = 0; const handler = (req, res) => { - if (!gen++) { - res.writeHead(301, { 'Location': '/' }); + res.writeHead(301, { Location: '/' }); res.end(); return; } @@ -533,183 +512,170 @@ describe('request()', () => { const server = await internals.server(handler); const headers = { - 'proxy-authorization': 'some-proxy-auth' + 'proxy-authorization': 'some-proxy-auth', }; const res = await Wreck.request('get', 'http://localhost:' + server.address().port, { redirects: 1, headers }); - expect(res.statusCode).to.equal(200); + expect(res.statusCode).toBe(200); server.close(); }); - it('handles redirections from http to https', async (flags) => { - + it('handles redirections from http to https', async () => { const handler = (req, res) => { - - res.writeHead(302, { 'Location': 'https://127.0.0.1:' + https.address().port }); + res.writeHead(302, { Location: 'https://127.0.0.1:' + https.address().port }); res.end(); }; const https = await internals.https(); const http = await internals.server(handler); - const res = await Wreck.request('get', 'http://localhost:' + http.address().port, { redirects: 1, rejectUnauthorized: false }); - expect(res.statusCode).to.equal(200); + const res = await Wreck.request('get', 'http://localhost:' + http.address().port, { + redirects: 1, + rejectUnauthorized: false, + }); + expect(res.statusCode).toBe(200); http.close(); https.close(); }); - it('handles redirections with relative location', async (flags) => { - + it('handles redirections with relative location', async () => { let gen = 0; const handler = (req, res) => { - if (!gen++) { - res.writeHead(301, { 'Location': '/' }); + res.writeHead(301, { Location: '/' }); res.end(); - } - else { - expect(req.url).to.equal('/'); + } else { + expect(req.url).toBe('/'); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(internals.payload); } }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 1 }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); + expect(body.toString()).toBe(internals.payload); }); - it('ignores 303 redirections by default', async (flags) => { - + it('ignores 303 redirections by default', async () => { const handler = (req, res) => { - - res.writeHead(303, { 'Location': `http://localhost:${server.address().port}` }); + res.writeHead(303, { Location: `http://localhost:${server.address().port}` }); res.end(); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 1, beforeRedirect: null, redirected: null }); - expect(res.statusCode).to.equal(303); + onTestFinished(() => server.close()); + const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { + redirects: 1, + beforeRedirect: null, + redirected: null, + }); + expect(res.statusCode).toBe(303); }); - it('handles 303 redirections when allowed', async (flags) => { - + it('handles 303 redirections when allowed', async () => { let gen = 0; const handler = (req, res) => { - if (!gen++) { - res.writeHead(303, { 'Location': `http://localhost:${server.address().port}` }); + res.writeHead(303, { Location: `http://localhost:${server.address().port}` }); res.end(); - } - else { + } else { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(internals.payload); } }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 1, beforeRedirect: null, redirected: null, redirect303: true }); + onTestFinished(() => server.close()); + const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { + redirects: 1, + beforeRedirect: null, + redirected: null, + redirect303: true, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); + expect(body.toString()).toBe(internals.payload); }); - it('handles redirections with different host than baseUrl in defaults', async (flags) => { - + it('handles redirections with different host than baseUrl in defaults', async () => { const handler = (req, res) => { - - res.writeHead(301, { 'Location': 'https://hapi.dev' }); + res.writeHead(301, { Location: 'https://hapi.dev' }); res.end(); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const wreckA = Wreck.defaults({ baseUrl: `http://localhost:${server.address().port}` }); const options = { redirects: 1, redirected: (statusCode, location, req) => { - - expect(location).to.equal('https://hapi.dev'); + expect(location).toBe('https://hapi.dev'); if (req.output) { - expect(req.output[0]).to.include('hapi.dev'); - } - else { - expect(req.outputData[0].data).to.include('hapi.dev'); + expect(req.output[0]).toContain('hapi.dev'); + } else { + expect(req.outputData[0].data).toContain('hapi.dev'); } - } + }, }; await wreckA.request('get', '/redirect', options); }); - it('handles uri with different host than baseUrl in defaults', async (flags) => { - + it('handles uri with different host than baseUrl in defaults', async () => { const server = await internals.server(); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const wreckA = Wreck.defaults({ baseUrl: 'http://no.such.domain.error' }); const res = await wreckA.request('get', `http://localhost:${server.address().port}`); const body = await Wreck.read(res); - expect(Buffer.isBuffer(body)).to.equal(true); - expect(body.toString()).to.equal(internals.payload); + expect(Buffer.isBuffer(body)).toBe(true); + expect(body.toString()).toBe(internals.payload); }); - it('handles uri with WHATWG parsing', async (flags) => { - + it('handles uri with WHATWG parsing', async () => { const promise = Wreck.get('http://localhost%60malicious.org'); - await expect(promise).to.reject(); + await expect(promise).rejects.toThrow(); }); - it('reaches max redirections count', async (flags) => { - + it('reaches max redirections count', async () => { let gen = 0; const handler = (req, res) => { - if (gen++ < 2) { - res.writeHead(301, { 'Location': `http://localhost:${server.address().port}` }); + res.writeHead(301, { Location: `http://localhost:${server.address().port}` }); res.end(); - } - else { + } else { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(internals.payload); } }; const server = await internals.server(handler); - await expect(Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 1 })).to.reject('Maximum redirections reached'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); + await expect( + Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 1 }), + ).rejects.toThrow('Maximum redirections reached'); }); - it('handles malformed redirection response', async (flags) => { - + it('handles malformed redirection response', async () => { const handler = (req, res) => { - res.writeHead(301); res.end(); }; const server = await internals.server(handler); - await expect(Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 1 })).to.reject('Received redirection without location'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); + await expect( + Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 1 }), + ).rejects.toThrow('Received redirection without location'); }); - it('handles redirections with POST stream payload', async (flags) => { - + it('handles redirections with POST stream payload', async () => { let gen = 0; const handler = async (req, res) => { - if (!gen++) { - res.writeHead(307, { 'Location': '/' }); + res.writeHead(307, { Location: '/' }); res.end(); - } - else { + } else { res.writeHead(200, { 'Content-Type': 'text/plain' }); const res2 = await Wreck.read(req); res.end(res2); @@ -717,24 +683,24 @@ describe('request()', () => { }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const payload = new Array(1639).join('0123456789'); const stream = Wreck.toReadableStream(payload); - const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { redirects: 1, payload: stream }); + const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { + redirects: 1, + payload: stream, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(payload); - flags.onCleanup = () => server.close(); + expect(body.toString()).toBe(payload); }); - it('handles timeouts after a redirect', async (flags) => { - + it('handles timeouts after a redirect', async () => { let redirectCount = 0; let timeout = 0; const handler = (req, res) => { - setTimeout(() => { - - res.writeHead(302, { 'Location': `http://localhost:${server.address().port}` }); + res.writeHead(302, { Location: `http://localhost:${server.address().port}` }); res.end(); }, timeout); @@ -743,30 +709,28 @@ describe('request()', () => { }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - const err = await expect(Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 5, timeout: 40 })).to.reject(); - expect(err.output.statusCode).to.equal(504); + onTestFinished(() => server.close()); + const err = await internals.rejection( + Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 5, timeout: 40 }), + ); + expect(err.output.statusCode).toBe(504); // Validate that no further requests are made const targetCount = redirectCount; await Hoek.wait(30); - expect(redirectCount).to.equal(targetCount); + expect(redirectCount).toBe(targetCount); }); - it('calls beforeRedirect option callback before redirections', async (flags) => { - + it('calls beforeRedirect option callback before redirections', async () => { let gen = 0; const handler = (req, res) => { - if (gen++ < 2) { - res.writeHead(301, { 'Location': `http://localhost:${server.address().port}` + '/redirected/' }); + res.writeHead(301, { Location: `http://localhost:${server.address().port}` + '/redirected/' }); res.end(); - } - else { - expect(req.url).to.equal('/redirected/'); - expect(req.headers['x-test']).to.equal('Modified'); + } else { + expect(req.url).toBe('/redirected/'); + expect(req.headers['x-test']).toBe('Modified'); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(internals.payload); @@ -774,203 +738,184 @@ describe('request()', () => { }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const beforeRedirectCallback = function (redirectMethod, statusCode, location, headers, redirectOptions, next) { - const dest = `http://localhost:${server.address().port}/redirected/`; - expect(redirectMethod).to.equal('GET'); - expect(statusCode).to.equal(301); - expect(location).to.equal(dest); - expect(redirectOptions).to.exist(); - expect(headers.location).to.equal(dest); + expect(redirectMethod).toBe('GET'); + expect(statusCode).toBe(301); + expect(location).toBe(dest); + expect(redirectOptions).toBeDefined(); + expect(headers.location).toBe(dest); redirectOptions.headers = { - 'x-test': 'Modified' + 'x-test': 'Modified', }; return next(); }; - const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 5, beforeRedirect: beforeRedirectCallback }); + const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { + redirects: 5, + beforeRedirect: beforeRedirectCallback, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); + expect(body.toString()).toBe(internals.payload); }); - it('cancels redirect if beforeRedirect callback is called with an error', async (flags) => { - + it('cancels redirect if beforeRedirect callback is called with an error', async () => { const handler = (req, res) => { - - res.writeHead(301, { 'Location': `http://localhost:${server.address().port}` + '/redirected/' }); + res.writeHead(301, { Location: `http://localhost:${server.address().port}` + '/redirected/' }); res.end(); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const err = new Error('Cancel'); const beforeRedirectCallback = function (redirectMethod, statusCode, location, headers, redirectOptions, next) { - return next(err); }; - const thrown = await expect(Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 5, beforeRedirect: beforeRedirectCallback })).to.reject(); - expect(thrown.isBoom).to.equal(true); - expect(thrown.message).to.equal('Invalid redirect: Cancel'); + const thrown = await internals.rejection( + Wreck.request('get', `http://localhost:${server.address().port}`, { + redirects: 5, + beforeRedirect: beforeRedirectCallback, + }), + ); + expect(thrown.isBoom).toBe(true); + expect(thrown.message).toBe('Invalid redirect: Cancel'); }); - it('calls redirected option callback on redirections', async (flags) => { - + it('calls redirected option callback on redirections', async () => { let gen = 0; const handler = (req, res) => { - if (gen++ < 2) { - res.writeHead(301, { 'Location': `http://localhost:${server.address().port}` + '/redirected/' }); + res.writeHead(301, { Location: `http://localhost:${server.address().port}` + '/redirected/' }); res.end(); - } - else { - expect(req.url).to.equal('/redirected/'); + } else { + expect(req.url).toBe('/redirected/'); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(internals.payload); } }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); let redirects = 0; const redirectedCallback = function (statusCode, location, req) { - - expect(statusCode).to.equal(301); - expect(location).to.equal(`http://localhost:${server.address().port}` + '/redirected/'); - expect(req).to.exist(); + expect(statusCode).toBe(301); + expect(location).toBe(`http://localhost:${server.address().port}` + '/redirected/'); + expect(req).toBeDefined(); redirects++; }; - const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 5, redirected: redirectedCallback }); + const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { + redirects: 5, + redirected: redirectedCallback, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); - expect(redirects).to.equal(2); + expect(body.toString()).toBe(internals.payload); + expect(redirects).toBe(2); }); - it('rejects non-function value for redirected option', async (flags) => { - - await expect(Wreck.request('get', 'https://google.com', { redirects: 1, redirected: true })).to.reject(); + it('rejects non-function value for redirected option', async () => { + await expect(Wreck.request('get', 'https://google.com', { redirects: 1, redirected: true })).rejects.toThrow(); }); - it('handles request errors with a boom response', async (flags) => { - + it('handles request errors with a boom response', async () => { const handler = (req, res) => { - req.destroy(); res.end(); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - const err = await expect(Wreck.request('get', 'http://127.0.0.1:' + server.address().port)).to.reject(); - expect(err.isBoom).to.equal(true); + onTestFinished(() => server.close()); + const err = await internals.rejection(Wreck.request('get', 'http://127.0.0.1:' + server.address().port)); + expect(err.isBoom).toBe(true); }); - it('handles request errors with a boom response when payload is being sent', async (flags) => { - + it('handles request errors with a boom response when payload is being sent', async () => { const handler = (req, res) => { - req.destroy(); res.end(); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - const err = await expect(Wreck.request('get', 'http://127.0.0.1:' + server.address().port, { payload: internals.payload })).to.reject(); - expect(err.isBoom).to.equal(true); + onTestFinished(() => server.close()); + const err = await internals.rejection( + Wreck.request('get', 'http://127.0.0.1:' + server.address().port, { payload: internals.payload }), + ); + expect(err.isBoom).toBe(true); }); - it('handles response errors with a boom response (res.destroy)', async (flags) => { - + it('handles response errors with a boom response (res.destroy)', async () => { const handler = (req, res) => { - res.destroy(); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - const err = await expect(Wreck.request('get', 'http://127.0.0.1:' + server.address().port)).to.reject(); - expect(err.isBoom).to.equal(true); + onTestFinished(() => server.close()); + const err = await internals.rejection(Wreck.request('get', 'http://127.0.0.1:' + server.address().port)); + expect(err.isBoom).toBe(true); }); - it('handles errors when remote server is unavailable', async (flags) => { - - await expect(Wreck.request('get', 'http://127.0.0.1:10')).to.reject(); + it('handles errors when remote server is unavailable', async () => { + await expect(Wreck.request('get', 'http://127.0.0.1:10')).rejects.toThrow(); }); - it('handles a timeout during a socket close', async (flags) => { - + it('handles a timeout during a socket close', async () => { const handler = (req, res) => { - - req.once('error', () => { }); - res.once('error', () => { }); + req.once('error', () => {}); + res.once('error', () => {}); setTimeout(() => { - req.destroy(); }, 5); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - await expect(Wreck.request('get', 'http://127.0.0.1:' + server.address().port, { timeout: 5 })).to.reject(); + onTestFinished(() => server.close()); + await expect( + Wreck.request('get', 'http://127.0.0.1:' + server.address().port, { timeout: 5 }), + ).rejects.toThrow(); }); - it('handles an error after a timeout', async (flags) => { - + it('handles an error after a timeout', async () => { const handler = (req, res) => { - - req.once('error', () => { }); - res.once('error', () => { }); + req.once('error', () => {}); + res.once('error', () => {}); setTimeout(() => { - res.socket.write('ERROR'); }, 5); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - await expect(Wreck.request('get', 'http://127.0.0.1:' + server.address().port, { timeout: 5 })).to.reject(); + onTestFinished(() => server.close()); + await expect( + Wreck.request('get', 'http://127.0.0.1:' + server.address().port, { timeout: 5 }), + ).rejects.toThrow(); }); - it('ignores negative timeout', async (flags) => { - + it('ignores negative timeout', async () => { const server = await internals.server(); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const res = await Wreck.request('get', `http://localhost:${server.address().port}`); const body = await Wreck.read(res, { timeout: -1 }); - expect(Buffer.isBuffer(body)).to.equal(true); - expect(body.toString()).to.equal(internals.payload); + expect(Buffer.isBuffer(body)).toBe(true); + expect(body.toString()).toBe(internals.payload); }); - it('requests can be aborted', async (flags) => { - + it('requests can be aborted', async () => { const server = await internals.server(); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const promise = Wreck.request('get', `http://localhost:${server.address().port}`); promise.req.abort(); - await expect(promise).to.reject(); + await expect(promise).rejects.toThrow(); }); - it('in-progress requests can be aborted', async (flags) => { - + it('in-progress requests can be aborted', async () => { const handler = (req, res) => { - res.writeHead(200); res.end(); @@ -978,34 +923,30 @@ describe('request()', () => { }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - + onTestFinished(() => server.close()); const promise = Wreck.request('get', `http://localhost:${server.address().port}`); - await expect(promise).to.reject(); + await expect(promise).rejects.toThrow(); }); - it('uses agent option', async (flags) => { - + it('uses agent option', async () => { const agent = new Http.Agent(); - expect(Object.keys(agent.sockets).length).to.equal(0); + expect(Object.keys(agent.sockets).length).toBe(0); const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); - - await expect(Wreck.request('get', `http://localhost:${server.address().port}`, { agent })).to.not.reject(); - expect(Object.keys(agent.sockets).length).to.equal(1); + onTestFinished(() => server.close()); + await expect( + Wreck.request('get', `http://localhost:${server.address().port}`, { agent }), + ).resolves.not.toThrow(); + expect(Object.keys(agent.sockets).length).toBe(1); }); - it('applies agent option when redirected', async (flags) => { - + it('applies agent option when redirected', async () => { let gen = 0; const handler = (req, res) => { - if (!gen++) { - res.writeHead(301, { 'Location': '/' }); + res.writeHead(301, { Location: '/' }); res.end(); - } - else { + } else { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(); } @@ -1015,91 +956,91 @@ describe('request()', () => { let requestCount = 0; const addRequest = agent.addRequest; agent.addRequest = function (...args) { - requestCount++; addRequest.apply(agent, args); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { redirects: 1, agent }); - expect(res.statusCode).to.equal(200); - expect(requestCount).to.equal(2); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(requestCount).toBe(2); }); - it('pooling can be disabled by setting agent to false', async (flags) => { - + it('pooling can be disabled by setting agent to false', async () => { let complete; const handler = (req, res) => { - res.writeHead(200); res.write('foo'); - complete = complete || function () { - - res.end(); - }; + complete = + complete || + function () { + res.end(); + }; }; const server = await internals.server(handler); - const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { agent: false, timeout: 50 }); - expect(Object.keys(Wreck.agents.http.sockets).length).to.equal(0); - expect(Object.keys(Wreck.agents.http.requests).length).to.equal(0); + const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { + agent: false, + timeout: 50, + }); + expect(Object.keys(Wreck.agents.http.sockets).length).toBe(0); + expect(Object.keys(Wreck.agents.http.requests).length).toBe(0); - await Wreck.request('get', `http://localhost:${server.address().port}` + '/thatone', { agent: false, timeout: 50 }); - expect(Object.keys(Wreck.agents.http.sockets).length).to.equal(0); - expect(Object.keys(Wreck.agents.http.requests).length).to.equal(0); + await Wreck.request('get', `http://localhost:${server.address().port}` + '/thatone', { + agent: false, + timeout: 50, + }); + expect(Object.keys(Wreck.agents.http.sockets).length).toBe(0); + expect(Object.keys(Wreck.agents.http.requests).length).toBe(0); complete(); await Wreck.read(res); - expect(Object.keys(Wreck.agents.http.sockets).length).to.equal(0); - expect(Object.keys(Wreck.agents.http.requests).length).to.equal(0); + expect(Object.keys(Wreck.agents.http.sockets).length).toBe(0); + expect(Object.keys(Wreck.agents.http.requests).length).toBe(0); }); - it('requests payload in buffer', async (flags) => { - + it('requests payload in buffer', async () => { const server = await internals.server('echo'); + onTestFinished(() => server.close()); const buf = Buffer.from(internals.payload, 'ascii'); const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { payload: buf }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); - flags.onCleanup = () => server.close(); + expect(body.toString()).toBe(internals.payload); }); - it('requests head method', async (flags) => { - + it('requests head method', async () => { const server = await internals.server('echo'); + onTestFinished(() => server.close()); const res = await Wreck.request('head', `http://localhost:${server.address().port}`, { payload: null }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(''); - flags.onCleanup = () => server.close(); + expect(body.toString()).toBe(''); }); - it('post null payload', async (flags) => { - + it('post null payload', async () => { const handler = (req, res) => { - res.statusCode = 500; res.end(); }; const server = await internals.server(handler); - const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { headers: { connection: 'close' }, payload: null }); + onTestFinished(() => server.close()); + const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { + headers: { connection: 'close' }, + payload: null, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(''); - flags.onCleanup = () => server.close(); + expect(body.toString()).toBe(''); }); - it('handles request timeout', async (flags) => { - + it('handles request timeout', async () => { const handler = (req, res) => { - setTimeout(() => { - res.writeHead(200); res.write(internals.payload); res.end(); @@ -1107,69 +1048,70 @@ describe('request()', () => { }; const server = await internals.server(handler); - const err = await expect(Wreck.request('get', `http://localhost:${server.address().port}`, { timeout: 100 })).to.reject(); - expect(err.output.statusCode).to.equal(504); + const err = await internals.rejection( + Wreck.request('get', `http://localhost:${server.address().port}`, { timeout: 100 }), + ); + expect(err.output.statusCode).toBe(504); }); - it('cleans socket on agent deferred request timeout', async (flags) => { - + it('cleans socket on agent deferred request timeout', async () => { let complete; const handler = (req, res) => { - res.writeHead(200); res.write('foo'); - complete = complete || function () { - - res.end(); - }; + complete = + complete || + function () { + res.end(); + }; }; const server = await internals.server(handler); const agent = new Http.Agent({ maxSockets: 1 }); - expect(Object.keys(agent.sockets).length).to.equal(0); + expect(Object.keys(agent.sockets).length).toBe(0); const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { agent, timeout: 15 }); - expect(Object.keys(agent.sockets).length).to.equal(1); - expect(Object.keys(agent.requests).length).to.equal(0); + expect(Object.keys(agent.sockets).length).toBe(1); + expect(Object.keys(agent.requests).length).toBe(0); - const err = await expect(Wreck.request('get', `http://localhost:${server.address().port}` + '/thatone', { agent, timeout: 15 })).to.reject(); - expect(err.output.statusCode).to.equal(504); + const err = await internals.rejection( + Wreck.request('get', `http://localhost:${server.address().port}` + '/thatone', { agent, timeout: 15 }), + ); + expect(err.output.statusCode).toBe(504); - expect(Object.keys(agent.sockets).length).to.equal(1); - expect(Object.keys(agent.requests).length).to.equal(1); + expect(Object.keys(agent.sockets).length).toBe(1); + expect(Object.keys(agent.requests).length).toBe(1); complete(); await Wreck.read(res); await Hoek.wait(100); - expect(Object.keys(agent.sockets).length).to.equal(0); - expect(Object.keys(agent.requests).length).to.equal(0); + expect(Object.keys(agent.sockets).length).toBe(0); + expect(Object.keys(agent.requests).length).toBe(0); }); - it('defaults maxSockets to Infinity', async (flags) => { - + it('defaults maxSockets to Infinity', async () => { const server = await internals.server(); const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { timeout: 100 }); - expect(res.statusCode).to.equal(200); - expect(Wreck.agents.http.maxSockets).to.equal(Infinity); + expect(res.statusCode).toBe(200); + expect(Wreck.agents.http.maxSockets).toBe(Infinity); }); - it('maxSockets on default agents can be changed', async (flags) => { - + it('maxSockets on default agents can be changed', async () => { let complete; const handler = (req, res) => { - res.writeHead(200); res.write('foo'); - complete = complete || function () { - - res.end(); - }; + complete = + complete || + function () { + res.end(); + }; }; const server = await internals.server(handler); @@ -1177,8 +1119,10 @@ describe('request()', () => { const res = await Wreck.request('get', `http://localhost:${server.address().port}`, { timeout: 15 }); - const err = await expect(Wreck.request('get', `http://localhost:${server.address().port}` + '/thatone', { timeout: 15 })).to.reject(); - expect(err.output.statusCode).to.equal(504); + const err = await internals.rejection( + Wreck.request('get', `http://localhost:${server.address().port}` + '/thatone', { timeout: 15 }), + ); + expect(err.output.statusCode).toBe(504); complete(); @@ -1186,373 +1130,362 @@ describe('request()', () => { Wreck.agents.http.maxSockets = Infinity; }); - it('sets the auth value on the request', async (flags) => { - + it('sets the auth value on the request', async () => { const server = await internals.server('ok'); - const promise = Wreck.request('get', '/foo', { baseUrl: `http://username:password@localhost:${server.address().port}` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - expect(promise.req.getHeader('authorization')).to.exist(); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); + const promise = Wreck.request('get', '/foo', { + baseUrl: `http://username:password@localhost:${server.address().port}`, + }); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); + expect(promise.req.getHeader('authorization')).toBeDefined(); }); - it('sets the auth value on the request with missing username', async (flags) => { - + it('sets the auth value on the request with missing username', async () => { const server = await internals.server('ok'); - const promise = Wreck.request('get', '/foo', { baseUrl: `http://:password@localhost:${server.address().port}/` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - expect(promise.req.getHeader('authorization')).to.exist(); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); + const promise = Wreck.request('get', '/foo', { + baseUrl: `http://:password@localhost:${server.address().port}/`, + }); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); + expect(promise.req.getHeader('authorization')).toBeDefined(); }); - describe('unix socket', { skip: process.platform === 'win32' }, () => { - - it('requests a resource', async (flags) => { - + describe.skipIf(process.platform === 'win32')('unix socket', () => { + it('requests a resource', async () => { const server = await internals.server(null, internals.socket); + onTestFinished(() => server.close()); const res = await Wreck.request('get', '/', { socketPath: internals.socket }); const body = await Wreck.read(res); - expect(Buffer.isBuffer(body)).to.equal(true); - expect(body.toString()).to.equal(internals.payload); - flags.onCleanup = () => server.close(); + expect(Buffer.isBuffer(body)).toBe(true); + expect(body.toString()).toBe(internals.payload); }); - it('requests a resource at a subpath', async (flags) => { - + it('requests a resource at a subpath', async () => { const server = await internals.server(null, internals.socket); + onTestFinished(() => server.close()); const res = await Wreck.request('get', '/subpath', { socketPath: internals.socket }); - expect(res.req.path).to.equal('/subpath'); - flags.onCleanup = () => server.close(); + expect(res.req.path).toBe('/subpath'); }); - it('requests a resource at a subpath with a default top level path', async (flags) => { - + it('requests a resource at a subpath with a default top level path', async () => { const server = await internals.server(null, internals.socket); + onTestFinished(() => server.close()); const wreck = Wreck.defaults({ socketPath: internals.socket }); const res = await wreck.request('get', '/subpath'); - expect(res.req.path).to.equal('/subpath'); - flags.onCleanup = () => server.close(); + expect(res.req.path).toBe('/subpath'); }); - it('requests a POST resource', async (flags) => { - + it('requests a POST resource', async () => { const server = await internals.server('echo', internals.socket); + onTestFinished(() => server.close()); const res = await Wreck.request('post', '/', { socketPath: internals.socket, payload: internals.payload }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); - flags.onCleanup = () => server.close(); + expect(body.toString()).toBe(internals.payload); }); - it('requests a POST resource with unicode characters in payload', async (flags) => { - + it('requests a POST resource with unicode characters in payload', async () => { const server = await internals.server('echo', internals.socket); + onTestFinished(() => server.close()); const unicodePayload = JSON.stringify({ field: 'ć' }); const res = await Wreck.request('post', '/', { socketPath: internals.socket, payload: unicodePayload }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(unicodePayload); - flags.onCleanup = () => server.close(); + expect(body.toString()).toBe(unicodePayload); }); - it('should not overwrite content-length if it is already in the headers', async (flags) => { - + it('should not overwrite content-length if it is already in the headers', async () => { const server = await internals.server('echo', internals.socket); - const options = { socketPath: internals.socket, payload: internals.payload, headers: { 'Content-Length': '16390' } }; + onTestFinished(() => server.close()); + const options = { + socketPath: internals.socket, + payload: internals.payload, + headers: { 'Content-Length': '16390' }, + }; const res = await Wreck.request('post', '/', options); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); - flags.onCleanup = () => server.close(); + expect(body.toString()).toBe(internals.payload); }); - it('requests a POST resource with headers', async (flags) => { - + it('requests a POST resource with headers', async () => { const server = await internals.server('echo', internals.socket); - const res = await Wreck.request('post', '/', { socketPath: internals.socket, headers: { 'user-agent': 'wreck' }, payload: internals.payload }); + onTestFinished(() => server.close()); + const res = await Wreck.request('post', '/', { + socketPath: internals.socket, + headers: { 'user-agent': 'wreck' }, + payload: internals.payload, + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); - flags.onCleanup = () => server.close(); + expect(body.toString()).toBe(internals.payload); }); - it('requests a POST resource with stream payload', async (flags) => { - + it('requests a POST resource with stream payload', async () => { const server = await internals.server('echo', internals.socket); - const res = await Wreck.request('post', '/', { socketPath: internals.socket, payload: Wreck.toReadableStream(internals.payload) }); + onTestFinished(() => server.close()); + const res = await Wreck.request('post', '/', { + socketPath: internals.socket, + payload: Wreck.toReadableStream(internals.payload), + }); const body = await Wreck.read(res); - expect(body.toString()).to.equal(internals.payload); - flags.onCleanup = () => server.close(); + expect(body.toString()).toBe(internals.payload); }); - it('requests a POST resource with headers using post shortcut', async (flags) => { - + it('requests a POST resource with headers using post shortcut', async () => { const server = await internals.server('echo', internals.socket); - const { payload } = await Wreck.post('/', { socketPath: internals.socket, headers: { 'user-agent': 'wreck' }, payload: internals.payload }); - expect(payload.toString()).to.equal(internals.payload); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); + const { payload } = await Wreck.post('/', { + socketPath: internals.socket, + headers: { 'user-agent': 'wreck' }, + payload: internals.payload, + }); + expect(payload.toString()).toBe(internals.payload); }); }); - it('errors on unix socket under Windows', { skip: process.platform !== 'win32' }, async () => { - - await expect(Wreck.request('get', '/', { socketPath: '/some/path/to/nothing' })).to.reject(); + it.skipIf(process.platform !== 'win32')('errors on unix socket under Windows', async () => { + await expect(Wreck.request('get', '/', { socketPath: '/some/path/to/nothing' })).rejects.toThrow(); }); }); describe('options.lookup', () => { - - it('uses the lookup function to resolve the server ip address', async (flags) => { - + it('uses the lookup function to resolve the server ip address', async () => { let dnsLookupCalled = false; const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); await Wreck.request('get', `http://localhost:${server.address().port}/`, { lookup: (hostname, options, callback) => { - dnsLookupCalled = true; return Dns.lookup(hostname, options, callback); - } + }, }); - expect(dnsLookupCalled).to.equal(true); + expect(dnsLookupCalled).toBe(true); }); - it('uses the lookup function and fails if the lookup function rejects the domain', async (flags) => { - + it('uses the lookup function and fails if the lookup function rejects the domain', async () => { const server = await internals.server('ok'); + onTestFinished(() => server.close()); const promise = Wreck.request('get', `http://localhost:${server.address().port}/`, { - lookup: (_hostname, _options, callback) => callback(new Error('failed lookup')) + lookup: (_hostname, _options, callback) => callback(new Error('failed lookup')), }); - await expect(promise).to.reject('Client request error: failed lookup'); - flags.onCleanup = () => server.close(); + await expect(promise).rejects.toThrow('Client request error: failed lookup'); }); }); describe('options.hints', () => { - - it('passes the hint parameter to the lookup function to resolve the server ip address', async (flags) => { - + it('passes the hint parameter to the lookup function to resolve the server ip address', async () => { const expectedHints = Dns.ADDRCONFIG; let actualHints; const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); await Wreck.request('get', `http://localhost:${server.address().port}/`, { lookup: (hostname, options, callback) => { - actualHints = options.hints; return Dns.lookup(hostname, options, callback); }, - hints: expectedHints + hints: expectedHints, }); - expect(actualHints).to.equal(expectedHints); + expect(actualHints).toBe(expectedHints); }); }); describe('options.family', () => { - - it('passes the family parameter to the lookup function to resolve the server ip address', async (flags) => { - + it('passes the family parameter to the lookup function to resolve the server ip address', async () => { const expectedFamily = 4; let actualFamily; const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); await Wreck.request('get', `http://localhost:${server.address().port}/`, { lookup: (hostname, options, callback) => { - actualFamily = options.family; return Dns.lookup(hostname, options, callback); }, - family: expectedFamily // IPv4 + family: expectedFamily, // IPv4 }); - expect(actualFamily).to.equal(expectedFamily); + expect(actualFamily).toBe(expectedFamily); }); }); describe('options.baseUrl', () => { - - it('uses path when path is a full URL', async (flags) => { - + it('uses path when path is a full URL', async () => { const unboundPort = await internals.unusedPort(); const promise = Wreck.request('get', `http://localhost:${unboundPort}/foo`, { baseUrl: 'http://localhost:0/' }); - await expect(promise).to.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${unboundPort}`); + await expect(promise).rejects.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${unboundPort}`); }); - it('uses lower-case host header when path is not a full URL', async (flags) => { - + it('uses lower-case host header when path is not a full URL', async () => { const server = await internals.server('ok'); - const promise = Wreck.request('get', '/foo', { baseUrl: `http://localhost:${server.address().port}`, headers: { host: 'localhost:8080' } }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal('localhost:8080'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); + const promise = Wreck.request('get', '/foo', { + baseUrl: `http://localhost:${server.address().port}`, + headers: { host: 'localhost:8080' }, + }); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe('localhost:8080'); }); - it('uses upper-case host header when path is not a full URL', async (flags) => { - + it('uses upper-case host header when path is not a full URL', async () => { const server = await internals.server('ok'); - const promise = Wreck.request('get', '/foo', { baseUrl: `http://localhost:${server.address().port}/`, headers: { Host: 'localhost:8080' } }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal('localhost:8080'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); + const promise = Wreck.request('get', '/foo', { + baseUrl: `http://localhost:${server.address().port}/`, + headers: { Host: 'localhost:8080' }, + }); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe('localhost:8080'); }); - it('ignores host header when it is undefined', async (flags) => { - + it('ignores host header when it is undefined', async () => { const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); - const promise = Wreck.request('get', '/foo', { baseUrl: `http://localhost:${server.address().port}/`, headers: { host: undefined } }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); + onTestFinished(() => server.close()); + const promise = Wreck.request('get', '/foo', { + baseUrl: `http://localhost:${server.address().port}/`, + headers: { host: undefined }, + }); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); }); - it('uses baseUrl option with trailing slash and uri is prefixed with a slash', async (flags) => { - + it('uses baseUrl option with trailing slash and uri is prefixed with a slash', async () => { const server = await internals.server('ok'); + onTestFinished(() => server.close()); const promise = Wreck.request('get', '/foo', { baseUrl: `http://localhost:${server.address().port}/` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - flags.onCleanup = () => server.close(); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); }); - it('uses baseUrl option without trailing slash and uri is prefixed with a slash', async (flags) => { - + it('uses baseUrl option without trailing slash and uri is prefixed with a slash', async () => { const server = await internals.server('ok'); + onTestFinished(() => server.close()); const promise = Wreck.request('get', '/foo', { baseUrl: `http://localhost:${server.address().port}` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - expect(promise.req.path).to.equal('/foo'); - flags.onCleanup = () => server.close(); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); + expect(promise.req.path).toBe('/foo'); }); - it('uses baseUrl option with trailing slash and uri is prefixed without a slash', async (flags) => { - + it('uses baseUrl option with trailing slash and uri is prefixed without a slash', async () => { const server = await internals.server('ok'); + onTestFinished(() => server.close()); const promise = Wreck.request('get', 'foo', { baseUrl: `http://localhost:${server.address().port}/` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - expect(promise.req.path).to.equal('/foo'); - flags.onCleanup = () => server.close(); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); + expect(promise.req.path).toBe('/foo'); }); - it('uses baseUrl option without trailing slash and uri is prefixed without a slash', async (flags) => { - + it('uses baseUrl option without trailing slash and uri is prefixed without a slash', async () => { const server = await internals.server('ok'); + onTestFinished(() => server.close()); const promise = Wreck.request('get', 'foo', { baseUrl: `http://localhost:${server.address().port}` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - expect(promise.req.path).to.equal('/foo'); - flags.onCleanup = () => server.close(); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); + expect(promise.req.path).toBe('/foo'); }); - it('uses baseUrl option when uri is an empty string', async (flags) => { - + it('uses baseUrl option when uri is an empty string', async () => { const server = await internals.server('ok'); + onTestFinished(() => server.close()); const promise = Wreck.request('get', '', { baseUrl: `http://localhost:${server.address().port}` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - expect(promise.req.path).to.equal('/'); - flags.onCleanup = () => server.close(); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); + expect(promise.req.path).toBe('/'); }); - it('uses baseUrl option with a path', async (flags) => { - + it('uses baseUrl option with a path', async () => { const server = await internals.server('ok'); + onTestFinished(() => server.close()); const promise = Wreck.request('get', '/bar', { baseUrl: `http://localhost:${server.address().port}/foo` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - expect(promise.req.path).to.equal('/bar'); - flags.onCleanup = () => server.close(); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); + expect(promise.req.path).toBe('/bar'); }); - it('uses baseUrl option with a relative path', async (flags) => { - + it('uses baseUrl option with a relative path', async () => { const server = await internals.server('ok'); + onTestFinished(() => server.close()); const promise = Wreck.request('get', 'bar', { baseUrl: `http://localhost:${server.address().port}/foo/` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - expect(promise.req.path).to.equal('/foo/bar'); - flags.onCleanup = () => server.close(); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); + expect(promise.req.path).toBe('/foo/bar'); }); - it('uses baseUrl option with a path and removes extra slashes', async (flags) => { - + it('uses baseUrl option with a path and removes extra slashes', async () => { const server = await internals.server('ok'); + onTestFinished(() => server.close()); const promise = Wreck.request('get', '/bar', { baseUrl: `http://localhost:${server.address().port}/foo/` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - expect(promise.req.path).to.equal('/bar'); - flags.onCleanup = () => server.close(); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); + expect(promise.req.path).toBe('/bar'); }); - it('uses baseUrl option with a url that has a querystring', async (flags) => { - + it('uses baseUrl option with a url that has a querystring', async () => { const server = await internals.server('ok'); - const promise = Wreck.request('get', 'bar?test=hello', { baseUrl: `http://localhost:${server.address().port}/foo/` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - expect(promise.req.path).to.equal('/foo/bar?test=hello'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); + const promise = Wreck.request('get', 'bar?test=hello', { + baseUrl: `http://localhost:${server.address().port}/foo/`, + }); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); + expect(promise.req.path).toBe('/foo/bar?test=hello'); }); - it('uses baseUrl option with a url that has a querystring will override any base querystring', async (flags) => { - + it('uses baseUrl option with a url that has a querystring will override any base querystring', async () => { const server = await internals.server('ok'); - const promise = Wreck.request('get', 'bar?test=hello', { baseUrl: `http://localhost:${server.address().port}/foo/?test=hi` }); - await expect(promise).to.not.reject(); - expect(promise.req.getHeader('host')).to.equal(`localhost:${server.address().port}`); - expect(promise.req.path).to.equal('/foo/bar?test=hello'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); + const promise = Wreck.request('get', 'bar?test=hello', { + baseUrl: `http://localhost:${server.address().port}/foo/?test=hi`, + }); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.getHeader('host')).toBe(`localhost:${server.address().port}`); + expect(promise.req.path).toBe('/foo/bar?test=hello'); }); }); describe('read()', () => { - - it('handles errors with a boom response', async (flags) => { - + it('handles errors with a boom response', async () => { const res = new Events.EventEmitter(); - res.pipe = function () { }; + res.pipe = function () {}; const promise = Wreck.read(res); res.emit('error', new Error('my error')); - const err = await expect(promise).to.reject('Payload stream error: my error'); - expect(err.isBoom).to.equal(true); - expect(err.output.statusCode).to.equal(500); + const err = await internals.rejection(promise); + expect(err.message).toBe('Payload stream error: my error'); + expect(err.isBoom).toBe(true); + expect(err.output.statusCode).toBe(500); }); - it('retains boom response error', async (flags) => { - + it('retains boom response error', async () => { const res = new Events.EventEmitter(); - res.pipe = function () { }; + res.pipe = function () {}; const promise = Wreck.read(res); res.emit('error', Boom.badRequest('You messed up')); - const err = await expect(promise).to.reject('You messed up'); - expect(err.isBoom).to.equal(true); - expect(err.output.statusCode).to.equal(400); + const err = await internals.rejection(promise); + expect(err.message).toBe('You messed up'); + expect(err.isBoom).toBe(true); + expect(err.output.statusCode).toBe(400); }); - it('handles "close" emit', async (flags) => { - + it('handles "close" emit', async () => { const res = new Events.EventEmitter(); - res.pipe = function () { }; + res.pipe = function () {}; const promise = Wreck.read(res); res.emit('close'); - const err = await expect(promise).to.reject(); - expect(err.isBoom).to.equal(true); + const err = await internals.rejection(promise); + expect(err.isBoom).toBe(true); }); - it('handles requests that close early', async (flags) => { - + it('handles requests that close early', async () => { let readPromise; let readError; const handler = (req, res) => { - readPromise = Wreck.read(req).catch((err) => { - readError = err; }); promise.req.abort(); @@ -1561,7 +1494,6 @@ describe('read()', () => { const payload = new Stream.Readable(); let written = 0; payload._read = function () { - if (written < 1) { this.push(Buffer.alloc(1)); ++written; @@ -1569,21 +1501,19 @@ describe('read()', () => { }; const headers = { - 'content-length': '123' + 'content-length': '123', }; const server = await internals.server(handler); const promise = Wreck.request('post', `http://localhost:${server.address().port}`, { payload, headers }); - await expect(promise).to.reject(); + await expect(promise).rejects.toThrow(); await readPromise; - expect(readError).to.be.an.error('Payload stream closed prematurely'); - expect(readError.isBoom).to.equal(true); + expect(readError.message).toBe('Payload stream closed prematurely'); + expect(readError.isBoom).toBe(true); }); - it('errors on partial payload transfers', async (flags) => { - + it('errors on partial payload transfers', async () => { const handler = (req, res) => { - res.setHeader('content-length', 2000); res.writeHead(200); res.write(internals.payload.slice(0, 1000)); @@ -1592,113 +1522,103 @@ describe('read()', () => { const server = await internals.server(handler); const res = await Wreck.request('get', `http://localhost:${server.address().port}`); - expect(res.statusCode).to.equal(200); - expect(res.headers['transfer-encoding']).to.not.exist(); - const err = await expect(Wreck.read(res)).to.reject(Error, 'Payload stream closed prematurely'); - expect(err.isBoom).to.equal(true); + expect(res.statusCode).toBe(200); + expect(res.headers['transfer-encoding']).toBeUndefined(); + const err = await internals.rejection(Wreck.read(res)); + expect(err.message).toBe('Payload stream closed prematurely'); + expect(err.isBoom).toBe(true); }); - it('errors on partial payload transfers (chunked)', async (flags) => { - + it('errors on partial payload transfers (chunked)', async () => { const handler = (req, res) => { - res.writeHead(200); res.write(internals.payload); setTimeout(() => { - res.destroy(new Error('go away')); }, 10); }; const server = await internals.server(handler); const res = await Wreck.request('get', `http://localhost:${server.address().port}`); - expect(res.statusCode).to.equal(200); - expect(res.headers['transfer-encoding']).to.equal('chunked'); - const err = await expect(Wreck.read(res)).to.reject(Error, 'Payload stream closed prematurely'); - expect(err.isBoom).to.equal(true); + expect(res.statusCode).toBe(200); + expect(res.headers['transfer-encoding']).toBe('chunked'); + const err = await internals.rejection(Wreck.read(res)); + expect(err.message).toBe('Payload stream closed prematurely'); + expect(err.isBoom).toBe(true); }); - it('will not pipe the stream if no socket can be established', async (flags) => { - + it('will not pipe the stream if no socket can be established', async () => { const agent = new internals.SlowAgent(); const stream = new Stream.Readable({ read() { - read = true; this.push(null); - } + }, }); let read = false; const promiseA = Wreck.request('post', 'http://localhost:0', { agent, - payload: stream + payload: stream, }); - await expect(promiseA).to.reject(Error, /Unable to obtain socket/); - expect(read).to.equal(false); + await expect(promiseA).rejects.toThrow(/Unable to obtain socket/); + expect(read).toBe(false); const handler = (req, res) => { - res.writeHead(200); res.end(internals.payload); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { - payload: stream + payload: stream, }); - expect(res.statusCode).to.equal(200); - expect(read).to.equal(true); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(read).toBe(true); }); - it('will handle stream payload errors between request creation and connection establishment', async (flags) => { - + it('will handle stream payload errors between request creation and connection establishment', async () => { const agent = new internals.SlowAgent(); const stream = new Stream.Readable(); const promiseA = Wreck.request('post', 'http://localhost:0', { agent, - payload: stream + payload: stream, }); process.nextTick(() => { - stream.emit('error', new Error('Asynchronous stream error')); }); - await expect(promiseA).to.reject(Error, /Asynchronous stream error/); + await expect(promiseA).rejects.toThrow(/Asynchronous stream error/); }); - it('will handle requests with payloads using re-used sockets', async (flags) => { - + it('will handle requests with payloads using re-used sockets', async () => { const server = await internals.server('echo'); const agent = new Http.Agent({ - keepAlive: true + keepAlive: true, }); const streamA = Wreck.toReadableStream('hello world', 'utf8'); const { payload: payloadA } = await Wreck.post(`http://localhost:${server.address().port}`, { agent, - payload: streamA + payload: streamA, }); - expect(payloadA.toString('utf8')).to.equal('hello world'); + expect(payloadA.toString('utf8')).toBe('hello world'); const streamB = Wreck.toReadableStream('hello world', 'utf8'); const { payload: payloadB } = await Wreck.post(`http://localhost:${server.address().port}`, { agent, - payload: streamB + payload: streamB, }); - expect(payloadB.toString('utf8')).to.equal('hello world'); + expect(payloadB.toString('utf8')).toBe('hello world'); }); - it('times out when stream read takes too long', async (flags) => { - + it('times out when stream read takes too long', async () => { const TestStream = class extends Stream.Readable { - _read(size) { - if (this.isDone) { return; } @@ -1708,232 +1628,205 @@ describe('read()', () => { this.push('x'); this.push('y'); setTimeout(() => { - this.push(null); }, 200); } }; - const err = await expect(Wreck.read(new TestStream(), { timeout: 100 })).to.reject(); - expect(err).to.exist(); - expect(err.output.statusCode).to.equal(408); + const err = await internals.rejection(Wreck.read(new TestStream(), { timeout: 100 })); + expect(err).toBeDefined(); + expect(err.output.statusCode).toBe(408); }); - it('errors when stream is too big', async (flags) => { - + it('errors when stream is too big', async () => { const server = await internals.server(); + onTestFinished(() => server.close()); const res = await Wreck.request('get', `http://localhost:${server.address().port}`); - const err = await expect(Wreck.read(res, { maxBytes: 120 })).to.reject(); - expect(err.output.statusCode).to.equal(413); - flags.onCleanup = () => server.close(); + const err = await internals.rejection(Wreck.read(res, { maxBytes: 120 })); + expect(err.output.statusCode).toBe(413); }); - it('ignores maxBytes when stream is not too big', async (flags) => { - + it('ignores maxBytes when stream is not too big', async () => { const server = await internals.server(); + onTestFinished(() => server.close()); const res = await Wreck.request('get', `http://localhost:${server.address().port}`); await Wreck.read(res, { maxBytes: 120000 }); - flags.onCleanup = () => server.close(); }); - it('reads a file streamed via HTTP', async (flags) => { - + it('reads a file streamed via HTTP', async () => { const path = Path.join(__dirname, '../LICENSE.md'); const stats = Fs.statSync(path); const fileStream = Fs.createReadStream(path); const handler = (req, res) => { - res.writeHead(200); fileStream.pipe(res); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const res = await Wreck.request('get', `http://localhost:${server.address().port}`); - expect(res.statusCode).to.equal(200); + expect(res.statusCode).toBe(200); const body = await Wreck.read(res); - expect(body.length).to.equal(stats.size); - flags.onCleanup = () => server.close(); + expect(body.length).toBe(stats.size); }); - it('reads a multiple buffers response', async (flags) => { - + it('reads a multiple buffers response', async () => { const path = Path.join(__dirname, '../LICENSE.md'); const stats = Fs.statSync(path); const file = Fs.readFileSync(path); const handler = (req, res) => { - res.writeHead(200); res.write(file); setTimeout(() => { - res.write(file); res.end(); }, 100); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const res = await Wreck.request('get', `http://localhost:${server.address().port}`); - expect(res.statusCode).to.equal(200); + expect(res.statusCode).toBe(200); const body = await Wreck.read(res); - expect(body.length).to.equal(stats.size * 2); - flags.onCleanup = () => server.close(); + expect(body.length).toBe(stats.size * 2); }); - it('writes a file streamed via HTTP', async (flags) => { - + it('writes a file streamed via HTTP', async () => { const path = Path.join(__dirname, '../LICENSE.md'); const stats = Fs.statSync(path); const fileStream = Fs.createReadStream(path); const handler = async (req, res) => { - res.writeHead(200); res.end(await Wreck.read(req)); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const res = await Wreck.request('post', `http://localhost:${server.address().port}`, { payload: fileStream }); - expect(res.statusCode).to.equal(200); + expect(res.statusCode).toBe(200); const body = await Wreck.read(res); - expect(body.length).to.equal(stats.size); - flags.onCleanup = () => server.close(); + expect(body.length).toBe(stats.size); }); - it('handles responses with no headers', async (flags) => { - + it('handles responses with no headers', async () => { const res = Wreck.toReadableStream(internals.payload); await Wreck.read(res, { json: true }); }); - it('handles responses with no headers (with gunzip)', async (flags) => { - + it('handles responses with no headers (with gunzip)', async () => { const res = Wreck.toReadableStream(internals.gzippedPayload); await Wreck.read(res, { json: true, gunzip: true }); }); - it('skips destroy when not available', async (flags) => { - + it('skips destroy when not available', async () => { const server = await internals.server(); + onTestFinished(() => server.close()); const res = await Wreck.request('get', `http://localhost:${server.address().port}`); res.destroy = null; res._readableState.autoDestroy = false; // As of node v16 autoDestroy is on, causing node to attempt to call destroy() - const err = await expect(Wreck.read(res, { maxBytes: 120 })).to.reject(); - expect(err.output.statusCode).to.equal(413); - flags.onCleanup = () => server.close(); + const err = await internals.rejection(Wreck.read(res, { maxBytes: 120 })); + expect(err.output.statusCode).toBe(413); }); }); describe('parseCacheControl()', () => { - it('parses valid header', () => { - const header = Wreck.parseCacheControl('must-revalidate, max-age=3600'); - expect(header).to.exist(); - expect(header['must-revalidate']).to.equal(true); - expect(header['max-age']).to.equal(3600); + expect(header).not.toBeNull(); + expect(header['must-revalidate']).toBe(true); + expect(header['max-age']).toBe(3600); }); it('parses valid header with quoted string', () => { - const header = Wreck.parseCacheControl('must-revalidate, max-age="3600"'); - expect(header).to.exist(); - expect(header['must-revalidate']).to.equal(true); - expect(header['max-age']).to.equal(3600); + expect(header).not.toBeNull(); + expect(header['must-revalidate']).toBe(true); + expect(header['max-age']).toBe(3600); }); it('errors on invalid header', () => { - const header = Wreck.parseCacheControl('must-revalidate, b =3600'); - expect(header).to.not.exist(); + expect(header).toBeNull(); }); it('errors on invalid max-age', () => { - const header = Wreck.parseCacheControl('must-revalidate, max-age=a3600'); - expect(header).to.not.exist(); + expect(header).toBeNull(); }); }); describe('Shortcut', () => { - - it('get request', async (flags) => { - + it('get request', async () => { const server = await internals.server('ok'); + onTestFinished(() => server.close()); const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`); - expect(res.statusCode).to.equal(200); - expect(payload.toString()).to.equal('ok'); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(payload.toString()).toBe('ok'); }); - it('post request', async (flags) => { - + it('post request', async () => { const server = await internals.server('ok'); + onTestFinished(() => server.close()); const { res, payload } = await Wreck.post(`http://localhost:${server.address().port}`, { payload: '123' }); - expect(res.statusCode).to.equal(200); - expect(payload.toString()).to.equal('ok'); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(payload.toString()).toBe('ok'); }); - it('patch request', async (flags) => { - + it('patch request', async () => { const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const { res, payload } = await Wreck.patch(`http://localhost:${server.address().port}`, { payload: '123' }); - expect(res.statusCode).to.equal(200); - expect(payload.toString()).to.equal('ok'); + expect(res.statusCode).toBe(200); + expect(payload.toString()).toBe('ok'); }); - it('put request', async (flags) => { - + it('put request', async () => { const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const { res, payload } = await Wreck.put(`http://localhost:${server.address().port}`); - expect(res.statusCode).to.equal(200); - expect(payload.toString()).to.equal('ok'); + expect(res.statusCode).toBe(200); + expect(payload.toString()).toBe('ok'); }); - it('delete request', async (flags) => { - + it('delete request', async () => { const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const { res, payload } = await Wreck.delete(`http://localhost:${server.address().port}`); - expect(res.statusCode).to.equal(200); - expect(payload.toString()).to.equal('ok'); + expect(res.statusCode).toBe(200); + expect(payload.toString()).toBe('ok'); }); - it('delete request with payload', async (flags) => { - + it('delete request with payload', async () => { const handler = (req, res) => { - - expect(req.headers['content-length']).to.equal('16390'); + expect(req.headers['content-length']).toBe('16390'); res.writeHead(200, { 'Content-Type': 'text/plain' }); req.pipe(res); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - const { res, payload } = await Wreck.delete('http://localhost:' + server.address().port, { payload: internals.payload }); - expect(res.statusCode).to.equal(200); - expect(payload.toString()).to.equal(internals.payload); + onTestFinished(() => server.close()); + const { res, payload } = await Wreck.delete('http://localhost:' + server.address().port, { + payload: internals.payload, + }); + expect(res.statusCode).toBe(200); + expect(payload.toString()).toBe(internals.payload); }); - it('errors on bad request', async (flags) => { - + it('errors on bad request', async () => { const server = await internals.server('fail'); - flags.onCleanup = () => server.close(); - await expect(Wreck.get(`http://localhost:${server.address().port}`)).to.reject(); + onTestFinished(() => server.close()); + await expect(Wreck.get(`http://localhost:${server.address().port}`)).rejects.toThrow(); }); - it('handles error responses with a boom error object', async (flags) => { - + it('handles error responses with a boom error object', async () => { const handler = (req, res) => { - res.setHeader('content-type', 'application/json'); res.setHeader('x-custom', 'yes'); res.writeHead(400); @@ -1941,514 +1834,468 @@ describe('Shortcut', () => { }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); - - const err = await expect(Wreck.get('http://127.0.0.1:' + server.address().port, { json: true })).to.reject(); - expect(err.isBoom).to.be.true(); - expect(err.message).to.equal('Response Error: 400 Bad Request'); - expect(err.data.isResponseError).to.be.true(); - expect(err.data.headers).to.include({ 'x-custom': 'yes' }); - expect(err.data.payload).to.equal({ details: 'failed' }); - expect(err.data.res.statusCode).to.equal(400); + onTestFinished(() => server.close()); + const err = await internals.rejection(Wreck.get('http://127.0.0.1:' + server.address().port, { json: true })); + expect(err.isBoom).toBe(true); + expect(err.message).toBe('Response Error: 400 Bad Request'); + expect(err.data.isResponseError).toBe(true); + expect(err.data.headers).toMatchObject({ 'x-custom': 'yes' }); + expect(err.data.payload).toStrictEqual({ details: 'failed' }); + expect(err.data.res.statusCode).toBe(400); }); }); describe('json', () => { - - it('json requested and received', async (flags) => { - + it('json requested and received', async () => { const handler = (req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ foo: 'bar' })); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: true }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload).to.not.equal(null); - expect(payload.foo).to.exist(); + expect(res.statusCode).toBe(200); + expect(payload).not.toBe(null); + expect(payload.foo).toBeDefined(); }); - it('json-based type requested and received', async (flags) => { - + it('json-based type requested and received', async () => { const handler = (req, res) => { - res.writeHead(200, { 'Content-Type': 'application/vnd.api+json' }); res.end(JSON.stringify({ foo: 'bar' })); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: true }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload).to.not.equal(null); - expect(payload.foo).to.exist(); + expect(res.statusCode).toBe(200); + expect(payload).not.toBe(null); + expect(payload.foo).toBeDefined(); }); - it('json requested but not received - flag is ignored', async (flags) => { - + it('json requested but not received - flag is ignored', async () => { const server = await internals.server('ok'); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: true }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload).to.not.equal(null); + expect(res.statusCode).toBe(200); + expect(payload).not.toBe(null); }); - it('invalid json received', async (flags) => { - + it('invalid json received', async () => { const handler = (req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); res.end('ok'); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: true }; - await expect(Wreck.get(`http://localhost:${server.address().port}`, options)).to.reject(); + await expect(Wreck.get(`http://localhost:${server.address().port}`, options)).rejects.toThrow(); }); - it('json not requested but received as string', async (flags) => { - + it('json not requested but received as string', async () => { const handler = (req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ foo: 'bar' })); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: false }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload).to.not.equal(null); + expect(res.statusCode).toBe(200); + expect(payload).not.toBe(null); }); - it('should not be parsed on empty buffer (json: SMART)', async (flags) => { - + it('should not be parsed on empty buffer (json: SMART)', async () => { const handler = (req, res) => { - res.writeHead(204, { 'Content-Type': 'application/json' }); res.end(); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: 'SMART' }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(204); - expect(payload).to.equal(null); + expect(res.statusCode).toBe(204); + expect(payload).toBe(null); }); - it('should not be parsed on empty buffer (json: force)', async (flags) => { - + it('should not be parsed on empty buffer (json: force)', async () => { const handler = (req, res) => { - res.writeHead(204, { 'Content-Type': 'application/json' }); res.end(); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: 'force' }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(204); - expect(payload).to.equal(null); + expect(res.statusCode).toBe(204); + expect(payload).toBe(null); }); - it('should return the empty buffer on text content-type (json: true)', async (flags) => { - + it('should return the empty buffer on text content-type (json: true)', async () => { const handler = (req, res) => { - res.writeHead(204, { 'Content-Type': 'text/plain' }); res.end(); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: true }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(204); - expect(Buffer.isBuffer(payload)).to.equal(true); - expect(payload.toString()).to.equal(''); + expect(res.statusCode).toBe(204); + expect(Buffer.isBuffer(payload)).toBe(true); + expect(payload.toString()).toBe(''); }); - it('should return null on empty buffer with text content-type (json: force)', async (flags) => { - + it('should return null on empty buffer with text content-type (json: force)', async () => { const handler = (req, res) => { - res.writeHead(204, { 'Content-Type': 'text/plain' }); res.end(); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: 'force' }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(204); - expect(payload).to.equal(null); + expect(res.statusCode).toBe(204); + expect(payload).toBe(null); }); - it('will try to parse json in "force" mode, regardless of the header', async (flags) => { - + it('will try to parse json in "force" mode, regardless of the header', async () => { const handler = (req, res) => { - res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(JSON.stringify({ foo: 'bar' })); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: 'force' }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload).to.not.equal(null); - expect(payload).to.equal({ foo: 'bar' }); + expect(res.statusCode).toBe(200); + expect(payload).not.toBe(null); + expect(payload).toStrictEqual({ foo: 'bar' }); }); - it('will error on invalid json received in "force" mode', async (flags) => { - + it('will error on invalid json received in "force" mode', async () => { const handler = (req, res) => { - res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('ok'); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: 'force' }; - await expect(Wreck.get(`http://localhost:${server.address().port}`, options)).to.reject(); + await expect(Wreck.get(`http://localhost:${server.address().port}`, options)).rejects.toThrow(); }); - it('will try to parse json in "strict" mode', async (flags) => { - + it('will try to parse json in "strict" mode', async () => { const handler = (req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ foo: 'bar' })); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: 'strict' }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload).to.not.equal(null); - expect(payload).to.equal({ foo: 'bar' }); + expect(res.statusCode).toBe(200); + expect(payload).not.toBe(null); + expect(payload).toStrictEqual({ foo: 'bar' }); }); - it('will error on invalid content-type header in "strict" mode', async (flags) => { - + it('will error on invalid content-type header in "strict" mode', async () => { const handler = (req, res) => { - res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(JSON.stringify({ foo: 'bar' })); }; const server = await internals.server(handler); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); const options = { json: 'strict' }; - const err = await expect(Wreck.get(`http://localhost:${server.address().port}`, options)).to.reject(); - expect(err.output.statusCode).to.equal(406); + const err = await internals.rejection(Wreck.get(`http://localhost:${server.address().port}`, options)); + expect(err.output.statusCode).toBe(406); }); }); describe('gunzip', () => { - describe('true', () => { - - it('automatically handles gzip', async (flags) => { - + it('automatically handles gzip', async () => { const handler = (req, res) => { - - expect(req.headers['accept-encoding']).to.equal('gzip'); + expect(req.headers['accept-encoding']).toBe('gzip'); res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' }); res.end(Zlib.gzipSync(JSON.stringify({ foo: 'bar' }))); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const options = { json: true, gunzip: true }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload).to.not.equal(null); - expect(payload.foo).to.exist(); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(payload).not.toBe(null); + expect(payload.foo).toBeDefined(); }); - it('automatically handles gzip (manual header)', async (flags) => { - + it('automatically handles gzip (manual header)', async () => { const handler = (req, res) => { - - expect(req.headers['accept-encoding']).to.equal('gzip'); + expect(req.headers['accept-encoding']).toBe('gzip'); res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' }); res.end(Zlib.gzipSync(JSON.stringify({ foo: 'bar' }))); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const options = { json: true, gunzip: true, headers: { 'accept-encoding': 'gzip' } }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload).to.not.equal(null); - expect(payload.foo).to.exist(); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(payload).not.toBe(null); + expect(payload.foo).toBeDefined(); }); - it('automatically handles gzip (with identity)', async (flags) => { - + it('automatically handles gzip (with identity)', async () => { const handler = (req, res) => { - - expect(req.headers['accept-encoding']).to.equal('gzip'); + expect(req.headers['accept-encoding']).toBe('gzip'); res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip, identity' }); res.end(Zlib.gzipSync(JSON.stringify({ foo: 'bar' }))); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const options = { json: true, gunzip: true }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload).to.not.equal(null); - expect(payload.foo).to.exist(); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(payload).not.toBe(null); + expect(payload.foo).toBeDefined(); }); - it('automatically handles gzip (without json)', async (flags) => { - + it('automatically handles gzip (without json)', async () => { const handler = (req, res) => { - - expect(req.headers['accept-encoding']).to.equal('gzip'); + expect(req.headers['accept-encoding']).toBe('gzip'); res.writeHead(200, { 'Content-Encoding': 'gzip' }); res.end(Zlib.gzipSync(JSON.stringify({ foo: 'bar' }))); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const options = { gunzip: true }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload.toString()).to.equal('{"foo":"bar"}'); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(payload.toString()).toBe('{"foo":"bar"}'); }); - it('automatically handles gzip (ignores when not gzipped)', async (flags) => { - + it('automatically handles gzip (ignores when not gzipped)', async () => { const handler = (req, res) => { - - expect(req.headers['accept-encoding']).to.equal('gzip'); + expect(req.headers['accept-encoding']).toBe('gzip'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ foo: 'bar' })); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const options = { json: true, gunzip: true }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload).to.not.equal(null); - expect(payload.foo).to.exist(); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(payload).not.toBe(null); + expect(payload.foo).toBeDefined(); }); - it('handles gzip errors', async (flags) => { - + it('handles gzip errors', async () => { const handler = (req, res) => { - - expect(req.headers['accept-encoding']).to.equal('gzip'); + expect(req.headers['accept-encoding']).toBe('gzip'); res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' }); res.end(Zlib.gzipSync(JSON.stringify({ foo: 'bar' })).slice(0, 10)); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const options = { json: true, gunzip: true }; - const err = await expect(Wreck.get(`http://localhost:${server.address().port}`, options)).to.reject(); - expect(err).to.be.an.error('unexpected end of file'); - expect(err.data.res.statusCode).to.equal(200); - flags.onCleanup = () => server.close(); + const err = await internals.rejection(Wreck.get(`http://localhost:${server.address().port}`, options)); + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe('unexpected end of file'); + expect(err.data.res.statusCode).toBe(200); }); }); describe('false/undefined', () => { - - it('fails parsing gzipped content', async (flags) => { - + it('fails parsing gzipped content', async () => { const handler = (req, res) => { - - expect(req.headers['accept-encoding']).to.not.exist(); + expect(req.headers['accept-encoding']).toBeUndefined(); res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' }); res.end(Zlib.gzipSync(JSON.stringify({ foo: 'bar' }))); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const options = { json: true }; - const err = await expect(Wreck.get(`http://localhost:${server.address().port}`, options)).to.reject(); - expect(err).to.be.an.error(/Unexpected token/); - expect(Boom.isBoom(err)).to.be.true(); - expect(err.data.res.statusCode).to.equal(200); - expect(err.data.payload).to.equal(Zlib.gzipSync(JSON.stringify({ foo: 'bar' }))); - flags.onCleanup = () => server.close(); + const err = await internals.rejection(Wreck.get(`http://localhost:${server.address().port}`, options)); + expect(err).toBeInstanceOf(Error); + expect(err.message).toMatch(/Unexpected token/); + expect(Boom.isBoom(err)).toBe(true); + expect(err.data.res.statusCode).toBe(200); + expect(err.data.payload.equals(Zlib.gzipSync(JSON.stringify({ foo: 'bar' })))).toBe(true); }); }); describe('force', () => { - - it('forcefully handles gzip', async (flags) => { - + it('forcefully handles gzip', async () => { const handler = (req, res) => { - - expect(req.headers['accept-encoding']).to.equal('gzip'); + expect(req.headers['accept-encoding']).toBe('gzip'); res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' }); res.end(Zlib.gzipSync(JSON.stringify({ foo: 'bar' }))); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const options = { json: true, gunzip: 'force' }; const { res, payload } = await Wreck.get(`http://localhost:${server.address().port}`, options); - expect(res.statusCode).to.equal(200); - expect(payload).to.not.equal(null); - expect(payload.foo).to.exist(); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(payload).not.toBe(null); + expect(payload.foo).toBeDefined(); }); - it('handles gzip errors', async (flags) => { - + it('handles gzip errors', async () => { const handler = (req, res) => { - - expect(req.headers['accept-encoding']).to.equal('gzip'); + expect(req.headers['accept-encoding']).toBe('gzip'); res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' }); res.end(Zlib.gzipSync(JSON.stringify({ foo: 'bar' })).slice(0, 10)); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const options = { json: true, gunzip: 'force' }; - const err = await expect(Wreck.get(`http://localhost:${server.address().port}`, options)).to.reject(); - expect(err).to.be.an.error('unexpected end of file'); - expect(err.data.res.statusCode).to.equal(200); - flags.onCleanup = () => server.close(); + const err = await internals.rejection(Wreck.get(`http://localhost:${server.address().port}`, options)); + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe('unexpected end of file'); + expect(err.data.res.statusCode).toBe(200); }); }); }); describe('toReadableStream()', () => { - it('handle empty payload', () => { - const stream = Wreck.toReadableStream(); - expect(stream instanceof Stream).to.be.true(); - const read = stream.read(); // Make sure read has no problems - expect(read).to.be.null(); + expect(stream instanceof Stream.default).toBe(true); + const read = stream.read(); // Make sure read has no problems + expect(read).toBeNull(); }); it('handle explicit encoding', () => { - const data = 'Hello'; const stream = Wreck.toReadableStream(data, 'ascii'); - expect(stream instanceof Stream).to.be.true(); + expect(stream instanceof Stream.default).toBe(true); const read = stream.read(); - expect(read.toString()).to.equal(data); + expect(read.toString()).toBe(data); }); it('chunks to requested size', () => { - let buf; const data = new Array(101).join('0123456789'); const stream = Wreck.toReadableStream(data); buf = stream.read(100); - expect(buf.length).to.equal(100); + expect(buf.length).toBe(100); buf = stream.read(400); - expect(buf.length).to.equal(400); + expect(buf.length).toBe(400); buf = stream.read(); - expect(buf.length).to.equal(500); + expect(buf.length).toBe(500); buf = stream.read(); - expect(buf).to.equal(null); + expect(buf).toBe(null); }); - it('does not signal end after a partial _read', () => { + it('defers end-of-stream while the payload exceeds the high water mark', () => { + // A single _read() never drains a payload larger than the high water mark, so this is + // the only shape that exercises the branch where the stream is not yet exhausted. + + const hwm = Stream.getDefaultHighWaterMark(false); + const stream = Wreck.toReadableStream('x'.repeat(hwm + 10)); + expect(stream.read(hwm).length).toBe(hwm); + expect(stream.read().length).toBe(10); + expect(stream.read()).toBeNull(); + }); + + it('does not signal end after a partial _read', () => { const data = Buffer.alloc(1000, 'x'); const stream = Wreck.toReadableStream(data); - stream._read(400); // partial read leaves position < length - expect(stream._position).to.equal(400); - expect(stream.readableEnded).to.be.false(); + stream._read(400); // partial read leaves position < length + expect(stream._position).toBe(400); + expect(stream.readableEnded).toBe(false); - stream._read(1000); // drains remainder, signals end - expect(stream._position).to.equal(1000); + stream._read(1000); // drains remainder, signals end + expect(stream._position).toBe(1000); }); }); describe('Events', () => { - - it('emits response event when wreck is finished', async (flags) => { - + it('emits response event when wreck is finished', async () => { const wreck = Wreck.defaults({ events: true }); let once = false; wreck.events.once('response', (err, details) => { - - expect(err).to.not.exist(); - expect(details.req).to.exist(); - expect(details.res).to.exist(); - expect(typeof details.start).to.equal('number'); - expect(details.uri.href).to.equal(`http://localhost:${server.address().port}` + '/'); + expect(err).toBeNull(); + expect(details.req).toBeDefined(); + expect(details.res).toBeDefined(); + expect(typeof details.start).toBe('number'); + expect(details.uri.href).toBe(`http://localhost:${server.address().port}` + '/'); once = true; }); const server = await internals.server('ok'); + onTestFinished(() => server.close()); const { res, payload } = await wreck.put(`http://localhost:${server.address().port}`); - expect(res.statusCode).to.equal(200); - expect(payload.toString()).to.equal('ok'); - expect(once).to.be.true(); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(payload.toString()).toBe('ok'); + expect(once).toBe(true); }); - it('response event includes error when it occurs', async (flags) => { - + it('response event includes error when it occurs', async () => { const wreck = Wreck.defaults({ events: true }); let once = false; wreck.events.once('response', (err, details) => { - - expect(err).to.exist(); - expect(details).to.exist(); - expect(details.req).to.exist(); - expect(details.res).to.not.exist(); + expect(err).toBeDefined(); + expect(details).toBeDefined(); + expect(details.req).toBeDefined(); + expect(details.res).toBeUndefined(); once = true; }); const server = await internals.server('fail'); - await expect(wreck.get(`http://localhost:${server.address().port}`, { timeout: 10 })).to.reject(); - expect(once).to.be.true(); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); + await expect(wreck.get(`http://localhost:${server.address().port}`, { timeout: 10 })).rejects.toThrow(); + expect(once).toBe(true); }); - it('multiple requests execute the same response handler', async (flags) => { - + it('multiple requests execute the same response handler', async () => { let count = 0; const handler = (err, details) => { - - expect(err).to.exist(); - expect(details.req).to.exist(); - expect(details.res).to.not.exist(); + expect(err).toBeDefined(); + expect(details.req).toBeDefined(); + expect(details.res).toBeUndefined(); count++; }; @@ -2456,17 +2303,15 @@ describe('Events', () => { wreck.events.on('response', handler); const server = await internals.server('fail'); - await expect(wreck.get(`http://localhost:${server.address().port}`, { timeout: 10 })).to.reject(); - await expect(wreck.get(`http://localhost:${server.address().port}`, { timeout: 10 })).to.reject(); - expect(count).to.equal(2); - flags.onCleanup = () => server.close(); + onTestFinished(() => server.close()); + await expect(wreck.get(`http://localhost:${server.address().port}`, { timeout: 10 })).rejects.toThrow(); + await expect(wreck.get(`http://localhost:${server.address().port}`, { timeout: 10 })).rejects.toThrow(); + expect(count).toBe(2); }); - it('emits preRequest event before wreck creates a request', async (flags) => { - + it('emits preRequest event before wreck creates a request', async () => { const handler = (req, res) => { - - expect(req.headers.foo).to.equal('bar'); + expect(req.headers.foo).toBe('bar'); res.writeHead(200); res.end('ok'); }; @@ -2474,53 +2319,46 @@ describe('Events', () => { const server = await internals.server(handler); const wreck = Wreck.defaults({ events: true }); wreck.events.once('preRequest', (uri, options) => { - - expect(uri.href).to.equal('http://user:pass@localhost:' + server.address().port + '/'); - expect(options).to.exist(); - expect(uri.auth).to.equal('user:pass'); + expect(uri.href).toBe('http://user:pass@localhost:' + server.address().port + '/'); + expect(options).toBeDefined(); + expect(uri.auth).toBe('user:pass'); uri.headers.foo = 'bar'; }); const { res, payload } = await wreck.put('http://user:pass@localhost:' + server.address().port); - expect(res.statusCode).to.equal(200); - expect(payload.toString()).to.equal('ok'); + expect(res.statusCode).toBe(200); + expect(payload.toString()).toBe('ok'); }); - it('emits request event after wreck creates a request', async (flags) => { - + it('emits request event after wreck creates a request', async () => { const handler = (req, res) => { - res.writeHead(200); res.end('ok'); }; const server = await internals.server(handler); + onTestFinished(() => server.close()); const wreck = Wreck.defaults({ events: true }); wreck.events.once('request', (req) => { - - expect(req).to.exist(); + expect(req).toBeDefined(); }); const { res, payload } = await wreck.put(`http://localhost:${server.address().port}`); - expect(res.statusCode).to.equal(200); - expect(payload.toString()).to.equal('ok'); - flags.onCleanup = () => server.close(); + expect(res.statusCode).toBe(200); + expect(payload.toString()).toBe('ok'); }); }); describe('Defaults', () => { - it('rejects attempts to use defaults without an options hash', () => { - expect(() => { - Wreck.defaults(); - }).to.throw(); + }).toThrow(); }); - it('respects defaults without bleeding across instances', async (flags) => { // Windows takes longer to error - + // Windows takes longer to error + it('respects defaults without bleeding across instances', async () => { const optionsA = { headers: { foo: 123 } }; const optionsB = { headers: { bar: 321 } }; @@ -2529,190 +2367,182 @@ describe('Defaults', () => { const wreckAB = wreckA.defaults(optionsB); const server = await internals.server('ok'); - const promise1 = wreckA.request('get', `http://127.0.0.1:${server.address().port}/`, { headers: { banana: 911 } }); - await expect(promise1).to.not.reject(); - expect(promise1.req.getHeader('banana')).to.exist(); - expect(promise1.req.getHeader('foo')).to.exist(); - expect(promise1.req.getHeader('bar')).to.not.exist(); + onTestFinished(() => server.close()); + const promise1 = wreckA.request('get', `http://127.0.0.1:${server.address().port}/`, { + headers: { banana: 911 }, + }); + await expect(promise1).resolves.not.toThrow(); + expect(promise1.req.getHeader('banana')).toBeDefined(); + expect(promise1.req.getHeader('foo')).toBeDefined(); + expect(promise1.req.getHeader('bar')).toBeUndefined(); - const promise2 = wreckB.request('get', `http://127.0.0.1:${server.address().port}/`, { headers: { banana: 911 } }); - await expect(promise2).to.not.reject(); - expect(promise2.req.getHeader('banana')).to.exist(); - expect(promise2.req.getHeader('foo')).to.not.exist(); - expect(promise2.req.getHeader('bar')).to.exist(); + const promise2 = wreckB.request('get', `http://127.0.0.1:${server.address().port}/`, { + headers: { banana: 911 }, + }); + await expect(promise2).resolves.not.toThrow(); + expect(promise2.req.getHeader('banana')).toBeDefined(); + expect(promise2.req.getHeader('foo')).toBeUndefined(); + expect(promise2.req.getHeader('bar')).toBeDefined(); - const promise3 = wreckAB.request('get', `http://127.0.0.1:${server.address().port}/`, { headers: { banana: 911 } }); - await expect(promise3).to.not.reject(); - expect(promise3.req.getHeader('banana')).to.exist(); - expect(promise3.req.getHeader('foo')).to.exist(); - expect(promise3.req.getHeader('bar')).to.exist(); - flags.onCleanup = () => server.close(); + const promise3 = wreckAB.request('get', `http://127.0.0.1:${server.address().port}/`, { + headers: { banana: 911 }, + }); + await expect(promise3).resolves.not.toThrow(); + expect(promise3.req.getHeader('banana')).toBeDefined(); + expect(promise3.req.getHeader('foo')).toBeDefined(); + expect(promise3.req.getHeader('bar')).toBeDefined(); }); - it('applies defaults correctly to requests', async (flags) => { - - const optionsA = { headers: { Accept: 'foo', 'Test': 123 } }; + it('applies defaults correctly to requests', async () => { + const optionsA = { headers: { Accept: 'foo', Test: 123 } }; const optionsB = { headers: { Accept: 'bar' } }; const wreckA = Wreck.defaults(optionsA); const server = await internals.server('ok'); + onTestFinished(() => server.close()); const promise1 = wreckA.request('get', `http://127.0.0.1:${server.address().port}/`, optionsB); - await expect(promise1).to.not.reject(); - expect(promise1.req.getHeader('accept')).to.equal('bar'); - expect(promise1.req.getHeader('test')).to.equal(123); - flags.onCleanup = () => server.close(); + await expect(promise1).resolves.not.toThrow(); + expect(promise1.req.getHeader('accept')).toBe('bar'); + expect(promise1.req.getHeader('test')).toBe(123); }); it('defaults inherits agents properly', () => { - const wreckNoDefaults = Wreck.defaults({}); const wreckDefaults = Wreck.defaults({ agents: { https: new Https.Agent({ maxSockets: 1 }), http: new Http.Agent({ maxSockets: 1 }), - httpsAllowUnauthorized: new Https.Agent({ maxSockets: 1, rejectUnauthorized: false }) - } + httpsAllowUnauthorized: new Https.Agent({ maxSockets: 1, rejectUnauthorized: false }), + }, }); - expect(Wreck.agents.http.maxSockets).to.equal(wreckNoDefaults.agents.http.maxSockets); - expect(wreckDefaults.agents.http.maxSockets).to.not.equal(wreckNoDefaults.agents.http.maxSockets); - expect(wreckDefaults.agents.http.maxSockets).to.equal(1); - expect(wreckDefaults.agents.https.maxSockets).to.equal(1); - expect(wreckDefaults.agents.httpsAllowUnauthorized.maxSockets).to.equal(1); + expect(Wreck.agents.http.maxSockets).toBe(wreckNoDefaults.agents.http.maxSockets); + expect(wreckDefaults.agents.http.maxSockets).not.toBe(wreckNoDefaults.agents.http.maxSockets); + expect(wreckDefaults.agents.http.maxSockets).toBe(1); + expect(wreckDefaults.agents.https.maxSockets).toBe(1); + expect(wreckDefaults.agents.httpsAllowUnauthorized.maxSockets).toBe(1); }); it('defaults disallows agents without all 3 types', () => { - expect(() => { - Wreck.defaults({ agents: { - 'http': new Http.Agent({ maxSockets: Infinity }) - } + http: new Http.Agent({ maxSockets: Infinity }), + }, }); - }).to.throw(); + }).toThrow(); expect(() => { - Wreck.defaults({ agents: { - 'https': new Https.Agent({ maxSockets: 1 }) - } + https: new Https.Agent({ maxSockets: 1 }), + }, }); - }).to.throw(); + }).toThrow(); expect(() => { - Wreck.defaults({ agents: { - 'httpsAllowUnauthorized': new Https.Agent({ maxSockets: Infinity, rejectUnauthorized: false }) - } + httpsAllowUnauthorized: new Https.Agent({ maxSockets: Infinity, rejectUnauthorized: false }), + }, }); - }).to.throw(); + }).toThrow(); expect(() => { - Wreck.defaults({ agents: { - 'http': new Http.Agent({ maxSockets: Infinity }), - 'https': new Https.Agent({ maxSockets: 1 }) - } + http: new Http.Agent({ maxSockets: Infinity }), + https: new Https.Agent({ maxSockets: 1 }), + }, }); - }).to.throw(); + }).toThrow(); expect(() => { - Wreck.defaults({ agents: { - 'http': new Http.Agent({ maxSockets: Infinity }), - 'httpsAllowUnauthorized': new Https.Agent({ maxSockets: Infinity, rejectUnauthorized: false }) - } + http: new Http.Agent({ maxSockets: Infinity }), + httpsAllowUnauthorized: new Https.Agent({ maxSockets: Infinity, rejectUnauthorized: false }), + }, }); - }).to.throw(); + }).toThrow(); expect(() => { - Wreck.defaults({ agents: { - 'https': new Https.Agent({ maxSockets: 1 }), - 'httpsAllowUnauthorized': new Https.Agent({ maxSockets: Infinity, rejectUnauthorized: false }) - } + https: new Https.Agent({ maxSockets: 1 }), + httpsAllowUnauthorized: new Https.Agent({ maxSockets: Infinity, rejectUnauthorized: false }), + }, }); - }).to.throw(); + }).toThrow(); expect(() => { - Wreck.defaults({ - agents: {} + agents: {}, }); - }).to.throw(); + }).toThrow(); }); - it('default agents can be overrode in request()', async (flags) => { - + it('default agents can be overrode in request()', async () => { const wreck = Wreck.defaults({ agents: { https: new Https.Agent({ maxSockets: 1 }), http: new Http.Agent({ maxSockets: 1 }), - httpsAllowUnauthorized: new Https.Agent({ maxSockets: 1, rejectUnauthorized: false }) - } + httpsAllowUnauthorized: new Https.Agent({ maxSockets: 1, rejectUnauthorized: false }), + }, }); - expect(wreck.agents.http.maxSockets).to.equal(1); + expect(wreck.agents.http.maxSockets).toBe(1); const agent = new Http.Agent({ maxSockets: 2 }); const server = await internals.server('ok'); + onTestFinished(() => server.close()); const promise = wreck.request('get', `http://localhost:${server.address().port}/`, { agent }); - await expect(promise).to.not.reject(); - expect(promise.req.agent.maxSockets).to.equal(2); - flags.onCleanup = () => server.close(); + await expect(promise).resolves.not.toThrow(); + expect(promise.req.agent.maxSockets).toBe(2); }); }); +internals.rejection = async function (promise) { + try { + await promise; + } catch (err) { + return err; + } -internals.unusedPort = function () { + throw new Error('Expected promise to reject'); +}; +internals.unusedPort = function () { return new Promise((resolve, reject) => { - const server = Http.createServer(); server.unref(); server.on('error', reject); server.listen(0, '127.0.0.1', () => { - const { port } = server.address(); server.close(() => resolve(port)); }); }); }; - internals.server = function (handler, socket) { - if (typeof handler !== 'function') { if (handler === 'echo') { handler = (req, res) => { - res.writeHead(200, { 'Content-Type': 'text/plain' }); req.pipe(res); }; - } - else if (handler === 'fail') { + } else if (handler === 'fail') { handler = (req, res) => { - res.socket.destroy(); }; - } - else if (handler === 'ok') { + } else if (handler === 'ok') { handler = (req, res) => { - res.writeHead(200); res.end('ok'); }; - } - else { + } else { handler = (req, res) => { - res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(internals.payload); }; @@ -2720,16 +2550,15 @@ internals.server = function (handler, socket) { } const server = Http.createServer((req, res) => { - const isValidHost = () => { - - return req.headers.host === 'localhost:' + server.address().port || - req.headers.host === '127.0.0.1:' + server.address().port || - req.headers.host === '[::1]:' + server.address().port; + return ( + req.headers.host === 'localhost:' + server.address().port || + req.headers.host === '127.0.0.1:' + server.address().port || + req.headers.host === '[::1]:' + server.address().port + ); }; if (!socket && !isValidHost()) { - res.writeHead(500); return res.end('bad host: ' + req.headers.host); } @@ -2737,17 +2566,13 @@ internals.server = function (handler, socket) { return handler(req, res); }); return new Promise((resolve) => { - server.listen(socket || 0, () => resolve(server)); }); }; - internals.https = function (handler) { - if (!handler) { handler = (req, res) => { - res.writeHead(200, { 'Content-Type': 'text/plain' }); req.pipe(res); }; @@ -2755,20 +2580,17 @@ internals.https = function (handler) { const httpsOptions = { key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0UqyXDCqWDKpoNQQK/fdr0OkG4gW6DUafxdufH9GmkX/zoKz\ng/SFLrPipzSGINKWtyMvo7mPjXqqVgE10LDI3VFV8IR6fnART+AF8CW5HMBPGt/s\nfQW4W4puvBHkBxWSW1EvbecgNEIS9hTGvHXkFzm4xJ2e9DHp2xoVAjREC73B7JbF\nhc5ZGGchKw+CFmAiNysU0DmBgQcac0eg2pWoT+YGmTeQj6sRXO67n2xy/hA1DuN6\nA4WBK3wM3O4BnTG0dNbWUEbe7yAbV5gEyq57GhJIeYxRvveVDaX90LoAqM4cUH06\n6rciON0UbDHV2LP/JaH5jzBjUyCnKLLo5snlbwIDAQABAoIBAQDJm7YC3pJJUcxb\nc8x8PlHbUkJUjxzZ5MW4Zb71yLkfRYzsxrTcyQA+g+QzA4KtPY8XrZpnkgm51M8e\n+B16AcIMiBxMC6HgCF503i16LyyJiKrrDYfGy2rTK6AOJQHO3TXWJ3eT3BAGpxuS\n12K2Cq6EvQLCy79iJm7Ks+5G6EggMZPfCVdEhffRm2Epl4T7LpIAqWiUDcDfS05n\nNNfAGxxvALPn+D+kzcSF6hpmCVrFVTf9ouhvnr+0DpIIVPwSK/REAF3Ux5SQvFuL\njPmh3bGwfRtcC5d21QNrHdoBVSN2UBLmbHUpBUcOBI8FyivAWJhRfKnhTvXMFG8L\nwaXB51IZAoGBAP/E3uz6zCyN7l2j09wmbyNOi1AKvr1WSmuBJveITouwblnRSdvc\nsYm4YYE0Vb94AG4n7JIfZLKtTN0xvnCo8tYjrdwMJyGfEfMGCQQ9MpOBXAkVVZvP\ne2k4zHNNsfvSc38UNSt7K0HkVuH5BkRBQeskcsyMeu0qK4wQwdtiCoBDAoGBANF7\nFMppYxSW4ir7Jvkh0P8bP/Z7AtaSmkX7iMmUYT+gMFB5EKqFTQjNQgSJxS/uHVDE\nSC5co8WGHnRk7YH2Pp+Ty1fHfXNWyoOOzNEWvg6CFeMHW2o+/qZd4Z5Fep6qCLaa\nFvzWWC2S5YslEaaP8DQ74aAX4o+/TECrxi0z2lllAoGAdRB6qCSyRsI/k4Rkd6Lv\nw00z3lLMsoRIU6QtXaZ5rN335Awyrfr5F3vYxPZbOOOH7uM/GDJeOJmxUJxv+cia\nPQDflpPJZU4VPRJKFjKcb38JzO6C3Gm+po5kpXGuQQA19LgfDeO2DNaiHZOJFrx3\nm1R3Zr/1k491lwokcHETNVkCgYBPLjrZl6Q/8BhlLrG4kbOx+dbfj/euq5NsyHsX\n1uI7bo1Una5TBjfsD8nYdUr3pwWltcui2pl83Ak+7bdo3G8nWnIOJ/WfVzsNJzj7\n/6CvUzR6sBk5u739nJbfgFutBZBtlSkDQPHrqA7j3Ysibl3ZIJlULjMRKrnj6Ans\npCDwkQKBgQCM7gu3p7veYwCZaxqDMz5/GGFUB1My7sK0hcT7/oH61yw3O8pOekee\nuctI1R3NOudn1cs5TAy/aypgLDYTUGQTiBRILeMiZnOrvQQB9cEf7TFgDoRNCcDs\nV/ZWiegVB/WY7H0BkCekuq5bHwjgtJTpvHGqQ9YD7RhE8RSYOhdQ/Q==\n-----END RSA PRIVATE KEY-----\n', - cert: '-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n' + cert: '-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n', }; const server = Https.createServer(httpsOptions, handler); return new Promise((resolve) => { - server.listen(0, () => resolve(server)); }); }; - internals.SlowAgent = class SlowAgent extends Http.Agent { createConnection(options, cb) { - setTimeout(cb, 200, new Error('Unable to obtain socket')); } }; diff --git a/test/index.ts b/test/index.ts index cd6a34b..7343d50 100755 --- a/test/index.ts +++ b/test/index.ts @@ -1,42 +1,94 @@ -import * as Http from 'http'; -import * as Net from 'net'; - -import * as Code from '@hapi/code'; -import * as Lab from '@hapi/lab'; -import * as Wreck from '..'; - - -const { expect } = Lab.types; - - -// Provision server - -const server = Http.createServer((req, res) => { - - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Some payload'); +import { describe, expectTypeOf, it } from 'vitest'; + +import Wreck from '../src/index.js'; + +import type * as Http from 'node:http'; +import type * as Stream from 'node:stream'; + +// Every assertion here goes through expectTypeOf, never a live call: vitest collects this +// file as a runtime suite as well as a type suite, so an invoked Wreck method would open a +// real socket. + +describe('typings', () => { + describe('request()', () => { + it('resolves an incoming message carrying the client request', () => { + expectTypeOf(Wreck.request).returns.toEqualTypeOf< + Promise & { req: Http.ClientRequest } + >(); + expectTypeOf(Wreck.request).toBeCallableWith('get', 'http://localhost'); + expectTypeOf(Wreck.request).toBeCallableWith('get', 'http://localhost', { redirects: 1 }); + }); + + it('requires a method and a url', () => { + // @ts-expect-error method and url are required + expectTypeOf(Wreck.request).toBeCallableWith(); + }); + }); + + describe('read()', () => { + it('resolves a Buffer unless parameterized otherwise', () => { + // Wrapped rather than instantiated so the assertion sees the T default resolved, and + // so the sample stream stays a type — an uninvoked arrow body needs no runtime value. + const readDefault = (res: Stream.Readable) => Wreck.read(res); + const readJson = (res: Stream.Readable) => Wreck.read(res, { json: true }); + + expectTypeOf(readDefault).returns.toEqualTypeOf>(); + expectTypeOf(Wreck.read<{ foo: string }>).returns.toEqualTypeOf>(); + expectTypeOf(readJson).returns.toEqualTypeOf>(); + }); + }); + + describe('toReadableStream()', () => { + it('returns a readable stream', () => { + expectTypeOf(Wreck.toReadableStream).returns.toEqualTypeOf(); + expectTypeOf(Wreck.toReadableStream).toBeCallableWith('One two three'); + expectTypeOf(Wreck.toReadableStream).toBeCallableWith([Buffer.from('One'), 'two'], 'ascii'); + }); + }); + + describe('parseCacheControl()', () => { + it('returns the parsed parameters or null', () => { + expectTypeOf(Wreck.parseCacheControl).returns.toExtend<{ 'max-age'?: number } | null>(); + }); + }); + + describe('defaults()', () => { + it('returns another client', () => { + expectTypeOf(Wreck.defaults).returns.toEqualTypeOf(); + expectTypeOf(Wreck.defaults).toBeCallableWith({ baseUrl: 'http://localhost' }); + }); + + it('requires an options object', () => { + // @ts-expect-error options are required + expectTypeOf(Wreck.defaults).toBeCallableWith(); + }); + }); + + describe('shortcuts', () => { + it('resolve a response paired with the payload', () => { + expectTypeOf(Wreck.get).returns.toEqualTypeOf< + Promise<{ res: Http.IncomingMessage; payload: string }> + >(); + expectTypeOf(Wreck.post).returns.toEqualTypeOf< + Promise<{ res: Http.IncomingMessage; payload: string }> + >(); + expectTypeOf(Wreck.patch).returns.toEqualTypeOf< + Promise<{ res: Http.IncomingMessage; payload: string }> + >(); + expectTypeOf(Wreck.put).returns.toEqualTypeOf< + Promise<{ res: Http.IncomingMessage; payload: string }> + >(); + expectTypeOf(Wreck.delete).returns.toEqualTypeOf< + Promise<{ res: Http.IncomingMessage; payload: string }> + >(); + }); + }); + + describe('agents', () => { + it('exposes the three pooled agents', () => { + expectTypeOf(Wreck.agents.http).toExtend(); + expectTypeOf(Wreck.agents.https).toExtend(); + expectTypeOf(Wreck.agents.httpsAllowUnauthorized).toExtend(); + }); + }); }); - -await new Promise((resolve) => server.listen(0, () => resolve(null))); -const address = server.address() as Net.AddressInfo; -const url = `http://localhost:${address.port}`; - - -// request() - -const res = await Wreck.request('get', url); -const body = await Wreck.read(res); - -Code.expect(Buffer.isBuffer(body)).to.equal(true); -Code.expect(body.toString()).to.equal('Some payload'); - -server.close(); - -expect.error(Wreck.request()); - - -// read() - -const stream = Wreck.toReadableStream('One two three'); -const result = Buffer.from('One two three'); -Code.expect(await Wreck.read(stream)).to.equal(result); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..849762f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": true, + "isolatedDeclarations": true, + "isolatedModules": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ESNext" + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..e1b0cc8 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,24 @@ +import Oxc from '@hapi/oxc-plugin/vitest'; +import { defineConfig } from 'vitest/config'; + +import type { ViteUserConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [Oxc()], + test: { + environment: 'node', + testTimeout: 10000, + include: ['test/**/*.{js,ts}'], + typecheck: { + enabled: true, + include: ['test/**/*.{js,ts}'], + }, + coverage: { + provider: 'v8', + include: ['src/**'], + thresholds: { + 100: true, + }, + }, + }, +}) as ViteUserConfig;