├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 18 14 | - 16 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'node:buffer'; 2 | import {Readable as ReadableStream} from 'node:stream'; 3 | 4 | export type Options = { 5 | /** 6 | The HTTP response status code. 7 | */ 8 | readonly statusCode: number; 9 | 10 | /** 11 | The HTTP headers object. 12 | 13 | Keys are in lowercase. 14 | */ 15 | readonly headers: Record; 16 | 17 | /** 18 | The response body. 19 | 20 | The contents will be streamable but is also exposed directly as `response.body`. 21 | */ 22 | readonly body: Buffer; 23 | 24 | /** 25 | The request URL string. 26 | */ 27 | readonly url: string; 28 | }; 29 | 30 | /** 31 | Returns a streamable response object similar to a [Node.js HTTP response stream](https://nodejs.org/api/http.html#http_class_http_incomingmessage). 32 | 33 | @example 34 | ``` 35 | import Response from 'responselike'; 36 | 37 | const response = new Response({ 38 | statusCode: 200, 39 | headers: { 40 | foo: 'bar' 41 | }, 42 | body: Buffer.from('Hi!'), 43 | url: 'https://example.com' 44 | }); 45 | 46 | response.statusCode; 47 | // 200 48 | 49 | response.headers; 50 | // {foo: 'bar'} 51 | 52 | response.body; 53 | // 54 | 55 | response.url; 56 | // 'https://example.com' 57 | 58 | response.pipe(process.stdout); 59 | // 'Hi!' 60 | ``` 61 | */ 62 | export default class Response extends ReadableStream { 63 | /** 64 | The HTTP response status code. 65 | */ 66 | readonly statusCode: number; 67 | 68 | /** 69 | The HTTP headers. 70 | 71 | Keys will be automatically lowercased. 72 | */ 73 | readonly headers: Record; 74 | 75 | /** 76 | The response body. 77 | */ 78 | readonly body: Buffer; 79 | 80 | /** 81 | The request URL string. 82 | */ 83 | readonly url: string; 84 | 85 | constructor(options?: Options); 86 | } 87 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import {Readable as ReadableStream} from 'node:stream'; 2 | import lowercaseKeys from 'lowercase-keys'; 3 | 4 | export default class Response extends ReadableStream { 5 | statusCode; 6 | headers; 7 | body; 8 | url; 9 | 10 | constructor({statusCode, headers, body, url}) { 11 | if (typeof statusCode !== 'number') { 12 | throw new TypeError('Argument `statusCode` should be a number'); 13 | } 14 | 15 | if (typeof headers !== 'object') { 16 | throw new TypeError('Argument `headers` should be an object'); 17 | } 18 | 19 | if (!(body instanceof Uint8Array)) { 20 | throw new TypeError('Argument `body` should be a buffer'); 21 | } 22 | 23 | if (typeof url !== 'string') { 24 | throw new TypeError('Argument `url` should be a string'); 25 | } 26 | 27 | super({ 28 | read() { 29 | this.push(body); 30 | this.push(null); 31 | }, 32 | }); 33 | 34 | this.statusCode = statusCode; 35 | this.headers = lowercaseKeys(headers); 36 | this.body = body; 37 | this.url = url; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'node:buffer'; 2 | import Response from './index.js'; 3 | 4 | // eslint-disable-next-line no-new 5 | new Response({ 6 | statusCode: 200, 7 | headers: {}, 8 | body: Buffer.from(''), 9 | url: 'https://sindresorhus.com', 10 | }); 11 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | Copyright (c) Luke Childs (https://lukechilds.co.uk) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "responselike", 3 | "version": "3.0.0", 4 | "description": "A response-like object for mocking a Node.js HTTP response stream", 5 | "license": "MIT", 6 | "repository": "sindresorhus/responselike", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": "Luke Childs (https://lukechilds.co.uk)", 9 | "type": "module", 10 | "exports": "./index.js", 11 | "engines": { 12 | "node": ">=14.16" 13 | }, 14 | "scripts": { 15 | "test": "xo && ava && tsd" 16 | }, 17 | "files": [ 18 | "index.js", 19 | "index.d.ts" 20 | ], 21 | "keywords": [ 22 | "http", 23 | "https", 24 | "response", 25 | "mock", 26 | "test", 27 | "request", 28 | "responselike" 29 | ], 30 | "dependencies": { 31 | "lowercase-keys": "^3.0.0" 32 | }, 33 | "devDependencies": { 34 | "ava": "^4.3.1", 35 | "get-stream": "^6.0.1", 36 | "tsd": "^0.22.0", 37 | "xo": "^0.50.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # responselike 2 | 3 | > A response-like object for mocking a Node.js HTTP response stream 4 | 5 | Returns a streamable response object similar to a [Node.js HTTP response stream](https://nodejs.org/api/http.html#http_class_http_incomingmessage). Useful for formatting cached responses so they can be consumed by code expecting a real response. 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install responselike 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import Response from 'responselike'; 17 | 18 | const response = new Response({ 19 | statusCode: 200, 20 | headers: { 21 | foo: 'bar' 22 | }, 23 | body: Buffer.from('Hi!'), 24 | url: 'https://example.com' 25 | }); 26 | 27 | response.statusCode; 28 | // 200 29 | 30 | response.headers; 31 | // {foo: 'bar'} 32 | 33 | response.body; 34 | // 35 | 36 | response.url; 37 | // 'https://example.com' 38 | 39 | response.pipe(process.stdout); 40 | // 'Hi!' 41 | ``` 42 | 43 | ## API 44 | 45 | ### new Response(options?) 46 | 47 | Returns a streamable response object similar to a [Node.js HTTP response stream](https://nodejs.org/api/http.html#http_class_http_incomingmessage). 48 | 49 | #### options 50 | 51 | Type: `object` 52 | 53 | ##### statusCode 54 | 55 | Type: `number` 56 | 57 | The HTTP response status code. 58 | 59 | ##### headers 60 | 61 | Type: `object` 62 | 63 | The HTTP headers. Keys will be automatically lowercased. 64 | 65 | ##### body 66 | 67 | Type: `Buffer` 68 | 69 | The response body. The Buffer contents will be streamable but is also exposed directly as `response.body`. 70 | 71 | ##### url 72 | 73 | Type: `string` 74 | 75 | The request URL string. 76 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'node:buffer'; 2 | import test from 'ava'; 3 | import lowercaseKeys from 'lowercase-keys'; 4 | import getStream from 'get-stream'; 5 | import Response from './index.js'; 6 | 7 | const statusCode = 200; 8 | const headers = {Foo: 'Bar'}; 9 | const bodyText = 'Hi.'; 10 | const body = Buffer.from(bodyText); 11 | const url = 'https://example.com'; 12 | const options = {statusCode, headers, body, url}; 13 | 14 | test('Response is a function', t => { 15 | t.is(typeof Response, 'function'); 16 | }); 17 | 18 | test('Response cannot be invoked without \'new\'', t => { 19 | t.throws(() => { 20 | // eslint-disable-next-line new-cap 21 | Response({statusCode, headers, body, url}); 22 | }); 23 | 24 | t.notThrows(() => { 25 | // eslint-disable-next-line no-new 26 | new Response({statusCode, headers, body, url}); 27 | }); 28 | }); 29 | 30 | test('new Response() throws on invalid statusCode', t => { 31 | t.throws(() => { 32 | // eslint-disable-next-line no-new 33 | new Response({headers, body, url}); 34 | }, { 35 | message: 'Argument `statusCode` should be a number', 36 | }); 37 | }); 38 | 39 | test('new Response() throws on invalid headers', t => { 40 | t.throws(() => { 41 | // eslint-disable-next-line no-new 42 | new Response({statusCode, body, url}); 43 | }, { 44 | message: 'Argument `headers` should be an object', 45 | }); 46 | }); 47 | 48 | test('new Response() throws on invalid body', t => { 49 | t.throws(() => { 50 | // eslint-disable-next-line no-new 51 | new Response({statusCode, headers, url}); 52 | }, { 53 | message: 'Argument `body` should be a buffer', 54 | }); 55 | }); 56 | 57 | test('new Response() throws on invalid url', t => { 58 | t.throws(() => { 59 | // eslint-disable-next-line no-new 60 | new Response({statusCode, headers, body}); 61 | }, { 62 | message: 'Argument `url` should be a string', 63 | }); 64 | }); 65 | 66 | test('response has expected properties', t => { 67 | const response = new Response(options); 68 | t.is(response.statusCode, statusCode); 69 | t.deepEqual(response.headers, lowercaseKeys(headers)); 70 | t.is(response.body, body); 71 | t.is(response.url, url); 72 | }); 73 | 74 | test('response headers have lowercase keys', t => { 75 | const response = new Response(options); 76 | t.not(JSON.stringify(headers), response.headers); 77 | t.deepEqual(response.headers, lowercaseKeys(headers)); 78 | }); 79 | 80 | test('response streams body', async t => { 81 | const response = new Response(options); 82 | const responseStream = await getStream(response); 83 | t.is(responseStream, bodyText); 84 | }); 85 | --------------------------------------------------------------------------------