├── .eslintrc ├── .github └── workflows │ ├── node.js.yml │ └── stale.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .mocharc.js ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CODING_RULES.md ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE ├── README.md ├── examples ├── express-route.js └── express-status-vs-json.js ├── lib ├── express │ ├── mock-application.js │ ├── mock-express.js │ ├── mock-request.js │ └── utils │ │ └── define-getter.js ├── headers.js ├── http-mock.d.ts ├── http-mock.js ├── mockEventEmitter.js ├── mockRequest.js ├── mockResponse.js ├── mockWritableStream.js ├── node │ ├── _http_incoming.js │ ├── _http_server.js │ └── http.js └── utils.js ├── package-lock.json ├── package.json ├── test └── lib │ ├── headers.spec.js │ ├── http-mock.test-d.ts │ ├── mockEventEmitter.spec.js │ ├── mockExpressResponse.spec.js │ ├── mockRequest.spec.ts │ ├── mockResponse.spec.js │ ├── mockWritableStream.spec.js │ └── node-http-mock.spec.js └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb-base", 4 | "prettier" 5 | ], 6 | "parser": "@typescript-eslint/parser", 7 | "rules": { 8 | "no-underscore-dangle": "off", 9 | "no-console": "off", 10 | "no-plusplus": "off", 11 | "func-names": [ 12 | "warn", 13 | "as-needed" 14 | ], 15 | "prefer-destructuring": [ 16 | "error", 17 | { 18 | "object": true, 19 | "array": false 20 | } 21 | ], 22 | "no-prototype-builtins": "warn", 23 | "no-restricted-syntax": [ 24 | "error", 25 | { 26 | "selector": "LabeledStatement", 27 | "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand." 28 | }, 29 | { 30 | "selector": "WithStatement", 31 | "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize." 32 | } 33 | ] 34 | }, 35 | "overrides": [ 36 | { 37 | "files": [ 38 | "test/**/*.js", 39 | "test/**/*.ts" 40 | ], 41 | "rules": { 42 | "no-unused-expressions": "off" 43 | }, 44 | "env": { 45 | "mocha": true 46 | } 47 | } 48 | ], 49 | "root": true 50 | } -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x, 18.x, 20.x, 22.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: npm run check 32 | - run: npm run coverage 33 | # - name: Code Coverage Report 34 | # uses: romeovs/lcov-reporter-action@v0.3.1 35 | # if: ${{ matrix.node-version == '20.x' }} 36 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'Stale issue message' 17 | stale-pr-message: 'Stale pull request message' 18 | stale-issue-label: 'no-issue-activity' 19 | stale-pr-label: 'no-pr-activity' 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .settings 3 | coverage 4 | node_modules 5 | test/results 6 | npm-debug.log 7 | TAGS 8 | /.idea/ 9 | .nyc_output 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run check 5 | 6 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | recursive: true, 5 | reporter: 'spec', 6 | extension: ['js', 'ts'], 7 | require: 'ts-node/register', 8 | ui: 'bdd' 9 | }; 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /.vscode/ 3 | /.idea/ 4 | .eslintrc 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "overrides": [ 7 | { 8 | "files": ["package.json", "package-lock.json"], 9 | "options": { 10 | "tabWidth": 2 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /CODING_RULES.md: -------------------------------------------------------------------------------- 1 | # Coding Rules 2 | 3 | For simplicity, we've divided our coding rules using the same categories as the ESLint documentation. 4 | 5 | - [Possible Errors](#errors) 6 | - [Best Practices](#best) 7 | - [Strict Mode](#strict) 8 | - [Variables](#variables) 9 | - [Node.js](#node) 10 | - [Stylistic Issues](#style) 11 | 12 | ## Possible Errors 13 | 14 | The following rules point out areas where you might have made mistakes. 15 | 16 | - [comma-dangle] - enforce trailing commas 17 | - [no-control-regex] - disallow control characters in regular expressions 18 | - [no-debugger] - disallow use of debugger 19 | - [no-dupe-keys] - disallow duplicate keys when creating object literals 20 | - [no-empty] - disallow empty statements 21 | - [no-empty-class] - disallow the use of empty character classes in regular expressions 22 | - [no-ex-assign] - disallow assigning to the exception in a `catch block 23 | - [no-func-assign] - disallow overwriting functions written as function declarations 24 | - [no-unreachable] - disallow unreachable statements after a return, throw, continue, or break statement 25 | - [no-obj-calls] - disallow the use of object properties of the global object (`Math` and `JSON`) as functions 26 | - [no-regex-spaces] - disallow multiple spaces in a regular expression literal 27 | - [use-isnan] - disallow comparisons with the value `NaN` 28 | - [valid-typeof] - Ensure that the results of `typeof` are compared against a valid string 29 | 30 | [Back to Top](#top) 31 | 32 | ## Best Practices 33 | 34 | These are rules designed to prevent you from making mistakes. 35 | 36 | - [no-caller] - disallow use of `arguments.caller` or `arguments.callee` 37 | - [no-div-regex] - disallow division operators explicitly at beginning of regular expression 38 | - [no-else-return] - disallow `else` after a `return` in an `if` 39 | - [no-eq-null] - disallow comparisons to null without a type-checking operator 40 | - [no-eval] - disallow use of `eval()` 41 | - [no-floating-decimal] - disallow the use of leading or trailing decimal points in numeric literals 42 | - [no-implied-eval] - disallow use of `eval()`-like methods 43 | - [no-with] - disallow use of the `with` statement 44 | - [no-fallthrough] - disallow fallthrough of case statements 45 | - [no-unused-expressions] - disallow usage of expressions in statement position 46 | - [no-octal] - disallow use of octal literals 47 | - [no-octal-escape] - disallow use of octal escape sequences in string literals, such as `var foo = "Copyright \251";` 48 | - [no-multi-str] - disallow use of multiline strings 49 | - [no-new-wrappers] - disallows creating new instances of `String`, `Number`, and `Boolean` 50 | - [no-new] - disallow use of new operator when not part of the assignment or comparison 51 | - [no-new-func] - disallow use of new operator for `Function` object 52 | - [no-native-reassign] - disallow reassignments of native objects 53 | - [no-return-assign] - disallow use of assignment in return statement 54 | - [no-self-compare] - disallow comparisons where both sides are exactly the same 55 | - [no-loop-func] - disallow creation of functions within loops 56 | - [no-empty-label] - disallow use of labels for anything other then loops and switches 57 | - [no-script-url] - disallow use of javascript: urls. 58 | - [no-proto] - disallow usage of `__proto__` property 59 | - [no-iterator] - disallow usage of `__iterator__` property 60 | - [no-redeclare] - disallow declaring the same variable more then once 61 | - [curly] - specify curly brace conventions for all control statements 62 | - [dot-notation] - encourages use of dot notation whenever possible 63 | - [eqeqeq] - require the use of `===` and `!==` 64 | - [wrap-iife] - require immediate function invocation to be wrapped in parentheses 65 | 66 | [Back to Top](#top) 67 | 68 | ## Strict Mode 69 | 70 | These rules relate to using strict mode. 71 | 72 | - [strict] - ensures all code is in strict mode and that there are no extraneous Use Strict Directives 73 | 74 | [Back to Top](#top) 75 | 76 | ## Variables 77 | 78 | These rules have to do with variable declarations. 79 | 80 | - [no-catch-shadow] - disallow the catch clause parameter name being the same as a variable in the outer scope 81 | - [no-undef] - disallow use of undeclared variables unless mentioned in a `/*global */` block 82 | - [no-undef-init] - disallow use of undefined when initializing variables 83 | - [no-delete-var] - disallow deletion of variables 84 | - [no-label-var] - disallow labels that share a name with a variable 85 | - [no-unused-vars] - disallow declaration of variables that are not used in the code 86 | - [no-shadow] - disallow declaration of variables already declared in the outer scope 87 | - [no-use-before-define] - disallow use of variables before they are defined 88 | 89 | [Back to Top](#top) 90 | 91 | ## Node.js 92 | 93 | These rules are specific to JavaScript running on Node.js. 94 | 95 | - [no-sync] - disallow use of synchronous methods 96 | - [no-mixed-requires] - allow mixing regular variable and require declarations 97 | 98 | [Back to Top](#top) 99 | 100 | ## Stylistic Issues 101 | 102 | - [no-array-constructor] - disallow use of the `Array` `constructor 103 | - [no-new-object] - disallow use of the `Object constructor` 104 | - [no-wrap-func] - disallow wrapping of non-IIFE statements in parens 105 | - [brace-style] - enforce one true brace style 106 | - [camelcase] - require camel case names 107 | - [consistent-this] - enforces consistent naming when capturing the current execution context 108 | - [new-cap] - require a capital letter for constructors 109 | - [new-parens] - disallow the omission of parentheses when invoking a constructor with no arguments 110 | - [quotes] - specify whether double or single quotes should be used 111 | - [semi] - require use of semicolons instead of ASI 112 | - [no-mixed-spaces-and-tabs] - disallow mixed spaces and tabs for indentation 113 | - [indent] - 4 spaces indentation with enabled switch cases validation 114 | 115 | [Back to Top](#top) 116 | 117 | [comma-dangle]: http://eslint.org/docs/rules/comma-dangle.html 118 | [no-control-regex]: http://eslint.org/docs/rules/no-control-regex.html 119 | [no-debugger]: http://eslint.org/docs/rules/no-debugger.html 120 | [no-dupe-keys]: http://eslint.org/docs/rules/no-dupe-keys.html 121 | [no-empty]: http://eslint.org/docs/rules/no-empty.html 122 | [no-empty-class]: http://eslint.org/docs/rules/no-empty-class.html 123 | [no-ex-assign]: http://eslint.org/docs/rules/no-ex-assign.html 124 | [no-func-assign]: http://eslint.org/docs/rules/no-func-assign.html 125 | [no-unreachable]: http://eslint.org/docs/rules/no-unreachable.html 126 | [no-obj-calls]: http://eslint.org/docs/rules/no-obj-calls.html 127 | [no-regex-spaces]: http://eslint.org/docs/rules/no-regex-spaces.html 128 | [use-isnan]: http://eslint.org/docs/rules/use-isnan.html 129 | [valid-typeof]: http://eslint.org/docs/rules/valid-typeof.html 130 | [no-caller]: http://eslint.org/docs/rules/no-caller.html 131 | [no-div-regex]: http://eslint.org/docs/rules/no-div-regex.html 132 | [no-else-return]: http://eslint.org/docs/rules/no-else-return.html 133 | [no-eq-null]: http://eslint.org/docs/rules/no-eq-null.html 134 | [no-eval]: http://eslint.org/docs/rules/no-eval.html 135 | [no-floating-decimal]: http://eslint.org/docs/rules/no-floating-decimal.html 136 | [no-implied-eval]: http://eslint.org/docs/rules/no-implied-eval.html 137 | [no-with]: http://eslint.org/docs/rules/no-with.html 138 | [no-fallthrough]: http://eslint.org/docs/rules/no-fallthrough.html 139 | [no-unused-expressions]: http://eslint.org/docs/rules/no-unused-expressions.html 140 | [no-octal]: http://eslint.org/docs/rules/no-octal.html 141 | [no-octal-escape]: http://eslint.org/docs/rules/no-octal-escape.html 142 | [no-multi-str]: http://eslint.org/docs/rules/no-multi-str.html 143 | [no-new-wrappers]: http://eslint.org/docs/rules/no-new-wrappers.html 144 | [no-new]: http://eslint.org/docs/rules/no-new.html 145 | [no-new-func]: http://eslint.org/docs/rules/no-new-func.html 146 | [no-native-reassign]: http://eslint.org/docs/rules/no-native-reassign.html 147 | [no-return-assign]: http://eslint.org/docs/rules/no-return-assign.html 148 | [no-self-compare]: http://eslint.org/docs/rules/no-self-compare.html 149 | [no-loop-func]: http://eslint.org/docs/rules/no-loop-func.html 150 | [no-empty-label]: http://eslint.org/docs/rules/no-empty-label.html 151 | [no-script-url]: http://eslint.org/docs/rules/no-script-url.html 152 | [no-proto]: http://eslint.org/docs/rules/no-proto.html 153 | [no-iterator]: http://eslint.org/docs/rules/no-iterator.html 154 | [no-redeclare]: http://eslint.org/docs/rules/no-redeclare.html 155 | [curly]: http://eslint.org/docs/rules/curly.html 156 | [dot-notation]: http://eslint.org/docs/rules/dot-notation.html 157 | [eqeqeq]: http://eslint.org/docs/rules/eqeqeq.html 158 | [wrap-iife]: http://eslint.org/docs/rules/wrap-iife.html 159 | [strict]: http://eslint.org/docs/rules/strict.html 160 | [no-catch-shadow]: http://eslint.org/docs/rules/no-catch-shadow.html 161 | [no-undef]: http://eslint.org/docs/rules/no-undef.html 162 | [no-undef-init]: http://eslint.org/docs/rules/no-undef-init.html 163 | [no-delete-var]: http://eslint.org/docs/rules/no-delete-var.html 164 | [no-label-var]: http://eslint.org/docs/rules/no-label-var.html 165 | [no-unused-vars]: http://eslint.org/docs/rules/no-unused-vars.html 166 | [no-shadow]: http://eslint.org/docs/rules/no-shadow.html 167 | [no-use-before-define]: http://eslint.org/docs/rules/no-use-before-define.html 168 | [no-sync]: http://eslint.org/docs/rules/no-sync.html 169 | [no-mixed-requires]: http://eslint.org/docs/rules/no-mixed-requires.html 170 | [no-array-constructor]: http://eslint.org/docs/rules/no-array-constructor.html 171 | [no-new-object]: http://eslint.org/docs/rules/no-new-object.html 172 | [no-wrap-func]: http://eslint.org/docs/rules/no-wrap-func.html 173 | [brace-style]: http://eslint.org/docs/rules/brace-style.html 174 | [camelcase]: http://eslint.org/docs/rules/camelcase.html 175 | [consistent-this]: http://eslint.org/docs/rules/consistent-this.html 176 | [new-cap]: http://eslint.org/docs/rules/new-cap.html 177 | [new-parens]: http://eslint.org/docs/rules/new-parens.html 178 | [quotes]: http://eslint.org/docs/rules/quotes.html 179 | [semi]: http://eslint.org/docs/rules/semi.html 180 | [no-mixed-spaces-and-tabs]: http://eslint.org/docs/rules/no-mixed-spaces-and-tabs.html 181 | [indent]: http://eslint.org/docs/rules/indent.html 182 | [no-underscore-dangle]: http://eslint.org/docs/rules/no-underscore-dangle.html 183 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | As a community-focused, open source project, contributions are always welcome, no matter how large or small. Here are the guidelines we ask our contributors to follow: 4 | 5 | - [Code of Conduct](#coc) 6 | - [Issues and Bugs](#issue) 7 | - [Feature Requests](#feature) 8 | - [Submission Guidelines](#submit) 9 | - [Coding Rules](#rules) 10 | - [Running Test Suite](#tests) 11 | - [Contact Us](#contact) 12 | 13 | > **NOTE** 14 | 15 | > While we complete our transition to version `2.0`, please adhere to the following: 16 | 17 | - For any contributions to our `2.0` track, please create a topic branch based off our `master` branch, push said topic branch onto your fork and submit your pull request from that branch. 18 | - Anyone wishing to contribute a bug fix to our `1.x` track, please create a topic branch based off our `1.x` branch, push said topic branch onto your fork and submit your pull request from that branch.. 19 | 20 | ## Code of Conduct 21 | 22 | We want to keep our project open and inclusive. We ask that before you 23 | contribute, you read and follow our [Code of Conduct](CODE_OF_CONDUCT.md). 24 | 25 | ## Found an Issue? 26 | 27 | We definitely want to hear from you! 28 | 29 | If you find a bug in the source code or a mistake in the docs, you can help us by 30 | submitting an issue to our [Repository][issues]. Make sure you search through our existing [open and closed issues][issues-archive] in order to avoid duplicate submissions. 31 | 32 | Want to contribute with a fix? Even better! Just submit a [Pull Request][pulls]. 33 | 34 | **Please read the [Submission Guidelines](#submit) below**. 35 | 36 | ## Want a Feature? 37 | 38 | Need a new feature no yet available on node-mocks-http? Submit a new feature to our [GitHub Repository][issues]. 39 | 40 | Think you can help us out by implementing the feature yourself? Go for it! Just craft and submit your [Pull Request][pulls]. 41 | 42 | **Please read the [Submission Guidelines](#submit) below**. 43 | 44 | ## Submission Guidelines 45 | 46 | ### Submitting an Issue 47 | 48 | Before you submit your issue search the [archive][issues-archive], maybe your question was already answered. Let's avoid duplicates. 49 | 50 | If you believe your issue is a bug, and you can't find a issue in the [archive][issues-archive], just open a new issue. 51 | 52 | **Help us help you!** 53 | 54 | Provide the following information to help us identify and fix the issue in a timely manner: 55 | 56 | - **Overview** - describe the issue the best way you can, and if possible include a stack trace 57 | - **Use Case** - explain why you consider this a bug 58 | - **Version(s)** - tell us what version of node-mocks-http you're currently using 59 | - **Reproduce** - it would be awesome if you could provide a live example (using [Plunker][plunker] or 60 | [JSFiddle][jsfiddle]), or at least a step-by-step description on how to reproduce it 61 | - **Suggestions** - if you have identified the lines of code or the commit responsible for the problem please include it as well 62 | 63 | ### Submitting a Pull Request 64 | 65 | We are a _Pull Request-friendly_ project! 66 | 67 | Your pull requests are always welcome. We only ask that you adhere to the following guidelines before you submit your pull request: 68 | 69 | - Search [GitHub][pulls] for an open or closed Pull Request that may be similar to yours. Avoid duplicates! 70 | - Fork our [repo][repo] and create a local clone, if you haven't done so already. 71 | 72 | ```shell 73 | git clone https://github.com/YOUR-NAME/node-mocks-http.git 74 | ``` 75 | 76 | - If you had previously cloned the [repo][repo], make sure you sync it with the upstream repository. 77 | 78 | ```shell 79 | git remote add upstream https://github.com/eugef/node-mocks-http.git 80 | git fetch upstream 81 | git checkout master 82 | git merge upstream/master 83 | ``` 84 | 85 | - Create a new topic branch: 86 | 87 | ```shell 88 | git checkout -b my-awesome-fix master 89 | ``` 90 | 91 | - Now do your thing! Create your fix/patch, **including appropriate test cases**. 92 | - Follow our [Coding Rules](#rules). 93 | - Run our test suite, as described in [below](#tests), 94 | and ensure that all tests pass. 95 | - Commit your changes using a descriptive commit message 96 | 97 | ```shell 98 | git commit -a 99 | ``` 100 | 101 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 102 | 103 | - Push your branch to GitHub: 104 | 105 | ```shell 106 | git push origin my-awesome-fix 107 | ``` 108 | 109 | - In GitHub, send a pull request to `node-mocks-http`. 110 | - If we find any issues we may suggest that you: 111 | 112 | - Make the required updates. 113 | - Re-run the [test suite](#tests) to ensure tests are still passing. 114 | - Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 115 | 116 | ```shell 117 | git rebase master -i 118 | git push origin my-awesome-fix -f 119 | ``` 120 | 121 | That's it! 122 | 123 | #### Post merged cleanup 124 | 125 | After we merge your pull request, you can safely delete your branch and pull the changes from our main (upstream) repository: 126 | 127 | - Delete the remote branch on GitHub either through the GitHub web interface or your local shell as follows: 128 | 129 | ```shell 130 | git push origin --delete my-awesome-fix 131 | ``` 132 | 133 | - Check out the master branch: 134 | 135 | ```shell 136 | git checkout master -f 137 | ``` 138 | 139 | - Delete the local branch: 140 | 141 | ```shell 142 | git branch -D my-awesome-fix 143 | ``` 144 | 145 | - Update your master with the latest upstream version: 146 | 147 | ```shell 148 | git pull --ff upstream master 149 | ``` 150 | 151 | ## Coding Rules 152 | 153 | For a detailed list our the conding conventions used in our project please read our [Coding Rules](CODING_RULES.md). 154 | 155 | ## Running Test Suite 156 | 157 | Navigate to the project folder and run `npm install` to install the 158 | project's dependencies. 159 | 160 | Then simply run the tests. This also checks that the code adheres to the ESLint rules. 161 | 162 | npm test 163 | 164 | Also, please adhere to the ESLint's rules by running the following: 165 | 166 | npm install -g gulp 167 | gulp lint 168 | 169 | Or: 170 | 171 | npx gulp lint 172 | 173 | Failures in the linting process may fail our continuous integration builds. 174 | 175 | ### TypeScript tests 176 | 177 | It's also possible to implement tests in TypeScript, primarily for the purpose of testing the TypeScript definitions. To run these tests, use: 178 | 179 | npm run test:ts 180 | 181 | Note: at this point ESLint is not ran against TypeScript tests. 182 | 183 | Thanks again for helping out! 184 | 185 | [repo]: https://github.com/eugef/node-mocks-http 186 | [issues]: https://github.com/eugef/node-mocks-http/issues 187 | [issues-archive]: https://github.com/eugef/node-mocks-http/issues?q=is%3Aissue 188 | [pulls]: https://github.com/eugef/node-mocks-http/pulls 189 | [pulls-archive]: https://github.com/eugef/node-mocks-http/pulls?q=is%3Apr 190 | [jsfiddle]: http://jsfiddle.net/ 191 | [plunker]: http://plnkr.co/edit 192 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## v 1.17.2 2 | 3 | - Fix request.get() when headers are set as an object [Issue #321][321]. 4 | 5 | [321]: https://github.com/eugef/node-mocks-http/pull/321 6 | 7 | ## v 1.17.1 8 | 9 | - Fix direct access to the headers [Issue #319][319]. 10 | 11 | [319]: https://github.com/eugef/node-mocks-http/pull/319 12 | 13 | ## v 1.17.0 14 | 15 | - Allow access request headers both in Express.js and Web Api (Next.js) manner [Issue #317][317]. 16 | 17 | [317]: https://github.com/eugef/node-mocks-http/pull/317 18 | 19 | ## v 1.16.2 20 | 21 | - Fix: writeHead() sets headersSent [Issue #313][313]. 22 | 23 | [313]: https://github.com/eugef/node-mocks-http/pull/313 24 | 25 | ## v 1.16.1 26 | 27 | - Allowing @types/express v5 [Issue #309][309]. 28 | 29 | [309]: https://github.com/eugef/node-mocks-http/pull/309 30 | 31 | ## v 1.16.0 32 | 33 | - Add appendHeader to MockResponse [Issue #306][306]. 34 | - Add Fetch API types as accepted mock parameters [Issue #291][291]. 35 | 36 | [306]: https://github.com/eugef/node-mocks-http/pull/306 37 | [291]: https://github.com/eugef/node-mocks-http/pull/291 38 | 39 | ## v 1.15.1 40 | 41 | - Allowing @types/node v22 [Issue #305][305]. 42 | 43 | [305]: https://github.com/eugef/node-mocks-http/pull/305 44 | 45 | ## v 1.15.0 46 | 47 | - Add to write() and end() support of TypedArray [Issue #300][300]. 48 | - Fix: return empty string when send() was called with undefined [Issue #298][298]. 49 | 50 | [300]: https://github.com/eugef/node-mocks-http/pull/300 51 | [298]: https://github.com/eugef/node-mocks-http/pull/298 52 | 53 | ## v 1.14.1 54 | 55 | - Move express and node types to prod deps [Issue #290][290]. 56 | 57 | [290]: https://github.com/eugef/node-mocks-http/pull/290 58 | 59 | ## v 1.14.0 60 | 61 | - Fix nodejs typing [Issue #281][281]. 62 | - Add testing capability in TypeScript [Issue #282][282]. 63 | 64 | [281]: https://github.com/eugef/node-mocks-http/pull/281 65 | [282]: https://github.com/eugef/node-mocks-http/pull/282 66 | 67 | ## v 1.13.0 68 | 69 | - Add Request async iterator [Issue #278][278]. 70 | 71 | [278]: https://github.com/eugef/node-mocks-http/issues/278 72 | 73 | ## v 1.12.2 74 | 75 | - 📦️ Upgrade @types/express [Issue #267][267]. 76 | - setHeader should return this [Issue #268][268]. 77 | 78 | [267]: https://github.com/eugef/node-mocks-http/issues/267 79 | [268]: https://github.com/eugef/node-mocks-http/issues/268 80 | 81 | ## v 1.12.1 82 | 83 | - Fix lint issue 84 | 85 | ## v 1.12.0 86 | 87 | - Make it easier to create mocks for the node http library [Issue #259][259]. 88 | - mockResponse.end(): added callback triggering once end() logic has run [Issue #248][248]. 89 | 90 | [259]: https://github.com/eugef/node-mocks-http/issues/259 91 | [248]: https://github.com/eugef/node-mocks-http/issues/248 92 | 93 | ## v 1.11.0 94 | 95 | - Fix request.ip and add request.ips [Issue #244][244]. 96 | - Add response.attachment() from express [Issue #246][246]. 97 | - Add request.getHeader() alias for request.header() [Issue #241][241]. 98 | 99 | [244]: https://github.com/eugef/node-mocks-http/issues/244 100 | [246]: https://github.com/eugef/node-mocks-http/issues/246 101 | [241]: https://github.com/eugef/node-mocks-http/issues/241 102 | 103 | ## v 1.10.1 104 | 105 | - Fix support for req.hostname [Issue #231][231]. 106 | 107 | [231]: https://github.com/eugef/node-mocks-http/issues/231 108 | 109 | ## v 1.10.0 110 | 111 | - Add support for req.hostname [Issue #224][224]. 112 | - Allow to chain writeHead() [Issue #229][229]. 113 | 114 | [224]: https://github.com/eugef/node-mocks-http/issues/224 115 | [229]: https://github.com/eugef/node-mocks-http/issues/229 116 | 117 | ## v 1.9.0 118 | 119 | - Implement response.getHeaderNames() and response.hasHeader() [Issue #222][222]. 120 | - Remove calls to deprecated Buffer constructors [Issue #221][221]. 121 | - Run tests for Node 10, 12 and 14. Drop support for Node 6 and 8 [Issue #218][218]. 122 | - Implement response.getHeaders() [Issue #217][217]. 123 | - Add req.subdomains [Issue #213][213]. 124 | - Add socket option to mockRequest [Issue #209][209]. 125 | - Fix index.d.ts [Issue #205][205]. 126 | - Added support for response.writableEnded and response.writableFinished [Issue #205][203]. 127 | 128 | [222]: https://github.com/eugef/node-mocks-http/issues/222 129 | [221]: https://github.com/eugef/node-mocks-http/issues/221 130 | [218]: https://github.com/eugef/node-mocks-http/issues/218 131 | [217]: https://github.com/eugef/node-mocks-http/issues/217 132 | [213]: https://github.com/eugef/node-mocks-http/issues/213 133 | [209]: https://github.com/eugef/node-mocks-http/issues/209 134 | [205]: https://github.com/eugef/node-mocks-http/issues/205 135 | [203]: https://github.com/eugef/node-mocks-http/issues/203 136 | 137 | ## v 1.8.1 138 | 139 | - Enable res.render() callback argument [Issue #197][197]. 140 | 141 | [197]: https://github.com/eugef/node-mocks-http/issues/197 142 | 143 | ## v 1.8.0 144 | 145 | - Added types for IncomingHeaders [Issue #192][192]. 146 | - Enabled method chaining [Issue #191][191]. 147 | - Added accepts language [Issue #188][188]. 148 | 149 | [192]: https://github.com/eugef/node-mocks-http/issues/192 150 | [191]: https://github.com/eugef/node-mocks-http/issues/191 151 | [188]: https://github.com/eugef/node-mocks-http/issues/188 152 | 153 | ## v 1.7.6 154 | 155 | - Fix for [Issue #182][182]. 156 | 157 | [182]: https://github.com/eugef/node-mocks-http/issues/182 158 | 159 | ## v 1.7.5 160 | 161 | - Updated the dependency tree with newer versions of `eslint`. 162 | 163 | ## v 1.7.4 164 | 165 | - Added `_getJSONData` function with data sent to the user as JSON. [#181][181] 166 | 167 | [181]: https://github.com/eugef/node-mocks-http/pull/181 168 | 169 | ## v 1.7.3 170 | 171 | - Added `.range()` on a mocked request mimicking the [same function](http://expressjs.com/en/4x/api.html#req.range) on Express' request. [#175][175] 172 | 173 | [175]: https://github.com/eugef/node-mocks-http/pull/175 174 | 175 | ## v 1.7.2 176 | 177 | - Revert Turn mock request into a stream [#174][174] 178 | - Fix security issues reported by npm audit 179 | 180 | [174]: https://github.com/eugef/node-mocks-http/pull/174 181 | 182 | ## v 1.7.1 183 | 184 | - Turn mock request into a stream [#169][169] 185 | - Added missing typings for "locals" & create a helper method to get locals [#170][170] 186 | - Make header names case-insensitive in response [#172][172] 187 | - Throw an error object instead of a string [#173][173] 188 | 189 | [169]: https://github.com/eugef/node-mocks-http/pull/169 190 | [170]: https://github.com/eugef/node-mocks-http/pull/170 191 | [172]: https://github.com/eugef/node-mocks-http/pull/172 192 | [173]: https://github.com/eugef/node-mocks-http/pull/173 193 | 194 | ## v 1.7.0 195 | 196 | - Add support for Buffer payload [#154][154] 197 | - Send request body/payload to trigger relevant events [#164][164] 198 | 199 | [154]: https://github.com/eugef/node-mocks-http/pull/154 200 | [164]: https://github.com/eugef/node-mocks-http/pull/164 201 | 202 | ## v 1.6.8 203 | 204 | - Better typings, including the following (see [PR #158][158] for details): 205 | - request object for a controller fn which is typed as extension of `express.Request` 206 | - same for `Response` 207 | - custom properties appended to request object 208 | - fixed missing `_getRenderView` method on `Response` 209 | 210 | **Note:** As of this release, we are officially supporting: 211 | 212 | - 6.13 213 | - 8.9 214 | - 9.6 215 | 216 | [158]: https://github.com/eugef/node-mocks-http/pull/158 217 | 218 | ## v 1.6.7 219 | 220 | - Set an expiration date to a cookie when deleting it [#155][155] 221 | - No `finish` event, `end` event called when it shouldn't be. [#112][112] 222 | - Add support for [append][] on MockResponse [#143][143] 223 | - Add [locals][] object to response [#135][135] 224 | 225 | Special shoutout to [Eugene Fidelin](https://github.com/eugef) for 226 | joining the team and helping out so much. 227 | 228 | [112]: https://github.com/eugef/node-mocks-http/issues/112 229 | [135]: https://github.com/eugef/node-mocks-http/issues/135 230 | [143]: https://github.com/eugef/node-mocks-http/issues/143 231 | [155]: https://github.com/eugef/node-mocks-http/issues/155 232 | [append]: http://expressjs.com/en/api.html#res.append 233 | [locals]: https://expressjs.com/en/api.html#res.locals 234 | 235 | ## v 1.6.6 236 | 237 | - Upgrade Fresh dependency to 0.5.2 due to a [Security advisory][166-SA]. [PR #147](https://github.com/eugef/node-mocks-http/pull/147) 238 | - Add the baseUrl property to the request object. [PR #150](https://github.com/eugef/node-mocks-http/pull/150) 239 | 240 | [166-SA]: https://nodesecurity.io/advisories/526 241 | 242 | ## v 1.6.5 243 | 244 | - Query type definition now more flexible [PR #146](https://github.com/eugef/node-mocks-http/pull/146) 245 | 246 | ## v 1.6.4 247 | 248 | - Incorporated a trimmed down published NPM artifact PR #141 249 | 250 | ## v 1.6.3 251 | 252 | - Moved @types/express to dev-dependencies. [PR #136][136] 253 | 254 | [136]: https://github.com/eugef/node-mocks-http/issues/136 255 | 256 | ## v 1.6.1 257 | 258 | - Fix for Issue #130 for method chaining for `cookie()` and `clearCookie()` 259 | - Fix for Issue #131 for adding `finished` to the response 260 | 261 | ## v 1.6.0 262 | 263 | - Dropping support for Node's "0" version, but will continue to support v4. 264 | - Verifying our builds with v6 (latest stable) as well as current work (v7) 265 | - Removing dependency on lodash and other bug fixes 266 | 267 | ## v 1.5.4 268 | 269 | - Call `write` method from json method of `responseMock` [PR #98][98] 270 | 271 | [98]: https://github.com/eugef/node-mocks-http/issues/98 272 | 273 | ## v 1.5.3 274 | 275 | - Add `.format` to the `mockResponse` object [PR #94][94] 276 | - Add `.location` to the `mockResponse` object [PR #96][96] 277 | - Add API method, `createMocks` to create both mocks with correct references 278 | 279 | [96]: https://github.com/eugef/node-mocks-http/issues/96 280 | [94]: https://github.com/eugef/node-mocks-http/issues/94 281 | 282 | ## v 1.5.2 283 | 284 | - Add case insensitive response headers [#85][85] 285 | - Fix behavior of `mockResponse.writeHead` [#92][92] 286 | - Add support for statusMessage [#84][84] 287 | - Fix issue with `req.param` not returning when false [#82][82] 288 | - Other bug fixes 289 | 290 | [92]: https://github.com/eugef/node-mocks-http/issues/92 291 | [84]: https://github.com/eugef/node-mocks-http/issues/84 292 | [82]: https://github.com/eugef/node-mocks-http/issues/82 293 | [85]: https://github.com/eugef/node-mocks-http/issues/85 294 | 295 | ## v 1.5.1 296 | 297 | - Add support for the `.vary()` response method 298 | 299 | ## v 1.5.0 300 | 301 | Documentation changes, a new feature, and better behaviors, including: 302 | 303 | - Added `jsonp` method that takes a status code and a payload, see [PR #79][79] 304 | - Now able to attach non-standard properties to the mock request object. [PR #74][74] 305 | - param now takes a default value, see [PR #76][76] 306 | - Emit `end` when redirecting, see [PR #77][77] 307 | - Documentation changes, see [PR #64][64], [PR #75][75], [PR #78][78] 308 | 309 | [64]: https://github.com/eugef/node-mocks-http/issues/64 310 | [74]: https://github.com/eugef/node-mocks-http/issues/74 311 | [75]: https://github.com/eugef/node-mocks-http/issues/75 312 | [76]: https://github.com/eugef/node-mocks-http/issues/76 313 | [77]: https://github.com/eugef/node-mocks-http/issues/77 314 | [78]: https://github.com/eugef/node-mocks-http/issues/78 315 | [79]: https://github.com/eugef/node-mocks-http/issues/79 316 | 317 | ## v 1.4.4 318 | 319 | Bug fix release, including the following: 320 | 321 | - Fixed for [#67][67] 322 | - Merge fix for [#68][68] 323 | - Merge fix for [#70][70] 324 | - Merge fix for [#73][73] 325 | 326 | [67]: https://github.com/eugef/node-mocks-http/issues/67 327 | [68]: https://github.com/eugef/node-mocks-http/issues/68 328 | [70]: https://github.com/eugef/node-mocks-http/issues/70 329 | [73]: https://github.com/eugef/node-mocks-http/issues/73 330 | 331 | ## v 1.2.0 332 | 333 | - Adds a `.header` and `.get` method to the request. 334 | 335 | ## v 1.1.0 336 | 337 | - Adds a `.header`, `.set`, and `.get` method to the response. 338 | 339 | ## v 1.0.4 340 | 341 | - Adds the MIT license 342 | 343 | ## v 1.0.3 344 | 345 | - Merged changes by [invernizzie](https://github.com/invernizzie): 346 | to address [#11](https://github.com/eugef/node-mocks-http/pull/11) 347 | 348 | - Merged changes by [ericchaves](https://github.com/ericchaves): 349 | > I extended your library a little but so it could also handle 350 | > some structured responses. By doing so res.send now evaluate the 351 | > data passed and search for either a statusCode or httpCode to be 352 | > used, and also for a body to send as \_data. 353 | > 354 | > It still working as expected (at least tests passed) for regular 355 | > HTTP responses. 356 | > 357 | > Although I did it with node-restify in mind, it should work well 358 | > for all other libs. 359 | 360 | ## v 1.0.2 361 | 362 | - Adds a `.json()` method to the response. (Thanks, diachedelic) 363 | - Cleaned up all source files so ./run-tests passes. 364 | - Cleaned up jshint issues. 365 | 366 | ## v 1.0.1 367 | 368 | - Adds support for response redirect and render 369 | 370 | ## v 0.0.9 371 | 372 | - Adds support for response cookies 373 | 374 | ## v 0.0.8 375 | 376 | - Adds support for request headers 377 | - Fix wrong function name of set cookies 378 | 379 | ## v 0.0.7 380 | 381 | - Adds support for request cookies 382 | 383 | ## v 0.0.6 384 | 385 | - Adds support for request files 386 | 387 | ## v 0.0.5 388 | 389 | - Fixed a bug where `response.send()` can take two parameters, the status code and the data to send. 390 | 391 | ## v 0.0.4 392 | 393 | - Adds a `request.session` that can be set during construction (or via calling the `_setSessionVariable()` method, and read as an object. 394 | 395 | ## v 0.0.3 396 | 397 | - Adds a `request.query` that can be set during construction and read as an object. 398 | 399 | ## v 0.0.2 400 | 401 | - Code refactoring of the `Response` mock. 402 | 403 | ## v 0.0.1 404 | 405 | - Initial code banged out one late night... 406 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is bound by the MIT License (MIT) 2 | 3 | Copyright (c) 2012-2014, Howard Abrams and other collaborators 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Logo](https://user-images.githubusercontent.com/895071/227720269-37e34019-eba0-4768-80ab-1a4dad043043.png)](https://github.com/eugef/node-mocks-http) 2 | 3 | --- 4 | 5 | [![NPM version][npm-badge]][npm-url] 6 | 7 | Mock 'http' objects for testing [Express][express-url], [Next.js][nextjs-url] and [Koa][koa-url] routing functions, 8 | but could be used for testing any [Node.js][node-url] web server applications that have code that requires mockups of the `request` and `response` objects. 9 | 10 | ## Installation 11 | 12 | This project is available as a 13 | [NPM package][npm-url]. 14 | 15 | ```bash 16 | $ npm install node-mocks-http --save-dev 17 | $ npm install @types/node @types/express --save-dev # when using TypeScript 18 | ``` 19 | 20 | or 21 | 22 | ```bash 23 | $ yarn add node-mocks-http --dev 24 | $ yarn add @types/node @types/express --dev # when using TypeScript 25 | ``` 26 | 27 | After installing the package include the following in your test files: 28 | 29 | ```js 30 | const httpMocks = require('node-mocks-http'); 31 | ``` 32 | 33 | ## Usage 34 | 35 | Suppose you have the following Express route: 36 | 37 | ```js 38 | app.get('/user/:id', routeHandler); 39 | ``` 40 | 41 | And you have created a function to handle that route's call: 42 | 43 | ```js 44 | const routeHandler = function( request, response ) { ... }; 45 | ``` 46 | 47 | You can easily test the `routeHandler` function with some code like 48 | this using the testing framework of your choice: 49 | 50 | ```js 51 | exports['routeHandler - Simple testing'] = function (test) { 52 | const request = httpMocks.createRequest({ 53 | method: 'GET', 54 | url: '/user/42', 55 | params: { 56 | id: 42 57 | } 58 | }); 59 | 60 | const response = httpMocks.createResponse(); 61 | 62 | routeHandler(request, response); 63 | 64 | const data = response._getJSONData(); // short-hand for JSON.parse( response._getData() ); 65 | test.equal('Bob Dog', data.name); 66 | test.equal(42, data.age); 67 | test.equal('bob@dog.com', data.email); 68 | 69 | test.equal(200, response.statusCode); 70 | test.ok(response._isEndCalled()); 71 | test.ok(response._isJSON()); 72 | test.ok(response._isUTF8()); 73 | 74 | test.done(); 75 | }; 76 | ``` 77 | 78 | ### TypeScript typings 79 | 80 | The typings for TypeScript are bundled with this project. In particular, the `.createRequest()`, `.createResponse()` and `.createMocks()` methods are typed and are generic. Unless specified explicitly, they will be return an Express-based request/response object: 81 | 82 | ```ts 83 | it('should handle expressjs requests', () => { 84 | const mockExpressRequest = httpMocks.createRequest({ 85 | method: 'GET', 86 | url: '/user/42', 87 | params: { 88 | id: 42 89 | } 90 | }); 91 | const mockExpressResponse = httpMocks.createResponse(); 92 | 93 | routeHandler(request, response); 94 | 95 | const data = response._getJSONData(); 96 | test.equal('Bob Dog', data.name); 97 | test.equal(42, data.age); 98 | test.equal('bob@dog.com', data.email); 99 | 100 | test.equal(200, response.statusCode); 101 | test.ok(response._isEndCalled()); 102 | test.ok(response._isJSON()); 103 | test.ok(response._isUTF8()); 104 | 105 | test.done(); 106 | }); 107 | ``` 108 | 109 | The expected type parameter in the mock request and response expects any type that extends the NodeJS 110 | `http.IncomingRequest` interface or Fetch API `Request` class. This means you can also mock requests 111 | coming from other frameworks too. An example for NextJS request will look like this: 112 | 113 | ```ts 114 | it('should handle nextjs requests', () => { 115 | const mockExpressRequest = httpMocks.createRequest({ 116 | method: 'GET', 117 | url: '/user/42', 118 | params: { 119 | id: 42 120 | } 121 | }); 122 | const mockExpressResponse = httpMocks.createResponse(); 123 | 124 | // ... the rest of the test as above. 125 | }); 126 | ``` 127 | 128 | It is also possible to mock requests from the NextJS new AppRouter: 129 | 130 | ```ts 131 | it('should handle nextjs app reouter requests', () => { 132 | const mockExpressRequest = httpMocks.createRequest({ 133 | method: 'GET', 134 | url: '/user/42', 135 | params: { 136 | id: 42 137 | } 138 | }); 139 | const mockExpressResponse = httpMocks.createResponse(); 140 | 141 | // ... the rest of the test as above. 142 | }); 143 | ``` 144 | 145 | ## API 146 | 147 | ### .createRequest() 148 | 149 | ``` 150 | httpMocks.createRequest(options) 151 | ``` 152 | 153 | Where options is an object hash with any of the following values: 154 | 155 | | option | description | default value | 156 | | --------------- | -------------------------------- | ------------- | 157 | | `method` | request HTTP method | 'GET' | 158 | | `url` | request URL | '' | 159 | | `originalUrl` | request original URL | `url` | 160 | | `baseUrl` | request base URL | `url` | 161 | | `path` | request path | '' | 162 | | `params` | object hash with params | {} | 163 | | `session` | object hash with session values | `undefined` | 164 | | `cookies` | object hash with request cookies | {} | 165 | | `socket` | object hash with request socket | {} | 166 | | `signedCookies` | object hash with signed cookies | `undefined` | 167 | | `headers` | object hash with request headers | {} | 168 | | `body` | object hash with body | {} | 169 | | `query` | object hash with query values | {} | 170 | | `files` | object hash with values | {} | 171 | 172 | The object returned from this function also supports the [Express request](http://expressjs.com/en/4x/api.html#req) functions ([`.accepts()`](http://expressjs.com/en/4x/api.html#req.accepts), [`.is()`](http://expressjs.com/en/4x/api.html#req.is), [`.get()`](http://expressjs.com/en/4x/api.html#req.get), [`.range()`](http://expressjs.com/en/4x/api.html#req.range), etc.). Please send a PR for any missing functions. 173 | 174 | ### .createResponse() 175 | 176 | ```js 177 | httpMocks.createResponse(options); 178 | ``` 179 | 180 | Where options is an object hash with any of the following values: 181 | 182 | | option | description | default value | 183 | | ---------------- | ----------------------------------------------- | -------------------- | 184 | | `locals` | object that contains `response` local variables | `{}` | 185 | | `eventEmitter` | event emitter used by `response` object | `mockEventEmitter` | 186 | | `writableStream` | writable stream used by `response` object | `mockWritableStream` | 187 | | `req` | Request object being responded to | null | 188 | 189 | > NOTE: The out-of-the-box mock event emitter included with `node-mocks-http` is 190 | > not a functional event emitter and as such does not actually emit events. If you 191 | > wish to test your event handlers you will need to bring your own event emitter. 192 | 193 | > Here's an example: 194 | 195 | ```js 196 | const httpMocks = require('node-mocks-http'); 197 | const res = httpMocks.createResponse({ 198 | eventEmitter: require('events').EventEmitter 199 | }); 200 | 201 | // ... 202 | it('should do something', function(done) { 203 | res.on('end', function() { 204 | assert.equal(...); 205 | done(); 206 | }); 207 | }); 208 | // ... 209 | ``` 210 | 211 | > This is an example to send request body and trigger it's 'data' and 'end' events: 212 | 213 | ```js 214 | const httpMocks = require('node-mocks-http'); 215 | const req = httpMocks.createRequest(); 216 | const res = httpMocks.createResponse({ 217 | eventEmitter: require('events').EventEmitter 218 | }); 219 | 220 | // ... 221 | it('should do something', function (done) { 222 | res.on('end', function () { 223 | expect(response._getData()).to.equal('data sent in request'); 224 | done(); 225 | }); 226 | 227 | route(req, res); 228 | 229 | req.send('data sent in request'); 230 | }); 231 | 232 | function route(req, res) { 233 | let data = []; 234 | req.on('data', (chunk) => { 235 | data.push(chunk); 236 | }); 237 | req.on('end', () => { 238 | data = Buffer.concat(data); 239 | res.write(data); 240 | res.end(); 241 | }); 242 | } 243 | // ... 244 | ``` 245 | 246 | ### .createMocks() 247 | 248 | ```js 249 | httpMocks.createMocks(reqOptions, resOptions); 250 | ``` 251 | 252 | Merges `createRequest` and `createResponse`. Passes given options object to each 253 | constructor. Returns an object with properties `req` and `res`. 254 | 255 | ## Design Decisions 256 | 257 | We wanted some simple mocks without a large framework. 258 | 259 | We also wanted the mocks to act like the original framework being 260 | mocked, but allow for setting of values before calling and inspecting 261 | of values after calling. 262 | 263 | ## For Developers 264 | 265 | We are looking for more volunteers to bring value to this project, 266 | including the creation of more objects from the 267 | [HTTP module][node-http-module-url]. 268 | 269 | This project doesn't address all features that must be 270 | mocked, but it is a good start. Feel free to send pull requests, 271 | and a member of the team will be timely in merging them. 272 | 273 | If you wish to contribute please read our [Contributing Guidelines](CONTRIBUTING.md). 274 | 275 | ## Release Notes 276 | 277 | Most releases fix bugs with our mocks or add features similar to the 278 | actual `Request` and `Response` objects offered by Node.js and extended 279 | by Express. 280 | 281 | See the [Release History](HISTORY.md) for details. 282 | 283 | [release-notes]: https://github.com/eugef/node-mocks-http/releases 284 | 285 | ## License 286 | 287 | Licensed under [MIT](LICENSE). 288 | 289 | [npm-badge]: https://badge.fury.io/js/node-mocks-http.png 290 | [npm-url]: https://www.npmjs.com/package/node-mocks-http 291 | [express-url]: https://expressjs.com 292 | [nextjs-url]: https://nextjs.org 293 | [koa-url]: https://koajs.com 294 | [node-url]: http://www.nodejs.org 295 | [node-http-module-url]: http://nodejs.org/docs/latest/api/http.html 296 | -------------------------------------------------------------------------------- /examples/express-route.js: -------------------------------------------------------------------------------- 1 | const httpMocks = require('../lib/http-mock'); 2 | 3 | // Suppose you have the following Express route: 4 | 5 | // app.get('/user/:id', routeHandler); 6 | 7 | // And you have created a function to handle that route's call: 8 | 9 | const routeHandler = function (request, response) { 10 | const { id } = request.params; 11 | 12 | console.log("We have a '%s' request for %s (ID: %d)", request.method, request.url, id); 13 | 14 | const body = { 15 | name: 'Bob Dog', 16 | age: 42, 17 | email: 'bob@dog.com' 18 | }; 19 | 20 | response.setHeader('Content-Type', 'application/json'); 21 | response.statusCode = 200; 22 | response.send(JSON.stringify(body), 'utf8'); 23 | response.end(); 24 | }; 25 | 26 | // ----------------------------------------------------------------- 27 | // In another file, you can easily test the routeHandler function 28 | // with some code like this using the testing framework of your choice: 29 | 30 | exports['routeHandler - Simple testing'] = function testing(test) { 31 | const request = httpMocks.createRequest({ 32 | method: 'GET', 33 | url: '/user/42', 34 | params: { 35 | id: 42 36 | } 37 | }); 38 | 39 | const response = httpMocks.createResponse(); 40 | 41 | routeHandler(request, response); 42 | 43 | const data = response._getJSONData(); 44 | 45 | test.equal('Bob Dog', data.name); 46 | test.equal(42, data.age); 47 | test.equal('bob@dog.com', data.email); 48 | 49 | test.equal(200, response.statusCode); 50 | test.ok(response._isEndCalled()); 51 | test.ok(response._isJSON()); 52 | test.ok(response._isUTF8()); 53 | 54 | test.done(); 55 | }; 56 | -------------------------------------------------------------------------------- /examples/express-status-vs-json.js: -------------------------------------------------------------------------------- 1 | const httpMocks = require('../lib/http-mock'); 2 | 3 | // Suppose you have the following Express route: 4 | 5 | // app.post('/users', routeHandler); 6 | 7 | // And you have created a function to handle that route's call: 8 | 9 | const routeHandler = function (request, response) { 10 | console.log("We have a '%s' request for %s", request.method, request.url); 11 | 12 | const body = { 13 | name: 'Bob Dog', 14 | age: 42, 15 | email: 'bob@dog.com' 16 | }; 17 | 18 | response.status(201).json(body); 19 | response.end(); 20 | }; 21 | 22 | // ----------------------------------------------------------------- 23 | // In another file, you can easily test the routeHandler function 24 | // with some code like this using the testing framework of your choice: 25 | 26 | exports['routeHandler - Simple testing of status() vs json()'] = function testing(test) { 27 | const request = httpMocks.createRequest({ 28 | method: 'POST', 29 | url: '/users' 30 | }); 31 | 32 | const response = httpMocks.createResponse(); 33 | 34 | routeHandler(request, response); 35 | 36 | const data = response._getJSONData(); 37 | 38 | test.equal('Bob Dog', data.name); 39 | test.equal(42, data.age); 40 | test.equal('bob@dog.com', data.email); 41 | 42 | test.equal(201, response.statusCode); 43 | test.ok(response._isJSON()); 44 | 45 | test.done(); 46 | }; 47 | -------------------------------------------------------------------------------- /lib/express/mock-application.js: -------------------------------------------------------------------------------- 1 | const methods = require('methods'); 2 | const deprecate = require('depd')('express'); 3 | 4 | const app = {}; 5 | const trustProxyDefaultSymbol = '@@symbol:trust_proxy_default'; 6 | 7 | app.init = function init() { 8 | this.cache = {}; 9 | this.settings = {}; 10 | this.engines = {}; 11 | this.defaultConfiguration(); 12 | }; 13 | 14 | app.defaultConfiguration = function defaultConfiguration() { 15 | this.enable('x-powered-by'); 16 | this.set('etag', 'weak'); 17 | const env = process.env.NODE_ENV || 'development'; 18 | this.set('env', env); 19 | this.set('query parser', 'extended'); 20 | this.set('subdomain offset', 2); 21 | this.set('trust proxy', false); 22 | Object.defineProperty(this.settings, trustProxyDefaultSymbol, { 23 | configurable: true, 24 | value: true 25 | }); 26 | 27 | this.locals = Object.create(null); 28 | this.mountpath = '/'; 29 | this.locals.settings = this.settings; 30 | this.set('jsonp callback name', 'callback'); 31 | 32 | if (env === 'production') { 33 | this.enable('view cache'); 34 | } 35 | 36 | Object.defineProperty(this, 'router', { 37 | get() { 38 | throw new Error( 39 | "'app.router' is deprecated!\nPlease see the 3.x to 4.x migration guide for details on how to update your app." 40 | ); 41 | } 42 | }); 43 | }; 44 | 45 | app.lazyrouter = () => {}; 46 | app.handle = () => {}; 47 | app.route = () => {}; 48 | app.render = () => {}; 49 | app.listen = () => {}; 50 | 51 | app.use = function use() { 52 | return this; 53 | }; 54 | 55 | app.engine = function engine() { 56 | return this; 57 | }; 58 | 59 | app.param = function param() { 60 | return this; 61 | }; 62 | 63 | app.set = function set(setting, val) { 64 | if (arguments.length === 1) { 65 | return this.settings[setting]; 66 | } 67 | 68 | this.settings[setting] = val; 69 | return this; 70 | }; 71 | 72 | app.path = function path() { 73 | return ''; 74 | }; 75 | 76 | app.enabled = function enabled(setting) { 77 | return !!this.set(setting); 78 | }; 79 | 80 | app.disabled = function disabled(setting) { 81 | return !this.set(setting); 82 | }; 83 | 84 | app.enable = function enable(setting) { 85 | return this.set(setting, true); 86 | }; 87 | 88 | app.disable = function disable(setting) { 89 | return this.set(setting, false); 90 | }; 91 | 92 | methods.forEach((method) => { 93 | app[method] = function httpMethod() { 94 | return this; 95 | }; 96 | }); 97 | 98 | app.all = function all() { 99 | return this; 100 | }; 101 | 102 | app.del = deprecate.function(app.delete, 'app.del: Use app.delete instead'); 103 | 104 | module.exports = app; 105 | -------------------------------------------------------------------------------- /lib/express/mock-express.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events'); 2 | 3 | const mixin = require('merge-descriptors'); 4 | 5 | const application = require('./mock-application'); 6 | const request = require('./mock-request'); 7 | const response = require('../mockResponse'); 8 | 9 | const expressResponse = { 10 | createResponse: response.createResponse 11 | }; 12 | 13 | function createApplication() { 14 | const app = function () {}; 15 | 16 | mixin(app, EventEmitter.prototype, false); 17 | mixin(app, application, false); 18 | 19 | app.request = { 20 | __proto__: request, 21 | app 22 | }; 23 | app.response = { 24 | __proto__: expressResponse.createResponse(), 25 | app 26 | }; 27 | app.init(); 28 | return app; 29 | } 30 | 31 | module.exports = createApplication; 32 | 33 | module.exports.application = application; 34 | module.exports.request = request; 35 | module.exports.response = expressResponse; 36 | -------------------------------------------------------------------------------- /lib/express/mock-request.js: -------------------------------------------------------------------------------- 1 | const accepts = require('accepts'); 2 | const typeis = require('type-is'); 3 | const parseRange = require('range-parser'); 4 | const parse = require('parseurl'); 5 | const { isIP } = require('net'); 6 | const fresh = require('fresh'); 7 | 8 | const http = require('http'); 9 | const defineGetter = require('./utils/define-getter'); 10 | 11 | const req = { 12 | __proto__: http.IncomingMessage.prototype 13 | }; 14 | 15 | req.header = function header(name) { 16 | const headerName = name.toLowerCase(); 17 | switch (headerName) { 18 | case 'referer': 19 | case 'referrer': 20 | return this.headers.referrer || this.headers.referer; 21 | default: 22 | return this.headers[headerName]; 23 | } 24 | }; 25 | 26 | req.get = req.header; 27 | 28 | req.accepts = function acceptTypes(...args) { 29 | const accept = accepts(this); 30 | return accept.types(...args); 31 | }; 32 | 33 | req.acceptsEncodings = function acceptsEncodings(...args) { 34 | const accept = accepts(this); 35 | return accept.encodings(...args); 36 | }; 37 | 38 | req.acceptsEncoding = req.acceptsEncodings; 39 | 40 | req.acceptsCharsets = function acceptsCharsets(...args) { 41 | const accept = accepts(this); 42 | return accept.charsets(...args); 43 | }; 44 | 45 | req.acceptsCharset = req.acceptsCharsets; 46 | 47 | req.acceptsLanguages = function acceptsLanguages(...args) { 48 | const accept = accepts(this); 49 | return accept.languages(...args); 50 | }; 51 | 52 | req.acceptsLanguage = req.acceptsLanguages; 53 | 54 | req.range = function getRange(size) { 55 | const range = this.get('Range'); 56 | if (!range) { 57 | return undefined; 58 | } 59 | return parseRange(size, range); 60 | }; 61 | 62 | req.param = function param(name, defaultValue) { 63 | const params = this.params || {}; 64 | const body = this.body || {}; 65 | const query = this.query || {}; 66 | 67 | if (params[name] !== null && {}.hasOwnProperty.call(params, name)) { 68 | return params[name]; 69 | } 70 | if (body[name] !== null) { 71 | return body[name]; 72 | } 73 | if (query[name] !== null) { 74 | return query[name]; 75 | } 76 | 77 | return defaultValue; 78 | }; 79 | 80 | req.is = function is(...args) { 81 | let types = args; 82 | 83 | if (Array.isArray(args[0])) { 84 | types = args[0]; 85 | } 86 | 87 | return typeis(this, types); 88 | }; 89 | 90 | defineGetter(req, 'protocol', function protocol() { 91 | let { proto } = this.options; 92 | proto = this.get('X-Forwarded-Proto') || proto; 93 | return proto.split(/\s*,\s*/)[0]; 94 | }); 95 | 96 | defineGetter(req, 'secure', function secure() { 97 | return this.protocol === 'https'; 98 | }); 99 | 100 | defineGetter(req, 'ip', function ip() { 101 | return this.options.ip || '127.0.0.1'; 102 | }); 103 | 104 | defineGetter(req, 'ips', function ips() { 105 | return [this.ip]; 106 | }); 107 | 108 | defineGetter(req, 'subdomains', function subdomains() { 109 | const { hostname } = this; 110 | 111 | if (!hostname) { 112 | return []; 113 | } 114 | 115 | const offset = this.app.get('subdomain offset'); 116 | const domains = !isIP(hostname) ? hostname.split('.').reverse() : [hostname]; 117 | 118 | return domains.slice(offset); 119 | }); 120 | 121 | defineGetter(req, 'path', function path() { 122 | return parse(this).pathname; 123 | }); 124 | 125 | defineGetter(req, 'hostname', function hostname() { 126 | let host = this.get('X-Forwarded-Host'); 127 | 128 | if (!host) { 129 | host = this.get('Host'); 130 | } 131 | 132 | if (!host) { 133 | return undefined; 134 | } 135 | 136 | const offset = host[0] === '[' ? host.indexOf(']') + 1 : 0; 137 | const index = host.indexOf(':', offset); 138 | 139 | return index < 0 ? host.substring(0, index) : host; 140 | }); 141 | 142 | defineGetter(req, 'host', function host() { 143 | return this.hostname; 144 | }); 145 | 146 | defineGetter(req, 'fresh', function isFresh() { 147 | const { method } = this; 148 | const { statusCode } = this.res; 149 | 150 | if (method !== 'GET' && method !== 'HEAD') { 151 | return false; 152 | } 153 | 154 | if ((statusCode >= 200 && statusCode < 300) || statusCode === 304) { 155 | return fresh(this.headers, this.res._headers || {}); 156 | } 157 | 158 | return false; 159 | }); 160 | 161 | defineGetter(req, 'stale', function stale() { 162 | return !this.fresh; 163 | }); 164 | 165 | defineGetter(req, 'xhr', function xhr() { 166 | const val = this.get('X-Requested-With') || ''; 167 | return val.toLowerCase() === 'xmlhttprequest'; 168 | }); 169 | 170 | module.exports = req; 171 | -------------------------------------------------------------------------------- /lib/express/utils/define-getter.js: -------------------------------------------------------------------------------- 1 | function defineGetter(obj, name, getter) { 2 | Object.defineProperty(obj, name, { 3 | configurable: true, 4 | enumerable: true, 5 | get: getter 6 | }); 7 | } 8 | 9 | module.exports = defineGetter; 10 | -------------------------------------------------------------------------------- /lib/headers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file implements the Web API Headers interface 3 | * (https://developer.mozilla.org/en-US/docs/Web/API/Headers) 4 | * while maintaining compatibility with Express.js style header access. 5 | */ 6 | const utils = require('./utils'); 7 | 8 | const REFERER_HEADER_NAMES = ['referer', 'referrer']; 9 | 10 | /** 11 | * Get a header value from the headers object. 12 | * Because headers can be set in multiple ways, their names can be uppercased and lowercased. 13 | * 14 | * @param {Object} headers 15 | * @param {string} name 16 | * @returns 17 | */ 18 | function getHeaderValue(headers, name) { 19 | const lowerName = name.toLowerCase(); 20 | 21 | return headers[ 22 | Object.keys(headers).find((key) => { 23 | const lowerKey = key.toLowerCase(); 24 | return ( 25 | lowerKey === lowerName || 26 | (REFERER_HEADER_NAMES.includes(lowerKey) && REFERER_HEADER_NAMES.includes(lowerName)) 27 | ); 28 | }) 29 | ]; 30 | } 31 | 32 | /** 33 | * Creates a Headers object that implements both Express.js style access 34 | * and the Web API Headers interface 35 | * 36 | * @param {Object} headers - Initial headers object 37 | * @returns {HeaderWebAPI} - A proxy that implements the HeaderWebAPI interface 38 | */ 39 | function createHeaders(headers = {}) { 40 | return new Proxy(utils.convertKeysToLowerCase(headers), { 41 | get(target, prop) { 42 | // Handle Headers interface methods 43 | switch (prop) { 44 | case 'get': 45 | return (name) => getHeaderValue(target, name); 46 | case 'getAll': 47 | return (name) => { 48 | const value = getHeaderValue(target, name); 49 | if (!value) { 50 | return []; 51 | } 52 | return Array.isArray(value) ? value : [value]; 53 | }; 54 | case 'has': 55 | return (name) => getHeaderValue(target, name) !== undefined; 56 | case 'set': 57 | return (name, value) => { 58 | // eslint-disable-next-line no-param-reassign 59 | target[name.toLowerCase()] = value; 60 | }; 61 | case 'append': 62 | return (name, value) => { 63 | const lowerName = name.toLowerCase(); 64 | if (lowerName in target) { 65 | const existingValue = target[lowerName]; 66 | if (Array.isArray(existingValue)) { 67 | existingValue.push(value); 68 | } else { 69 | // eslint-disable-next-line no-param-reassign 70 | target[lowerName] = [existingValue, value]; 71 | } 72 | } else { 73 | // eslint-disable-next-line no-param-reassign 74 | target[lowerName] = value; 75 | } 76 | }; 77 | case 'delete': 78 | return (name) => { 79 | // eslint-disable-next-line no-param-reassign 80 | delete target[name.toLowerCase()]; 81 | }; 82 | case 'forEach': 83 | return (callback, thisArg) => { 84 | Object.entries(target).forEach(([key, value]) => { 85 | callback.call(thisArg, value, key, target); 86 | }); 87 | }; 88 | case 'entries': 89 | return () => Object.entries(target)[Symbol.iterator](); 90 | case 'keys': 91 | return () => Object.keys(target)[Symbol.iterator](); 92 | case 'values': 93 | return () => Object.values(target)[Symbol.iterator](); 94 | case Symbol.iterator: 95 | return ( 96 | target[Symbol.iterator] || 97 | function* iterator() { 98 | yield* Object.entries(target); 99 | } 100 | ); 101 | default: 102 | return typeof prop === 'string' ? getHeaderValue(target, prop) : target[prop]; 103 | } 104 | }, 105 | set(target, prop, value) { 106 | if (typeof prop === 'string') { 107 | // eslint-disable-next-line no-param-reassign 108 | target[prop.toLowerCase()] = value; 109 | return true; 110 | } 111 | return false; 112 | }, 113 | has(target, prop) { 114 | if (typeof prop === 'string') { 115 | return prop.toLowerCase() in target; 116 | } 117 | return prop in target; 118 | }, 119 | deleteProperty(target, prop) { 120 | if (typeof prop === 'string') { 121 | // eslint-disable-next-line no-param-reassign 122 | delete target[prop.toLowerCase()]; 123 | return true; 124 | } 125 | // eslint-disable-next-line no-param-reassign 126 | return delete target[prop]; 127 | }, 128 | ownKeys(target) { 129 | return Object.keys(target); 130 | }, 131 | getOwnPropertyDescriptor(target, prop) { 132 | return Object.getOwnPropertyDescriptor(target, prop); 133 | } 134 | }); 135 | } 136 | 137 | module.exports = { createHeaders, getHeaderValue }; 138 | -------------------------------------------------------------------------------- /lib/http-mock.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, CookieOptions } from 'express'; 2 | import { IncomingMessage, OutgoingMessage } from 'http'; 3 | 4 | export type RequestType = IncomingMessage | globalThis.Request; 5 | export type ResponseType = OutgoingMessage | globalThis.Response; 6 | 7 | export type RequestMethod = 'CONNECT' | 'DELETE' | 'GET' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'POST' | 'PUT' | 'TRACE'; 8 | 9 | export interface Params { 10 | [key: string]: any; 11 | } 12 | 13 | export interface Session { 14 | [key: string]: any; 15 | } 16 | 17 | export interface Cookies { 18 | [key: string]: string; 19 | } 20 | 21 | export interface Headers { 22 | // Standard HTTP headers 23 | accept?: string; 24 | 'accept-language'?: string; 25 | 'accept-patch'?: string; 26 | 'accept-ranges'?: string; 27 | 'access-control-allow-credentials'?: string; 28 | 'access-control-allow-headers'?: string; 29 | 'access-control-allow-methods'?: string; 30 | 'access-control-allow-origin'?: string; 31 | 'access-control-expose-headers'?: string; 32 | 'access-control-max-age'?: string; 33 | age?: string; 34 | allow?: string; 35 | 'alt-svc'?: string; 36 | authorization?: string; 37 | 'cache-control'?: string; 38 | connection?: string; 39 | 'content-disposition'?: string; 40 | 'content-encoding'?: string; 41 | 'content-language'?: string; 42 | 'content-length'?: string; 43 | 'content-location'?: string; 44 | 'content-range'?: string; 45 | 'content-type'?: string; 46 | cookie?: string; 47 | date?: string; 48 | expect?: string; 49 | expires?: string; 50 | forwarded?: string; 51 | from?: string; 52 | host?: string; 53 | 'if-match'?: string; 54 | 'if-modified-since'?: string; 55 | 'if-none-match'?: string; 56 | 'if-unmodified-since'?: string; 57 | 'last-modified'?: string; 58 | location?: string; 59 | pragma?: string; 60 | 'proxy-authenticate'?: string; 61 | 'proxy-authorization'?: string; 62 | 'public-key-pins'?: string; 63 | range?: string; 64 | referer?: string; 65 | 'retry-after'?: string; 66 | 'set-cookie'?: string[]; 67 | 'strict-transport-security'?: string; 68 | tk?: string; 69 | trailer?: string; 70 | 'transfer-encoding'?: string; 71 | upgrade?: string; 72 | 'user-agent'?: string; 73 | vary?: string; 74 | via?: string; 75 | warning?: string; 76 | 'www-authenticate'?: string; 77 | 78 | // Support for arbitrary headers 79 | [header: string]: string | string[] | undefined; 80 | } 81 | 82 | /** 83 | * HeaderWebAPI interface combines the existing Headers type with 84 | * standard Web API Headers interface methods for better compatibility 85 | * with browser environments. 86 | */ 87 | export interface HeaderWebAPI { 88 | // Include all the header properties 89 | [header: string]: any; // 'any' to accommodate both header values and methods 90 | 91 | // Web API Headers methods 92 | append(name: string, value: string): void; 93 | delete(name: string): void; 94 | get(name: string): string | null; 95 | has(name: string): boolean; 96 | set(name: string, value: string): void; 97 | forEach(callbackfn: (value: string, key: string, parent: HeaderWebAPI) => void, thisArg?: any): void; 98 | 99 | // Iterator methods 100 | entries(): IterableIterator<[string, string]>; 101 | keys(): IterableIterator; 102 | values(): IterableIterator; 103 | [Symbol.iterator](): IterableIterator<[string, string]>; 104 | } 105 | 106 | export interface Query { 107 | [key: string]: any; 108 | } 109 | 110 | export interface Files { 111 | [key: string]: string; 112 | } 113 | 114 | export interface Body { 115 | [key: string]: any; 116 | } 117 | 118 | export interface RequestOptions { 119 | method?: RequestMethod; 120 | url?: string; 121 | originalUrl?: string; 122 | baseUrl?: string; 123 | path?: string; 124 | params?: Params; 125 | session?: Session; 126 | cookies?: Cookies; 127 | signedCookies?: Cookies; 128 | headers?: Headers; 129 | body?: Body; 130 | query?: Query; 131 | files?: Files; 132 | ip?: string; 133 | 134 | // Support custom properties appended on Request objects. 135 | [key: string]: any; 136 | } 137 | 138 | export type MockRequest = T & { 139 | _setParameter: (key: string, value?: string) => void; 140 | _setSessionVariable: (variable: string, value?: string) => void; 141 | _setCookiesVariable: (variable: string, value?: string) => void; 142 | _setSignedCookiesVariable: (variable: string, value?: string) => void; 143 | _setHeadersCookiesVariable: (variable: string, value: string) => void; 144 | _setFilesCookiesVariable: (variable: string, value?: string) => void; 145 | _setMethod: (method?: string) => void; 146 | _setURL: (value?: string) => void; 147 | _setOriginalUrl: (value?: string) => void; 148 | _setBody: (body?: Body) => void; 149 | _addBody: (key: string, value?: any) => void; 150 | 151 | headers: HeaderWebAPI; 152 | 153 | // Support custom properties appended on Request objects. 154 | [key: string]: any; 155 | }; 156 | 157 | export interface ResponseOptions { 158 | eventEmitter?: any; 159 | writableStream?: any; 160 | req?: any; 161 | locals?: any; 162 | } 163 | 164 | export type ResponseCookie = { 165 | value: any; 166 | options: CookieOptions; 167 | }; 168 | 169 | export type MockResponse = T & { 170 | _isEndCalled: () => boolean; 171 | _getHeaders: () => HeaderWebAPI; 172 | _getData: () => any; 173 | _getJSONData: () => any; 174 | _getBuffer: () => Buffer; 175 | _getLocals: () => any; 176 | _getStatusCode: () => number; 177 | _getStatusMessage: () => string; 178 | _isJSON: () => boolean; 179 | _isUTF8: () => boolean; 180 | _isDataLengthValid: () => boolean; 181 | _getRedirectUrl: () => string; 182 | _getRenderData: () => any; 183 | _getRenderView: () => string; 184 | 185 | cookies: { [name: string]: ResponseCookie }; 186 | }; 187 | 188 | export function createRequest(options?: RequestOptions): MockRequest; 189 | 190 | export function createResponse(options?: ResponseOptions): MockResponse; 191 | 192 | export interface Mocks { 193 | req: MockRequest; 194 | res: MockResponse; 195 | } 196 | 197 | export function createMocks( 198 | reqOptions?: RequestOptions, 199 | resOptions?: ResponseOptions 200 | ): Mocks; 201 | -------------------------------------------------------------------------------- /lib/http-mock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module: http-mock 3 | * 4 | * The interface for this entire module that just exposes the exported 5 | * functions from the other libraries. 6 | */ 7 | 8 | const request = require('./mockRequest'); 9 | const response = require('./mockResponse'); 10 | const express = require('./express/mock-express'); 11 | 12 | /** 13 | * Creates linked req and res objects. Enables using methods that require both 14 | * objects to interact, for example res.format. 15 | * 16 | * @param {Object} reqOpts Options for req creation, see 17 | * @mockRequest.createRequest 18 | * @param {Object} resOpts Options for res creation, see 19 | * @mockResponse.createResponse 20 | * @return {Object} Object with both mocks: { req, res } 21 | */ 22 | const createRequestResponse = function (reqOpts, resOpts) { 23 | const req = request.createRequest(reqOpts); 24 | const res = response.createResponse({ ...resOpts, req }); 25 | 26 | return { req, res }; 27 | }; 28 | 29 | exports.createRequest = request.createRequest; 30 | exports.createResponse = response.createResponse; 31 | exports.createMocks = createRequestResponse; 32 | exports.express = express; 33 | -------------------------------------------------------------------------------- /lib/mockEventEmitter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * http://nodejs.org/api/events.html 3 | */ 4 | 5 | function EventEmitter() {} 6 | 7 | EventEmitter.prototype.addListener = () => {}; 8 | EventEmitter.prototype.on = () => {}; 9 | EventEmitter.prototype.once = () => {}; 10 | EventEmitter.prototype.removeListener = () => {}; 11 | EventEmitter.prototype.removeAllListeners = () => {}; 12 | // EventEmitter.prototype.removeAllListeners = function([event]) 13 | EventEmitter.prototype.setMaxListeners = () => {}; 14 | EventEmitter.prototype.listeners = () => {}; 15 | EventEmitter.prototype.emit = () => {}; 16 | EventEmitter.prototype.prependListener = () => {}; 17 | // EventEmitter.prototype.emit = function(event, [arg1], [arg2], [...]){} 18 | 19 | module.exports = EventEmitter; 20 | -------------------------------------------------------------------------------- /lib/mockRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * File: mockRequest 3 | * 4 | * This file implements node.js's implementation of a 'request' object. 5 | * This is actually closer to what Express offers the user, in that the 6 | * body is really a parsed object of values. 7 | * 8 | * @author Howard Abrams 9 | */ 10 | 11 | /** 12 | * Function: createRequest 13 | * 14 | * Creates a new mock 'request' instance. All values are reset to the 15 | * defaults. 16 | * 17 | * Parameters: 18 | * 19 | * options - An object of named parameters. 20 | * 21 | * Options: 22 | * 23 | * method - The method value, see 24 | * url - The url value, see 25 | * originalUrl - The originalUrl value, see 26 | * baseUrl - The baseUrl value, see 27 | * params - The parameters, see 28 | * body - The body values, , see 29 | */ 30 | 31 | const url = require('url'); 32 | const typeis = require('type-is'); 33 | const accepts = require('accepts'); 34 | const parseRange = require('range-parser'); 35 | let { EventEmitter } = require('events'); 36 | const querystring = require('querystring'); 37 | const { createHeaders, getHeaderValue } = require('./headers'); 38 | 39 | const standardRequestOptions = [ 40 | 'method', 41 | 'url', 42 | 'originalUrl', 43 | 'baseUrl', 44 | 'path', 45 | 'params', 46 | 'session', 47 | 'cookies', 48 | 'headers', 49 | 'body', 50 | 'query', 51 | 'files' 52 | ]; 53 | 54 | function createRequest(options = {}) { 55 | if (options.eventEmitter) { 56 | EventEmitter = options.eventEmitter; 57 | } 58 | 59 | // create mockRequest 60 | const mockRequest = Object.create(EventEmitter.prototype); 61 | EventEmitter.call(mockRequest); 62 | 63 | mockRequest.method = options.method ? options.method : 'GET'; 64 | mockRequest.url = options.url || options.path || ''; 65 | mockRequest.originalUrl = options.originalUrl || mockRequest.url; 66 | mockRequest.baseUrl = options.baseUrl || mockRequest.url; 67 | mockRequest.path = options.path || (options.url ? url.parse(options.url).pathname : ''); 68 | mockRequest.params = options.params ? options.params : {}; 69 | if (options.session) { 70 | mockRequest.session = options.session; 71 | } 72 | mockRequest.cookies = options.cookies ? options.cookies : {}; 73 | if (options.signedCookies) { 74 | mockRequest.signedCookies = options.signedCookies; 75 | } 76 | 77 | // Create headers using the Headers.js module 78 | mockRequest.headers = createHeaders(options.headers); 79 | 80 | mockRequest.body = options.body ? options.body : {}; 81 | mockRequest.query = options.query ? options.query : {}; 82 | mockRequest.files = options.files ? options.files : {}; 83 | mockRequest.socket = options.socket ? options.socket : {}; 84 | 85 | mockRequest.ip = options.ip || '127.0.0.1'; 86 | mockRequest.ips = [mockRequest.ip]; 87 | 88 | mockRequest.destroy = () => {}; 89 | 90 | // parse query string from url to object 91 | if (Object.keys(mockRequest.query).length === 0) { 92 | mockRequest.query = querystring.parse(mockRequest.url.split('?')[1]); 93 | 94 | if (!mockRequest.query.hasOwnProperty) { 95 | Object.defineProperty(mockRequest.query, 'hasOwnProperty', { 96 | enumerable: false, 97 | value: Object.hasOwnProperty.bind(mockRequest.query) 98 | }); 99 | } 100 | } 101 | 102 | // attach any other provided objects into the request for more advanced testing 103 | for (const n in options) { 104 | if (standardRequestOptions.indexOf(n) === -1) { 105 | mockRequest[n] = options[n]; 106 | } 107 | } 108 | 109 | /** 110 | * Return request header. 111 | * 112 | * The `Referrer` header field is special-cased, 113 | * both `Referrer` and `Referer` are interchangeable. 114 | * 115 | * Examples: 116 | * 117 | * mockRequest.get('Content-Type'); 118 | * // => "text/plain" 119 | * 120 | * mockRequest.get('content-type'); 121 | * // => "text/plain" 122 | * 123 | * mockRequest.get('Something'); 124 | * // => undefined 125 | * 126 | * Aliased as `mockRequest.header()`. 127 | * 128 | * @param {String} name 129 | * @return {String} 130 | * @api public 131 | */ 132 | mockRequest.getHeader = function getHeader(name) { 133 | return getHeaderValue(mockRequest.headers, name); 134 | }; 135 | mockRequest.header = mockRequest.getHeader; 136 | mockRequest.get = mockRequest.getHeader; 137 | 138 | /** 139 | * Function: is 140 | * 141 | * Checks for matching content types in the content-type header. 142 | * Requires a request body, identified by transfer-encoding or content-length headers 143 | * 144 | * Examples: 145 | * 146 | * mockRequest.headers['content-type'] = 'text/html'; 147 | * mockRequest.headers['transfer-encoding'] = 'chunked'; 148 | * mockRequest.headers['content-length'] = '100'; 149 | * 150 | * mockRequest.is('html'); 151 | * // => "html" 152 | * 153 | * mockRequest.is('json'); 154 | * // => false 155 | * 156 | * mockRequest.is(['json', 'html', 'text']); 157 | * // => "html" 158 | * 159 | * @param {String|String[]} types content type or array of types to match 160 | * @return {String|false|null} Matching content type as string, false if no match, null if request has no body. 161 | * @api public 162 | */ 163 | mockRequest.is = function isContentType(...args) { 164 | let types = args; 165 | 166 | if (Array.isArray(args[0])) { 167 | types = args[0]; 168 | } 169 | 170 | return typeis(mockRequest, types); 171 | }; 172 | 173 | /** 174 | * Function: accepts 175 | * 176 | * Checks for matching content types in the Accept header. 177 | * 178 | * Examples: 179 | * 180 | * mockRequest.headers['accept'] = 'application/json' 181 | * 182 | * mockRequest.accepts('json'); 183 | * // => 'json' 184 | * 185 | * mockRequest.accepts('html'); 186 | * // => false 187 | * 188 | * mockRequest.accepts(['html', 'json']); 189 | * // => 'json' 190 | * 191 | * @param {String|String[]} types Mime type(s) to check against 192 | * @return {String|false} Matching type or false if no match. 193 | */ 194 | mockRequest.accepts = function acceptsTypes(types) { 195 | const Accepts = accepts(mockRequest); 196 | return Accepts.type(types); 197 | }; 198 | 199 | /** 200 | * Check if the given `encoding`s are accepted. 201 | * 202 | * @param {String} ...encoding 203 | * @return {String|Array} 204 | * @public 205 | */ 206 | mockRequest.acceptsEncodings = function acceptsEncodings(...args) { 207 | let encodings = args; 208 | 209 | if (Array.isArray(args[0])) { 210 | encodings = args[0]; 211 | } 212 | 213 | const accept = accepts(mockRequest); 214 | return accept.encodings(encodings); 215 | }; 216 | 217 | /** 218 | * Check if the given `charset`s are acceptable, 219 | * otherwise you should respond with 406 "Not Acceptable". 220 | * 221 | * @param {String} ...charset 222 | * @return {String|Array} 223 | * @public 224 | */ 225 | mockRequest.acceptsCharsets = function acceptsCharsets(...args) { 226 | let charsets = args; 227 | 228 | if (Array.isArray(args[0])) { 229 | charsets = args[0]; 230 | } 231 | 232 | const accept = accepts(mockRequest); 233 | return accept.charsets(charsets); 234 | }; 235 | 236 | /** 237 | * Check if the given `lang`s are acceptable, 238 | * otherwise you should respond with 406 "Not Acceptable". 239 | * 240 | * @param {String} ...lang 241 | * @return {String|Array} 242 | * @public 243 | */ 244 | mockRequest.acceptsLanguages = function acceptsLanguages(...args) { 245 | let languages = args; 246 | 247 | if (Array.isArray(args[0])) { 248 | languages = args[0]; 249 | } 250 | 251 | const accept = accepts(mockRequest); 252 | return accept.languages(languages); 253 | }; 254 | 255 | /** 256 | * Function: range 257 | * 258 | * Parse Range header field, capping to the given `size`. 259 | * 260 | * Unspecified ranges such as "0-" require knowledge of your resource length. In 261 | * the case of a byte range this is of course the total number of bytes. If the 262 | * Range header field is not given `undefined` is returned, `-1` when unsatisfiable, 263 | * and `-2` when syntactically invalid. 264 | * 265 | * When ranges are returned, the array has a "type" property which is the type of 266 | * range that is required (most commonly, "bytes"). Each array element is an object 267 | * with a "start" and "end" property for the portion of the range. 268 | * 269 | * The "combine" option can be set to `true` and overlapping & adjacent ranges 270 | * will be combined into a single range. 271 | * 272 | * NOTE: remember that ranges are inclusive, so for example "Range: users=0-3" 273 | * should respond with 4 users when available, not 3. 274 | * 275 | * @param {number} size 276 | * @param {object} [opts] 277 | * @param {boolean} [opts.combine=false] 278 | * @return {false|number|array} 279 | * @public 280 | */ 281 | mockRequest.range = function isRange(size, opts) { 282 | const range = mockRequest.get('Range'); 283 | if (!range) { 284 | return undefined; 285 | } 286 | return parseRange(size, range, opts); 287 | }; 288 | 289 | /** 290 | * Function: param 291 | * 292 | * Return the value of param name when present. 293 | * Lookup is performed in the following order: 294 | * - req.params 295 | * - req.body 296 | * - req.query 297 | */ 298 | mockRequest.param = function param(parameterName, defaultValue) { 299 | if ({}.hasOwnProperty.call(mockRequest.params, parameterName)) { 300 | return mockRequest.params[parameterName]; 301 | } 302 | if ({}.hasOwnProperty.call(mockRequest.body, parameterName)) { 303 | return mockRequest.body[parameterName]; 304 | } 305 | if ({}.hasOwnProperty.call(mockRequest.query, parameterName)) { 306 | return mockRequest.query[parameterName]; 307 | } 308 | return defaultValue; 309 | }; 310 | 311 | /** 312 | * Function: _setParameter 313 | * 314 | * Set parameters that the client can then get using the 'params' 315 | * key. 316 | * 317 | * Parameters: 318 | * 319 | * key - The key. For instance, 'bob' would be accessed: request.params.bob 320 | * value - The value to return when accessed. 321 | */ 322 | mockRequest._setParameter = function _setParameter(key, value) { 323 | mockRequest.params[key] = value; 324 | }; 325 | 326 | /** 327 | * Sets a variable that is stored in the session. 328 | * 329 | * @param variable The variable to store in the session 330 | * @param value The value associated with the variable 331 | */ 332 | mockRequest._setSessionVariable = function _setSessionVariable(variable, value) { 333 | if (typeof mockRequest.session !== 'object') { 334 | mockRequest.session = {}; 335 | } 336 | mockRequest.session[variable] = value; 337 | }; 338 | 339 | /** 340 | * Sets a variable that is stored in the cookies. 341 | * 342 | * @param variable The variable to store in the cookies 343 | * @param value The value associated with the variable 344 | */ 345 | mockRequest._setCookiesVariable = function _setCookiesVariable(variable, value) { 346 | mockRequest.cookies[variable] = value; 347 | }; 348 | 349 | /** 350 | * Sets a variable that is stored in the signed cookies. 351 | * 352 | * @param variable The variable to store in the signed cookies 353 | * @param value The value associated with the variable 354 | */ 355 | mockRequest._setSignedCookiesVariable = function _setSignedCookiesVariable(variable, value) { 356 | if (typeof mockRequest.signedCookies !== 'object') { 357 | mockRequest.signedCookies = {}; 358 | } 359 | mockRequest.signedCookies[variable] = value; 360 | }; 361 | 362 | /** 363 | * Sets a variable that is stored in the headers. 364 | * 365 | * @param variable The variable to store in the headers 366 | * @param value The value associated with the variable 367 | */ 368 | mockRequest._setHeadersVariable = function _setHeadersVariable(variable, value) { 369 | mockRequest.headers[variable] = value; 370 | }; 371 | 372 | /** 373 | * Sets a variable that is stored in the files. 374 | * 375 | * @param variable The variable to store in the files 376 | * @param value The value associated with the variable 377 | */ 378 | mockRequest._setFilesVariable = function _setFilesVariable(variable, value) { 379 | mockRequest.files[variable] = value; 380 | }; 381 | 382 | /** 383 | * Function: _setMethod 384 | * 385 | * Sets the HTTP method that the client gets when the called the 'method' 386 | * property. This defaults to 'GET' if it is not set. 387 | * 388 | * Parameters: 389 | * 390 | * method - The HTTP method, e.g. GET, POST, PUT, DELETE, etc. 391 | * 392 | * Note: We don't validate the string. We just return it. 393 | */ 394 | mockRequest._setMethod = function _setMethod(method) { 395 | mockRequest.method = method; 396 | }; 397 | 398 | /** 399 | * Function: _setURL 400 | * 401 | * Sets the URL value that the client gets when the called the 'url' 402 | * property. 403 | * 404 | * Parameters: 405 | * 406 | * value - The request path, e.g. /my-route/452 407 | * 408 | * Note: We don't validate the string. We just return it. Typically, these 409 | * do not include hostname, port or that part of the URL. 410 | */ 411 | mockRequest._setURL = function _setURL(value) { 412 | mockRequest.url = value; 413 | }; 414 | 415 | /** 416 | * Function: _setBaseUrl 417 | * 418 | * Sets the URL value that the client gets when the called the 'baseUrl' 419 | * property. 420 | * 421 | * Parameters: 422 | * 423 | * value - The request base path, e.g. /my-route 424 | * 425 | * Note: We don't validate the string. We just return it. Typically, these 426 | * do not include hostname, port or that part of the URL. 427 | */ 428 | mockRequest._setBaseUrl = function _setBaseUrl(value) { 429 | mockRequest.baseUrl = value; 430 | }; 431 | 432 | /** 433 | * Function: _setOriginalUrl 434 | * 435 | * Sets the URL value that the client gets when the called the 'originalUrl' 436 | * property. 437 | * 438 | * Parameters: 439 | * 440 | * value - The request path, e.g. /my-route/452 441 | * 442 | * Note: We don't validate the string. We just return it. Typically, these 443 | * do not include hostname, port or that part of the URL. 444 | */ 445 | mockRequest._setOriginalUrl = function _setOriginalUrl(value) { 446 | mockRequest.originalUrl = value; 447 | }; 448 | 449 | /** 450 | * Function: _setBody 451 | * 452 | * Sets the body that the client gets when the called the 'body' 453 | * parameter. This defaults to 'GET' if it is not set. 454 | * 455 | * Parameters: 456 | * 457 | * body - An object representing the body. 458 | * 459 | * If you expect the 'body' to come from a form, this typically means that 460 | * it would be a flat object of properties and values, as in: 461 | * 462 | * > { name: 'Howard Abrams', 463 | * > age: 522 464 | * > } 465 | * 466 | * If the client is expecting a JSON object through a REST interface, then 467 | * this object could be anything. 468 | */ 469 | mockRequest._setBody = function _setBody(body) { 470 | mockRequest.body = body; 471 | }; 472 | 473 | /** 474 | * Function: _addBody 475 | * 476 | * Adds another body parameter the client gets when calling the 'body' 477 | * parameter with another property value, e.g. the name of a form element 478 | * that was passed in. 479 | * 480 | * Parameters: 481 | * 482 | * key - The key. For instance, 'bob' would be accessed: request.params.bob 483 | * value - The value to return when accessed. 484 | */ 485 | mockRequest._addBody = function _addBody(key, value) { 486 | mockRequest.body[key] = value; 487 | }; 488 | 489 | /** 490 | * Function: send 491 | * 492 | * Write data to the request stream which will trigger request's 'data', and 'end' event 493 | * 494 | * Parameters: 495 | * 496 | * data - string, array, object, number, buffer 497 | */ 498 | mockRequest.send = function send(data) { 499 | if (Buffer.isBuffer(data)) { 500 | this.emit('data', data); 501 | } else if (typeof data === 'object' || typeof data === 'number') { 502 | this.emit('data', Buffer.from(JSON.stringify(data))); 503 | } else if (typeof data === 'string') { 504 | this.emit('data', Buffer.from(data)); 505 | } 506 | this.emit('end'); 507 | }; 508 | 509 | /** 510 | * Function: hostname 511 | * 512 | * If Hostname is not set explicitly, then derive it from the Host header without port information 513 | * 514 | */ 515 | if (!mockRequest.hostname) { 516 | mockRequest.hostname = (function getHostname() { 517 | if (!mockRequest.headers.host) { 518 | return ''; 519 | } 520 | 521 | const hostname = mockRequest.headers.host.split(':')[0].split('.'); 522 | return hostname.join('.'); 523 | })(); 524 | } 525 | 526 | /** 527 | * Function: subdomains 528 | * 529 | * Subdomains are the dot-separated parts of the host before the main domain of the app. 530 | * 531 | */ 532 | mockRequest.subdomains = (function getSubdomains() { 533 | if (!mockRequest.headers.host) { 534 | return []; 535 | } 536 | 537 | const offset = 2; 538 | const subdomains = mockRequest.headers.host.split('.').reverse(); 539 | 540 | return subdomains.slice(offset); 541 | })(); 542 | 543 | /** 544 | * Function: asyncIterator 545 | * 546 | * Buffers data, error, end, and close events and yields them in order. 547 | * Unlike stream.Readable, this async iterator implementation will not exit 548 | * early on error or close. 549 | */ 550 | mockRequest[Symbol.asyncIterator] = async function* asyncIterator() { 551 | let ended = false; 552 | let closed = false; 553 | let error = null; 554 | const chunks = []; 555 | let resolvePromise = null; 556 | 557 | const promiseExecutor = (resolve) => { 558 | resolvePromise = resolve; 559 | }; 560 | 561 | const promiseResolver = () => { 562 | if (resolvePromise) { 563 | resolvePromise(); 564 | resolvePromise = null; 565 | } 566 | }; 567 | const dataEventHandler = (chunk) => { 568 | if (ended || closed || error) { 569 | return; 570 | } 571 | chunks.push(chunk); 572 | promiseResolver(); 573 | }; 574 | const endEventHandler = () => { 575 | if (ended || closed || error) { 576 | return; 577 | } 578 | ended = true; 579 | promiseResolver(); 580 | }; 581 | const closeEventHandler = () => { 582 | if (closed || error) { 583 | return; 584 | } 585 | closed = true; 586 | promiseResolver(); 587 | }; 588 | const errorEventHandler = (err) => { 589 | if (closed || error) { 590 | return; 591 | } 592 | error = err; 593 | promiseResolver(); 594 | }; 595 | 596 | mockRequest.on('data', dataEventHandler); 597 | mockRequest.on('end', endEventHandler); 598 | mockRequest.on('close', closeEventHandler); 599 | mockRequest.on('error', errorEventHandler); 600 | 601 | // Emit custom event after entering the loop. 602 | setTimeout(() => { 603 | this.emit('async_iterator'); 604 | }); 605 | 606 | try { 607 | for (;;) { 608 | // eslint-disable-next-line no-await-in-loop 609 | await new Promise(promiseExecutor); 610 | let i = 0; 611 | for (;;) { 612 | if (error) { 613 | throw error; 614 | } 615 | if (closed) { 616 | return; 617 | } 618 | 619 | const hasChunks = i < chunks.length; 620 | if (!hasChunks) { 621 | if (ended) { 622 | // End signaled. Bail. 623 | return; 624 | } 625 | // Wait for next push. 626 | break; 627 | } 628 | 629 | const chunk = chunks[i]; 630 | chunks[i] = undefined; 631 | i += 1; 632 | yield chunk; 633 | } 634 | chunks.length = 0; 635 | } 636 | } finally { 637 | chunks.length = 0; 638 | error = null; 639 | 640 | mockRequest.off('data', dataEventHandler); 641 | mockRequest.off('end', endEventHandler); 642 | mockRequest.off('close', closeEventHandler); 643 | mockRequest.off('error', errorEventHandler); 644 | } 645 | }; 646 | 647 | return mockRequest; 648 | } 649 | 650 | module.exports.createRequest = createRequest; 651 | -------------------------------------------------------------------------------- /lib/mockResponse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * File: mockResponse 3 | * 4 | * This file implements node.js's implementation of a 'response' object. 5 | * Like all good mocks, the response file that can be called and used in 6 | * place of a real HTTP response object. 7 | * 8 | * @author Howard Abrams 9 | */ 10 | 11 | /** 12 | * Function: createResponse 13 | * 14 | * Creates a new mock 'response' instance. All values are reset to the 15 | * defaults. 16 | * 17 | * Parameters: 18 | * 19 | * options - An object of named parameters. 20 | * 21 | * Options: 22 | * 23 | * encoding - The default encoding for the response 24 | */ 25 | 26 | const mime = require('mime'); 27 | const path = require('path'); 28 | const contentDisposition = require('content-disposition'); 29 | let WritableStream = require('./mockWritableStream'); 30 | let EventEmitter = require('./mockEventEmitter'); 31 | const http = require('./node/http'); 32 | const utils = require('./utils'); 33 | 34 | const TypedArray = Object.getPrototypeOf(Uint8Array); 35 | 36 | function createResponse(options = {}) { 37 | let _endCalled = false; 38 | let _data = ''; 39 | let _buffer = Buffer.alloc(0); 40 | const _chunks = []; 41 | let _size = 0; 42 | let _encoding = options.encoding; 43 | 44 | let _redirectUrl = ''; 45 | let _renderView = ''; 46 | let _renderData = {}; 47 | 48 | if (options.writableStream) { 49 | WritableStream = options.writableStream; 50 | } 51 | if (options.eventEmitter) { 52 | EventEmitter = options.eventEmitter; 53 | } 54 | const writableStream = new WritableStream(); 55 | 56 | const mockRequest = options.req; 57 | // create mockResponse 58 | 59 | const mockResponse = Object.create(EventEmitter.prototype); 60 | EventEmitter.call(mockResponse); 61 | 62 | mockResponse._headers = {}; 63 | 64 | mockResponse.cookies = {}; 65 | mockResponse.finished = false; 66 | mockResponse.writableEnded = false; 67 | mockResponse.writableFinished = false; 68 | mockResponse.headersSent = false; 69 | mockResponse.statusCode = 200; 70 | mockResponse.statusMessage = 'OK'; 71 | 72 | // http://expressjs.com/en/api.html#res.locals 73 | mockResponse.locals = options.locals || {}; 74 | 75 | mockResponse.cookie = function cookie(name, value, opt) { 76 | mockResponse.cookies[name] = { 77 | value, 78 | options: opt 79 | }; 80 | 81 | return this; 82 | }; 83 | 84 | mockResponse.clearCookie = function clearCookie(name, opt) { 85 | const opts = opt || {}; 86 | opts.expires = new Date(1); 87 | opts.path = '/'; 88 | 89 | return this.cookie(name, '', opts); 90 | }; 91 | 92 | mockResponse.status = function status(code) { 93 | mockResponse.statusCode = code; 94 | return this; 95 | }; 96 | 97 | /** 98 | * Function: writeHead 99 | * 100 | * The 'writeHead' function from node's HTTP API. 101 | * 102 | * Parameters: 103 | * 104 | * statusCode - A number to send as a the HTTP status 105 | * headers - An object of properties that will be used for 106 | * the HTTP headers. 107 | */ 108 | mockResponse.writeHead = function writeHead(statusCode, statusMessage, headers) { 109 | if (_endCalled) { 110 | throw new Error('The end() method has already been called.'); 111 | } 112 | 113 | if (mockResponse.headersSent) { 114 | // Node docs: "This method must only be called once on a message" 115 | // but it doesn't error if you do call it after first chunk of body is sent 116 | // so we shouldn't throw here either (although it's a bug in the code). 117 | // We return without updating since in real life it's just possible the double call didn't 118 | // completely corrupt the response (for example not using chunked encoding due to HTTP/1.0 client) 119 | // and in this case the client will see the _original_ headers. 120 | return this; 121 | } 122 | 123 | mockResponse.statusCode = statusCode; 124 | 125 | // resolve statusMessage and headers as optional 126 | if (Object.prototype.toString.call(statusMessage) === '[object Object]') { 127 | // eslint-disable-next-line no-param-reassign 128 | headers = statusMessage; 129 | // eslint-disable-next-line no-param-reassign 130 | statusMessage = null; 131 | } 132 | 133 | if (statusMessage) { 134 | mockResponse.statusMessage = statusMessage; 135 | } 136 | 137 | // The headers specified earlier (been set with `mockResponse.setHeader`) 138 | // should not be overwritten but be merged with the headers 139 | // passed into `mockResponse.writeHead`. 140 | if (headers) { 141 | Object.assign(mockResponse._headers, utils.convertKeysToLowerCase(headers)); 142 | } 143 | 144 | this.headersSent = true; 145 | return this; 146 | }; 147 | 148 | /** 149 | * The 'send' function from restify's Response API that returns data 150 | * to the client. Can be called multiple times. 151 | * 152 | * @see http://mcavage.me/node-restify/#response-api 153 | * 154 | * @param data The data to return. Must be a string. 155 | */ 156 | mockResponse.send = function send(a, b, c) { 157 | const _formatData = (data) => { 158 | if (typeof data === 'object') { 159 | if (data.statusCode) { 160 | mockResponse.statusCode = data.statusCode; 161 | } else if (data.httpCode) { 162 | mockResponse.statusCode = data.httpCode; 163 | } 164 | 165 | if (data.body) { 166 | _data = data.body; 167 | } else { 168 | _data = data; 169 | } 170 | } else { 171 | _data += data ?? ''; 172 | } 173 | }; 174 | 175 | switch (arguments.length) { 176 | case 1: 177 | if (typeof a === 'number') { 178 | mockResponse.statusCode = a; 179 | } else { 180 | _formatData(a); 181 | } 182 | break; 183 | 184 | case 2: 185 | if (typeof a === 'number') { 186 | _formatData(b); 187 | mockResponse.statusCode = a; 188 | } else if (typeof b === 'number') { 189 | _formatData(a); 190 | mockResponse.statusCode = b; 191 | console.warn('WARNING: Called send() with deprecated parameter order'); 192 | } else { 193 | _formatData(a); 194 | _encoding = b; 195 | } 196 | break; 197 | 198 | case 3: 199 | _formatData(a); 200 | mockResponse._headers = utils.convertKeysToLowerCase(b); 201 | mockResponse.statusCode = c; 202 | console.warn('WARNING: Called send() with deprecated three parameters'); 203 | break; 204 | 205 | default: 206 | break; 207 | } 208 | 209 | mockResponse.headersSent = true; 210 | 211 | mockResponse.emit('send'); 212 | mockResponse.end(); 213 | 214 | return mockResponse; 215 | }; 216 | 217 | /** 218 | * Send given HTTP status code. 219 | * 220 | * Sets the response status to `statusCode` and the body of the 221 | * response to the standard description from node's http.STATUS_CODES 222 | * or the statusCode number if no description. 223 | * 224 | * Examples: 225 | * 226 | * mockResponse.sendStatus(200); 227 | * 228 | * @param {number} statusCode 229 | * @api public 230 | */ 231 | 232 | mockResponse.sendStatus = function sendStatus(statusCode) { 233 | const body = http.STATUS_CODES[statusCode] || String(statusCode); 234 | 235 | mockResponse.statusCode = statusCode; 236 | mockResponse.type('txt'); 237 | 238 | return mockResponse.send(body); 239 | }; 240 | 241 | /** 242 | * Function: json 243 | * 244 | * The 'json' function from node's HTTP API that returns JSON 245 | * data to the client. 246 | * 247 | * Parameters: 248 | * 249 | * a - Either a statusCode or string containing JSON payload 250 | * b - Either a statusCode or string containing JSON payload 251 | * 252 | * If not specified, the statusCode defaults to 200. 253 | * Second parameter is optional. 254 | */ 255 | mockResponse.json = function json(a, b) { 256 | mockResponse.setHeader('Content-Type', 'application/json'); 257 | if (typeof a !== 'undefined') { 258 | if (typeof a === 'number' && typeof b !== 'undefined') { 259 | mockResponse.statusCode = a; 260 | mockResponse.write(JSON.stringify(b), 'utf8'); 261 | } else if (typeof b !== 'undefined' && typeof b === 'number') { 262 | mockResponse.statusCode = b; 263 | mockResponse.write(JSON.stringify(a), 'utf8'); 264 | } else { 265 | mockResponse.write(JSON.stringify(a), 'utf8'); 266 | } 267 | } 268 | mockResponse.emit('send'); 269 | mockResponse.end(); 270 | 271 | return mockResponse; 272 | }; 273 | 274 | /** 275 | * Function: jsonp 276 | * 277 | * The 'jsonp' function from node's HTTP API that returns JSON 278 | * data to the client. 279 | * 280 | * Parameters: 281 | * 282 | * a - Either a statusCode or string containing JSON payload 283 | * b - Either a statusCode or string containing JSON payload 284 | * 285 | * If not specified, the statusCode defaults to 200. 286 | * Second parameter is optional. 287 | */ 288 | mockResponse.jsonp = function jsonp(a, b) { 289 | mockResponse.setHeader('Content-Type', 'text/javascript'); 290 | if (typeof a !== 'undefined') { 291 | if (typeof a === 'number' && typeof b !== 'undefined') { 292 | mockResponse.statusCode = a; 293 | _data += JSON.stringify(b); 294 | } else if (typeof b !== 'undefined' && typeof b === 'number') { 295 | mockResponse.statusCode = b; 296 | _data += JSON.stringify(a); 297 | } else { 298 | _data += JSON.stringify(a); 299 | } 300 | } 301 | mockResponse.emit('send'); 302 | mockResponse.end(); 303 | 304 | return mockResponse; 305 | }; 306 | 307 | /** 308 | * Set "Content-Type" response header with `type` through `mime.lookup()` 309 | * when it does not contain "/", or set the Content-Type to `type` otherwise. 310 | * 311 | * Examples: 312 | * 313 | * res.type('.html'); 314 | * res.type('html'); 315 | * res.type('json'); 316 | * res.type('application/json'); 317 | * res.type('png'); 318 | * 319 | * @param {String} type 320 | * @return {ServerResponse} for chaining 321 | * @api public 322 | */ 323 | mockResponse.contentType = function contentType(type) { 324 | return mockResponse.set('Content-Type', type.indexOf('/') >= 0 ? type : mime.lookup(type)); 325 | }; 326 | mockResponse.type = mockResponse.contentType; 327 | 328 | /** 329 | * Set 'Location' response header. 330 | * 331 | * @see http://expressjs.com/en/api.html#res.location 332 | * 333 | * @param {String} location The location to set in the header. 334 | * @return {ServerResponse} For chaining 335 | */ 336 | mockResponse.location = function setLocation(location) { 337 | return mockResponse.set('Location', location); 338 | }; 339 | 340 | /** 341 | * Function: write 342 | * 343 | * This function has the same behavior as the 'send' function. 344 | * 345 | * Parameters: 346 | * 347 | * data - The data to return. Must be a string. Appended to 348 | * previous calls to data. 349 | * encoding - Optional encoding value. 350 | */ 351 | 352 | mockResponse.write = function write(data, encoding) { 353 | mockResponse.headersSent = true; 354 | 355 | if (data instanceof Buffer) { 356 | _chunks.push(data); 357 | _size += data.length; 358 | } else if (data instanceof TypedArray) { 359 | _data += new TextDecoder(encoding).decode(data); 360 | } else { 361 | _data += data; 362 | } 363 | 364 | if (encoding) { 365 | _encoding = encoding; 366 | } 367 | }; 368 | 369 | /** 370 | * Function: getEndArguments 371 | * 372 | * Utility function that parses and names parameters for the various 373 | * mockResponse.end() signatures. Reference: 374 | * https://nodejs.org/api/http.html#http_response_end_data_encoding_callback 375 | * 376 | */ 377 | function getEndArguments(args) { 378 | let data; 379 | let encoding; 380 | let callback; 381 | if (args[0]) { 382 | if (typeof args[0] === 'function') { 383 | callback = args[0]; 384 | } else { 385 | data = args[0]; 386 | } 387 | } 388 | if (args[1]) { 389 | const type = typeof args[1]; 390 | if (type === 'function') { 391 | callback = args[1]; 392 | } else if (type === 'string' || args[1] instanceof String) { 393 | encoding = args[1]; 394 | } 395 | } 396 | if (args[2] && typeof args[2] === 'function') { 397 | callback = args[2]; 398 | } 399 | return { data, encoding, callback }; 400 | } 401 | 402 | /** 403 | * Function: end 404 | * 405 | * The 'end' function from node's HTTP API that finishes 406 | * the connection request. This must be called. 407 | * 408 | * Signature: response.end([data[, encoding]][, callback]) 409 | * 410 | * Parameters: 411 | * 412 | * data - Optional data to return. Must be a string or Buffer instance. 413 | * Appended to previous calls to . 414 | * encoding - Optional encoding value. 415 | * callback - Optional callback function, called once the logic has run 416 | * 417 | */ 418 | mockResponse.end = function end(...endArgs) { 419 | if (_endCalled) { 420 | // Do not emit this event twice. 421 | return; 422 | } 423 | 424 | mockResponse.finished = true; 425 | mockResponse.writableEnded = true; 426 | mockResponse.headersSent = true; 427 | 428 | _endCalled = true; 429 | 430 | const args = getEndArguments(endArgs); 431 | 432 | if (args.data) { 433 | if (args.data instanceof Buffer) { 434 | _chunks.push(args.data); 435 | _size += args.data.length; 436 | } else if (args.data instanceof TypedArray) { 437 | _data += new TextDecoder(args.encoding).decode(args.data); 438 | } else { 439 | _data += args.data; 440 | } 441 | } 442 | 443 | if (_chunks.length) { 444 | switch (_chunks.length) { 445 | case 1: 446 | _buffer = _chunks[0]; 447 | break; 448 | default: 449 | _buffer = Buffer.alloc(_size); 450 | for (let i = 0, pos = 0, l = _chunks.length; i < l; i++) { 451 | const chunk = _chunks[i]; 452 | chunk.copy(_buffer, pos); 453 | pos += chunk.length; 454 | } 455 | break; 456 | } 457 | } 458 | 459 | if (args.encoding) { 460 | _encoding = args.encoding; 461 | } 462 | 463 | mockResponse.emit('end'); 464 | mockResponse.writableFinished = true; // Reference: https://nodejs.org/docs/latest-v12.x/api/http.html#http_request_writablefinished 465 | mockResponse.emit('finish'); 466 | 467 | if (args.callback) { 468 | args.callback(); 469 | } 470 | }; 471 | 472 | /** 473 | * Function: vary 474 | * 475 | * Adds the field/s to the Vary response header 476 | * 477 | * Examples: 478 | * 479 | * res.vary('A-B-Test'); 480 | * res.vary(['A-B-Test', 'Known-User']); 481 | */ 482 | mockResponse.vary = function vary(fields) { 483 | const header = mockResponse.getHeader('Vary') || ''; 484 | let values = header.length ? header.split(', ') : []; 485 | 486 | const uniqueFields = (Array.isArray(fields) ? fields : [fields]).filter((field) => { 487 | const regex = new RegExp(field, 'i'); 488 | 489 | const matches = values.filter((value) => value.match(regex)); 490 | 491 | return !matches.length; 492 | }); 493 | 494 | values = values.concat(uniqueFields); 495 | 496 | return mockResponse.setHeader('Vary', values.join(', ')); 497 | }; 498 | 499 | /** 500 | * Set _Content-Disposition_ header to _attachment_ with optional `filename`. 501 | * 502 | * Example: 503 | * 504 | * res.attachment('download.csv') 505 | * 506 | * @param {String} filename 507 | * @return {ServerResponse} 508 | * @api public 509 | */ 510 | 511 | mockResponse.attachment = function attachment(filename) { 512 | if (filename) { 513 | mockResponse.type(path.extname(filename)); 514 | } 515 | 516 | mockResponse.set('Content-Disposition', contentDisposition(filename)); 517 | 518 | return this; 519 | }; 520 | 521 | /** 522 | * Append additional header `field` with value `val`. 523 | * 524 | * Example: 525 | * 526 | * res.append('Link', ['', '']); 527 | * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); 528 | * res.append('Warning', '199 Miscellaneous warning'); 529 | * 530 | * @param {String} field 531 | * @param {String|Array} val 532 | * @return {ServerResponse} for chaining 533 | * @api public 534 | */ 535 | mockResponse.append = function append(field, val) { 536 | const prev = mockResponse.get(field); 537 | let value = val; 538 | 539 | if (prev) { 540 | // concat the new and prev vals 541 | value = Array.isArray(prev) ? prev.concat(val) : [prev].concat(val); 542 | } 543 | 544 | return mockResponse.set(field, value); 545 | }; 546 | 547 | /** 548 | * Set header `field` to `val`, or pass 549 | * an object of header fields. 550 | * 551 | * Examples: 552 | * 553 | * res.set('Foo', ['bar', 'baz']); 554 | * res.set('Accept', 'application/json'); 555 | * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); 556 | * 557 | * Aliased as `mockResponse.header()`. 558 | * 559 | * @param {String|Object|Array} field 560 | * @param {String} val 561 | * @return {ServerResponse} for chaining 562 | * @api public 563 | */ 564 | mockResponse.header = function header(field, val) { 565 | if (arguments.length === 2) { 566 | let value; 567 | if (Array.isArray(val)) { 568 | value = val.map(String); 569 | } else { 570 | value = String(val); 571 | } 572 | mockResponse.setHeader(field, value); 573 | } else if (typeof field === 'string') { 574 | return mockResponse.getHeader(field); 575 | } else { 576 | for (const key in field) { 577 | if ({}.hasOwnProperty.call(field, key)) { 578 | mockResponse.setHeader(key, field[key]); 579 | } 580 | } 581 | } 582 | return mockResponse; 583 | }; 584 | mockResponse.set = mockResponse.header; 585 | 586 | /** 587 | * Function: getHeaders 588 | * 589 | * Returns a shallow copy of the current outgoing headers. 590 | */ 591 | mockResponse.getHeaders = function getHeaders() { 592 | return JSON.parse(JSON.stringify(mockResponse._headers)); 593 | }; 594 | 595 | /** 596 | * Function: getHeader 597 | * Function: get 598 | * 599 | * Returns a particular header by name. 600 | */ 601 | mockResponse.getHeader = function getHeader(name) { 602 | return mockResponse._headers[name.toLowerCase()]; 603 | }; 604 | mockResponse.get = mockResponse.getHeader; 605 | 606 | /** 607 | * Function: getHeaderNames 608 | * 609 | * Returns an array containing the unique names of the current outgoing headers. 610 | */ 611 | mockResponse.getHeaderNames = function getHeaderNames() { 612 | return Object.keys(mockResponse._headers); // names are already stored in lowercase 613 | }; 614 | 615 | /** 616 | * Function: hasHeader 617 | * 618 | * Returns `true` if the header identified by `name` is currently set. 619 | */ 620 | mockResponse.hasHeader = function hasHeader(name) { 621 | return name.toLowerCase() in mockResponse._headers; 622 | }; 623 | 624 | /** 625 | * Function: setHeader 626 | * Function: set 627 | * 628 | * Set a particular header by name. 629 | */ 630 | mockResponse.setHeader = function setHeader(name, value) { 631 | mockResponse._headers[name.toLowerCase()] = value; 632 | return this; 633 | }; 634 | 635 | /** 636 | * Function: appendHeader 637 | * 638 | * Append a header by name. If a header already exists, the new value is appended to the existing header. 639 | */ 640 | mockResponse.appendHeader = function appendHeader(name, value) { 641 | mockResponse.append(name, value); 642 | return this; 643 | }; 644 | 645 | /** 646 | * Function: removeHeader 647 | * 648 | * Removes an HTTP header by name. 649 | */ 650 | mockResponse.removeHeader = function removeHeader(name) { 651 | delete mockResponse._headers[name.toLowerCase()]; 652 | }; 653 | 654 | /** 655 | * Function: setEncoding 656 | * 657 | * Sets the encoding for the data. Generally 'utf8'. 658 | * 659 | * Parameters: 660 | * 661 | * encoding - The string representing the encoding value. 662 | */ 663 | mockResponse.setEncoding = function setEncoding(encoding) { 664 | _encoding = encoding; 665 | }; 666 | 667 | mockResponse.getEncoding = function getEncoding() { 668 | return _encoding; 669 | }; 670 | 671 | /** 672 | * Function: redirect 673 | * 674 | * Redirect to a url with response code 675 | */ 676 | mockResponse.redirect = function redirect(a, b) { 677 | switch (arguments.length) { 678 | case 1: 679 | mockResponse.statusCode = 302; 680 | _redirectUrl = a; 681 | break; 682 | 683 | case 2: 684 | if (typeof a === 'number') { 685 | mockResponse.statusCode = a; 686 | _redirectUrl = b; 687 | } 688 | break; 689 | 690 | default: 691 | break; 692 | } 693 | mockResponse.end(); 694 | }; 695 | 696 | /** 697 | * Function: render 698 | * 699 | * Render a view with a callback responding with the 700 | * rendered string. 701 | */ 702 | mockResponse.render = function render(a, b, c) { 703 | _renderView = a; 704 | 705 | let data = b; 706 | let done = c; 707 | 708 | // support callback function as second arg 709 | if (typeof b === 'function') { 710 | done = b; 711 | data = {}; 712 | } 713 | 714 | switch (arguments.length) { 715 | case 2: 716 | case 3: 717 | _renderData = data; 718 | break; 719 | 720 | default: 721 | break; 722 | } 723 | 724 | if (typeof done === 'function') { 725 | done(null, ''); 726 | } else { 727 | mockResponse.emit('render'); 728 | mockResponse.end(); 729 | } 730 | }; 731 | 732 | /** 733 | * Chooses the correct response function from the given supported types. 734 | * This method requires that the mockResponse object be initialized with a 735 | * mockRequest object reference, otherwise it will throw. @see createMocks. 736 | * 737 | * @param {Object} supported Object with formats to handler functions. 738 | * @return {Object} Whatever handler function returns. 739 | */ 740 | mockResponse.format = function format(supported = {}) { 741 | const types = Object.keys(supported); 742 | 743 | if (types.length === 0) { 744 | return mockResponse.sendStatus(406); 745 | } 746 | 747 | if (!mockRequest) { 748 | throw new Error( 749 | 'Request object unavailable. Use createMocks or pass in a request object in createResponse to use format.' 750 | ); 751 | } 752 | 753 | const accepted = mockRequest.accepts(types); 754 | 755 | if (accepted) { 756 | return supported[accepted](); 757 | } 758 | 759 | if (supported.default) { 760 | return supported.default(); 761 | } 762 | 763 | return mockResponse.sendStatus(406); 764 | }; 765 | 766 | // WritableStream.writable is not a function 767 | // mockResponse.writable = function() { 768 | // return writableStream.writable.apply(this, arguments); 769 | // }; 770 | 771 | // mockResponse.end = function(){ 772 | // return writableStream.end.apply(this, arguments); 773 | // }; 774 | 775 | mockResponse.destroy = function destroy(...args) { 776 | return writableStream.destroy.apply(this, args); 777 | }; 778 | 779 | mockResponse.destroySoon = function destroySoon(...args) { 780 | return writableStream.destroySoon.apply(this, args); 781 | }; 782 | 783 | // This mock object stores some state as well 784 | // as some test-analysis functions: 785 | 786 | /** 787 | * Function: _isEndCalled 788 | * 789 | * Since the function must be called, this function 790 | * returns true if it has been called. False otherwise. 791 | */ 792 | mockResponse._isEndCalled = function _isEndCalled() { 793 | return _endCalled; 794 | }; 795 | 796 | /** 797 | * Function: _getHeaders 798 | * 799 | * Returns all the headers that were set. This may be an 800 | * empty object, but probably will have "Content-Type" set. 801 | */ 802 | mockResponse._getHeaders = function _getHeaders() { 803 | return mockResponse._headers; 804 | }; 805 | 806 | /** 807 | * Function: _getLocals 808 | * 809 | * Returns all the locals that were set. 810 | */ 811 | mockResponse._getLocals = function _getLocals() { 812 | return mockResponse.locals; 813 | }; 814 | 815 | /** 816 | * Function: _getData 817 | * 818 | * The data sent to the user. 819 | */ 820 | mockResponse._getData = function _getData() { 821 | return _data; 822 | }; 823 | 824 | /** 825 | * Function: _getJSONData 826 | * 827 | * The data sent to the user as JSON. 828 | */ 829 | mockResponse._getJSONData = function _getJSONData() { 830 | return JSON.parse(_data); 831 | }; 832 | 833 | /** 834 | * Function: _getBuffer 835 | * 836 | * The buffer containing data to be sent to the user. 837 | * Non-empty if Buffers were given in calls to write() and end() 838 | */ 839 | mockResponse._getBuffer = function _getBuffer() { 840 | return _buffer; 841 | }; 842 | 843 | /** 844 | * Function: _getChunks 845 | * 846 | * The buffer containing data to be sent to the user. 847 | * Non-empty if Buffers were given in calls to write() and end() 848 | */ 849 | mockResponse._getChunks = function _getChunks() { 850 | return _chunks; 851 | }; 852 | 853 | /** 854 | * Function: _getStatusCode 855 | * 856 | * The status code that was sent to the user. 857 | */ 858 | mockResponse._getStatusCode = function _getStatusCode() { 859 | return mockResponse.statusCode; 860 | }; 861 | 862 | /** 863 | * Function: _getStatusMessage 864 | * 865 | * The status message that was sent to the user. 866 | */ 867 | mockResponse._getStatusMessage = function _getStatusMessage() { 868 | return mockResponse.statusMessage; 869 | }; 870 | 871 | /** 872 | * Function: _isJSON 873 | * 874 | * Returns true if the data sent was defined as JSON. 875 | * It doesn't validate the data that was sent. 876 | */ 877 | mockResponse._isJSON = function _isJSON() { 878 | return mockResponse.getHeader('Content-Type') === 'application/json'; 879 | }; 880 | 881 | /** 882 | * Function: _isUTF8 883 | * 884 | * If the encoding was set, and it was set to UTF-8, then 885 | * this function return true. False otherwise. 886 | * 887 | * Returns: 888 | * 889 | * False if the encoding wasn't set and wasn't set to "utf8". 890 | */ 891 | mockResponse._isUTF8 = function _isUTF8() { 892 | if (!_encoding) { 893 | return false; 894 | } 895 | return _encoding === 'utf8'; 896 | }; 897 | 898 | /** 899 | * Function: _isDataLengthValid 900 | * 901 | * If the Content-Length header was set, this will only 902 | * return true if the length is actually the length of the 903 | * data that was set. 904 | * 905 | * Returns: 906 | * 907 | * True if the "Content-Length" header was not 908 | * set. Otherwise, it compares it. 909 | */ 910 | mockResponse._isDataLengthValid = function _isDataLengthValid() { 911 | if (mockResponse.getHeader('Content-Length')) { 912 | return mockResponse.getHeader('Content-Length').toString() === _data.length.toString(); 913 | } 914 | return true; 915 | }; 916 | 917 | /** 918 | * Function: _getRedirectUrl 919 | * 920 | * Return redirect url of redirect method 921 | * 922 | * Returns: 923 | * 924 | * Redirect url 925 | */ 926 | mockResponse._getRedirectUrl = function _getRedirectUrl() { 927 | return _redirectUrl; 928 | }; 929 | 930 | /** 931 | * Function: _getRenderView 932 | * 933 | * Return render view of render method 934 | * 935 | * Returns: 936 | * 937 | * render view 938 | */ 939 | mockResponse._getRenderView = function _getRenderView() { 940 | return _renderView; 941 | }; 942 | 943 | /** 944 | * Function: _getRenderData 945 | * 946 | * Return render data of render method 947 | * 948 | * Returns: 949 | * 950 | * render data 951 | */ 952 | mockResponse._getRenderData = function _getRenderData() { 953 | return _renderData; 954 | }; 955 | 956 | return mockResponse; 957 | } 958 | 959 | module.exports.createResponse = createResponse; 960 | -------------------------------------------------------------------------------- /lib/mockWritableStream.js: -------------------------------------------------------------------------------- 1 | /* 2 | * http://nodejs.org/api/stream.html#stream_writable_stream 3 | */ 4 | 5 | function WritableStream() {} 6 | 7 | Object.defineProperty(WritableStream, 'writable', { 8 | configurable: true, 9 | enumerable: true, 10 | get() { 11 | return true; 12 | } 13 | }); 14 | 15 | // WritableStream.prototype.write = function(string, [encoding], [fd]){} 16 | // WritableStream.prototype.write = function(buffer){} 17 | WritableStream.prototype.end = () => {}; 18 | // WritableStream.prototype.end = function(string, encoding){} 19 | // WritableStream.prototype.end = function(buffer){} 20 | WritableStream.prototype.destroy = () => {}; 21 | WritableStream.prototype.destroySoon = () => {}; 22 | 23 | module.exports = WritableStream; 24 | -------------------------------------------------------------------------------- /lib/node/_http_incoming.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const Stream = require('stream'); 3 | 4 | function readStart() {} 5 | exports.readStart = readStart; 6 | 7 | function readStop() {} 8 | exports.readStop = readStop; 9 | 10 | function IncomingMessage() { 11 | Stream.Readable.call(this); 12 | 13 | this.httpVersionMajor = null; 14 | this.httpVersionMinor = null; 15 | this.httpVersion = null; 16 | this.complete = false; 17 | this.headers = {}; 18 | this.rawHeaders = []; 19 | this.trailers = {}; 20 | this.rawTrailers = []; 21 | 22 | this.readable = true; 23 | 24 | this._pendings = []; 25 | this._pendingIndex = 0; 26 | this.upgrade = null; 27 | 28 | this.url = ''; 29 | this.method = null; 30 | 31 | this.statusCode = null; 32 | this.statusMessage = null; 33 | 34 | this._consuming = false; 35 | 36 | this._dumped = false; 37 | } 38 | util.inherits(IncomingMessage, Stream.Readable); 39 | 40 | exports.IncomingMessage = IncomingMessage; 41 | 42 | IncomingMessage.prototype.read = () => {}; 43 | IncomingMessage.prototype._read = () => {}; 44 | IncomingMessage.prototype.destroy = () => {}; 45 | 46 | IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { 47 | if (callback) { 48 | setTimeout(callback, msecs); 49 | } 50 | }; 51 | 52 | IncomingMessage.prototype._addHeaderLines = function _addHeaderLines(headers, n) { 53 | if (headers && headers.length) { 54 | let raw; 55 | let dest; 56 | if (this.complete) { 57 | raw = this.rawTrailers; 58 | dest = this.trailers; 59 | } else { 60 | raw = this.rawHeaders; 61 | dest = this.headers; 62 | } 63 | 64 | for (let i = 0; i < n; i += 2) { 65 | const k = headers[i]; 66 | const v = headers[i + 1]; 67 | raw.push(k); 68 | raw.push(v); 69 | this._addHeaderLine(k, v, dest); 70 | } 71 | } 72 | }; 73 | 74 | IncomingMessage.prototype._addHeaderLine = function _addHeaderLine(field, value, dest) { 75 | const fieldName = field.toLowerCase(); 76 | switch (fieldName) { 77 | // Array headers: 78 | case 'set-cookie': 79 | if (!util.isUndefined(dest[fieldName])) { 80 | // eslint-disable-next-line no-param-reassign 81 | dest[fieldName].push(value); 82 | } else { 83 | // eslint-disable-next-line no-param-reassign 84 | dest[fieldName] = [value]; 85 | } 86 | break; 87 | 88 | case 'content-type': 89 | case 'content-length': 90 | case 'user-agent': 91 | case 'referer': 92 | case 'host': 93 | case 'authorization': 94 | case 'proxy-authorization': 95 | case 'if-modified-since': 96 | case 'if-unmodified-since': 97 | case 'from': 98 | case 'location': 99 | case 'max-forwards': 100 | if (util.isUndefined(dest[fieldName])) { 101 | // eslint-disable-next-line no-param-reassign 102 | dest[fieldName] = value; 103 | } 104 | break; 105 | 106 | default: 107 | if (!util.isUndefined(dest[fieldName])) { 108 | // eslint-disable-next-line no-param-reassign 109 | dest[fieldName] += `, ${value}`; 110 | } else { 111 | // eslint-disable-next-line no-param-reassign 112 | dest[fieldName] = value; 113 | } 114 | } 115 | }; 116 | 117 | IncomingMessage.prototype._dump = function _dump() { 118 | if (!this._dumped) { 119 | this._dumped = true; 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /lib/node/_http_server.js: -------------------------------------------------------------------------------- 1 | exports.STATUS_CODES = { 2 | 100: 'Continue', 3 | 101: 'Switching Protocols', 4 | 102: 'Processing', 5 | 200: 'OK', 6 | 201: 'Created', 7 | 202: 'Accepted', 8 | 203: 'Non-Authoritative Information', 9 | 204: 'No Content', 10 | 205: 'Reset Content', 11 | 206: 'Partial Content', 12 | 207: 'Multi-Status', 13 | 300: 'Multiple Choices', 14 | 301: 'Moved Permanently', 15 | 302: 'Moved Temporarily', 16 | 303: 'See Other', 17 | 304: 'Not Modified', 18 | 305: 'Use Proxy', 19 | 307: 'Temporary Redirect', 20 | 308: 'Permanent Redirect', 21 | 400: 'Bad Request', 22 | 401: 'Unauthorized', 23 | 402: 'Payment Required', 24 | 403: 'Forbidden', 25 | 404: 'Not Found', 26 | 405: 'Method Not Allowed', 27 | 406: 'Not Acceptable', 28 | 407: 'Proxy Authentication Required', 29 | 408: 'Request Time-out', 30 | 409: 'Conflict', 31 | 410: 'Gone', 32 | 411: 'Length Required', 33 | 412: 'Precondition Failed', 34 | 413: 'Request Entity Too Large', 35 | 414: 'Request-URI Too Large', 36 | 415: 'Unsupported Media Type', 37 | 416: 'Requested Range Not Satisfiable', 38 | 417: 'Expectation Failed', 39 | 418: "I'm a teapot", 40 | 422: 'Unprocessable Entity', 41 | 423: 'Locked', 42 | 424: 'Failed Dependency', 43 | 425: 'Unordered Collection', 44 | 426: 'Upgrade Required', 45 | 428: 'Precondition Required', 46 | 429: 'Too Many Requests', 47 | 431: 'Request Header Fields Too Large', 48 | 500: 'Internal Server Error', 49 | 501: 'Not Implemented', 50 | 502: 'Bad Gateway', 51 | 503: 'Service Unavailable', 52 | 504: 'Gateway Time-out', 53 | 505: 'HTTP Version Not Supported', 54 | 506: 'Variant Also Negotiates', 55 | 507: 'Insufficient Storage', 56 | 509: 'Bandwidth Limit Exceeded', 57 | 510: 'Not Extended', 58 | 511: 'Network Authentication Required' 59 | }; 60 | -------------------------------------------------------------------------------- /lib/node/http.js: -------------------------------------------------------------------------------- 1 | const server = require('./_http_server'); 2 | 3 | exports.IncomingMessage = require('./_http_incoming').IncomingMessage; 4 | 5 | exports.STATUS_CODES = server.STATUS_CODES; 6 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | module.exports.convertKeysToLowerCase = (map) => { 2 | const newMap = {}; 3 | for (const key in map) { 4 | if ({}.hasOwnProperty.call(map, key)) { 5 | newMap[key.toLowerCase()] = map[key]; 6 | } 7 | } 8 | return newMap; 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Howard Abrams (http://www.github.com/howardabrams)", 3 | "name": "node-mocks-http", 4 | "description": "Mock 'http' objects for testing Express, Next.js and Koa routing functions", 5 | "version": "1.17.2", 6 | "homepage": "https://github.com/eugef/node-mocks-http", 7 | "bugs": { 8 | "url": "https://github.com/eugef/node-mocks-http/issues" 9 | }, 10 | "contributors": [ 11 | { 12 | "name": "Howard Abrams", 13 | "email": "howard.abrams@gmail.com", 14 | "url": "https://github.com/howardabrams" 15 | }, 16 | { 17 | "name": "Johnny Estilles", 18 | "email": "johnny.estilles@agentia.asia", 19 | "url": "https://github.com/JohnnyEstilles" 20 | }, 21 | { 22 | "name": "Eugene Fidelin", 23 | "email": "eugene.fidelin@gmail.com", 24 | "url": "https://github.com/eugef" 25 | } 26 | ], 27 | "license": "MIT", 28 | "keywords": [ 29 | "mock", 30 | "stub", 31 | "dummy", 32 | "nodejs", 33 | "js", 34 | "testing", 35 | "test", 36 | "http", 37 | "http mock" 38 | ], 39 | "repository": { 40 | "type": "git", 41 | "url": "git://github.com/eugef/node-mocks-http.git" 42 | }, 43 | "main": "./lib/http-mock.js", 44 | "types": "./lib/http-mock.d.ts", 45 | "engines": { 46 | "node": ">=14" 47 | }, 48 | "dependencies": { 49 | "accepts": "^1.3.7", 50 | "content-disposition": "^0.5.3", 51 | "depd": "^1.1.0", 52 | "fresh": "^0.5.2", 53 | "merge-descriptors": "^1.0.1", 54 | "methods": "^1.1.2", 55 | "mime": "^1.3.4", 56 | "parseurl": "^1.3.3", 57 | "range-parser": "^1.2.0", 58 | "type-is": "^1.6.18" 59 | }, 60 | "peerDependencies": { 61 | "@types/express": "^4.17.21 || ^5.0.0", 62 | "@types/node": "*" 63 | }, 64 | "peerDependenciesMeta": { 65 | "@types/express": { 66 | "optional": true 67 | }, 68 | "@types/node": { 69 | "optional": true 70 | } 71 | }, 72 | "devDependencies": { 73 | "@types/chai": "^4.3.11", 74 | "@types/mocha": "^10.0.6", 75 | "@typescript-eslint/eslint-plugin": "^6.17.0", 76 | "@typescript-eslint/parser": "^6.17.0", 77 | "chai": "^4.2.0", 78 | "eslint": "^8.56.0", 79 | "eslint-config-airbnb-base": "^15.0.0", 80 | "eslint-config-prettier": "^9.1.0", 81 | "eslint-plugin-import": "^2.29.1", 82 | "husky": "^8.0.3", 83 | "mocha": "^10.2.0", 84 | "nyc": "^15.1.0", 85 | "prettier": "^3.1.1", 86 | "sinon": "^17.0.1", 87 | "sinon-chai": "^3.5.0", 88 | "ts-node": "^10.9.1", 89 | "tsd": "^0.29.0", 90 | "typescript": "^5.3.2" 91 | }, 92 | "scripts": { 93 | "test": "mocha", 94 | "coverage": "nyc --reporter=html --reporter=lcov --reporter=text-summary mocha", 95 | "format": "prettier --write --list-different .", 96 | "lint": "eslint --fix .", 97 | "check": "npm run check:format && npm run check:lint && npm run check:types", 98 | "check:format": "prettier --list-different .", 99 | "check:lint": "eslint .", 100 | "check:types": "tsd --files ./test/**/*.test-d.ts .", 101 | "postversion": "npm publish && git push --follow-tags", 102 | "prepare": "husky install" 103 | }, 104 | "files": [ 105 | "lib" 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /test/lib/headers.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test: headers.spec.js 3 | * 4 | * This file tests the Headers implementation which provides both 5 | * Express.js style header access and Web API Headers interface functionality. 6 | */ 7 | const { expect } = require('chai'); 8 | const { createHeaders } = require('../../lib/headers'); 9 | 10 | describe('Headers', () => { 11 | describe('Headers basic HTTP', () => { 12 | it('should create an empty headers object', () => { 13 | const headers = createHeaders(); 14 | expect(headers).to.be.an('object'); 15 | expect(Object.keys(headers).length).to.equal(0); 16 | }); 17 | 18 | it('should initialize with provided headers', () => { 19 | const initialHeaders = { 'Content-Type': 'application/json' }; 20 | const headers = createHeaders(initialHeaders); 21 | 22 | expect(headers['Content-Type']).to.equal('application/json'); 23 | }); 24 | 25 | it('should allow to directly access headers', () => { 26 | const headers = createHeaders(); 27 | headers['Content-Type'] = 'application/json'; 28 | 29 | expect(headers['Content-Type']).to.equal('application/json'); 30 | }); 31 | }); 32 | 33 | describe('Headers Web API Methods', () => { 34 | describe('#get()', () => { 35 | it('should get a header value', () => { 36 | const headers = createHeaders({ 'Content-Type': 'application/json' }); 37 | 38 | expect(headers.get('content-type')).to.equal('application/json'); 39 | expect(headers.get('Content-Type')).to.equal('application/json'); 40 | }); 41 | 42 | it('should return undefined for non-existent headers', () => { 43 | const headers = createHeaders(); 44 | 45 | expect(headers.get('cContent-Type')).to.be.undefined; 46 | }); 47 | 48 | it('should handle the referer/referrer special case', () => { 49 | const headersWithReferer = createHeaders({ referer: 'http://example.com' }); 50 | expect(headersWithReferer.get('referrer')).to.equal('http://example.com'); 51 | 52 | const headersWithReferrer = createHeaders({ referrer: 'http://example.com' }); 53 | expect(headersWithReferrer.get('referer')).to.equal('http://example.com'); 54 | }); 55 | }); 56 | 57 | describe('#getAll()', () => { 58 | it('should get all values for a header as an array', () => { 59 | const headers = createHeaders({ 'Set-Cookie': ['cookie1=value1', 'cookie2=value2'] }); 60 | 61 | expect(headers.getAll('Set-Cookie')).to.deep.equal(['cookie1=value1', 'cookie2=value2']); 62 | }); 63 | 64 | it('should return a single value as an array', () => { 65 | const headers = createHeaders({ 'Content-Type': 'application/json' }); 66 | 67 | expect(headers.getAll('Content-Type')).to.deep.equal(['application/json']); 68 | }); 69 | 70 | it('should return an empty array for non-existent headers', () => { 71 | const headers = createHeaders(); 72 | 73 | expect(headers.getAll('Content-Type')).to.deep.equal([]); 74 | }); 75 | }); 76 | 77 | describe('#has()', () => { 78 | it('should check if a header exists', () => { 79 | const headers = createHeaders({ 'Content-Type': 'application/json' }); 80 | 81 | expect(headers.has('content-type')).to.be.true; 82 | expect(headers.has('Content-Type')).to.be.true; 83 | }); 84 | 85 | it('should return false for non-existent headers', () => { 86 | const headers = createHeaders(); 87 | 88 | expect(headers.has('Content-Type')).to.be.false; 89 | }); 90 | }); 91 | 92 | describe('#set()', () => { 93 | it('should set a header value', () => { 94 | const headers = createHeaders(); 95 | 96 | headers.set('Content-Type', 'application/json'); 97 | expect(headers['Content-Type']).to.equal('application/json'); 98 | }); 99 | 100 | it('should overwrite existing headers', () => { 101 | const headers = createHeaders({ 'Content-Type': 'text/html' }); 102 | 103 | headers.set('Content-Type', 'application/json'); 104 | expect(headers['Content-Type']).to.equal('application/json'); 105 | }); 106 | }); 107 | 108 | describe('#append()', () => { 109 | it('should append a value to a non-existent header', () => { 110 | const headers = createHeaders(); 111 | 112 | headers.append('Content-Type', 'application/json'); 113 | expect(headers['Content-Type']).to.equal('application/json'); 114 | }); 115 | 116 | it('should convert a single value to an array when appending', () => { 117 | const headers = createHeaders({ Accept: 'text/html' }); 118 | 119 | headers.append('Accept', 'application/json'); 120 | expect(headers.Accept).to.deep.equal(['text/html', 'application/json']); 121 | }); 122 | 123 | it('should append to an existing array of values', () => { 124 | const headers = createHeaders({ 'Set-Cookie': ['cookie1=value1'] }); 125 | 126 | headers.append('Set-Cookie', 'cookie2=value2'); 127 | expect(headers['Set-Cookie']).to.deep.equal(['cookie1=value1', 'cookie2=value2']); 128 | }); 129 | }); 130 | 131 | describe('#delete()', () => { 132 | it('should delete a header', () => { 133 | const headers = createHeaders({ 'Content-Type': 'application/json' }); 134 | 135 | headers.delete('Content-Type'); 136 | expect(headers['Content-Type']).to.be.undefined; 137 | expect('Content-Type' in headers).to.be.false; 138 | }); 139 | 140 | it('should handle case-insensitive deletion', () => { 141 | const headers = createHeaders({ 'Content-Type': 'application/json' }); 142 | 143 | headers.delete('Content-Type'); 144 | expect('content-type' in headers).to.be.false; 145 | }); 146 | }); 147 | 148 | describe('#forEach()', () => { 149 | it('should iterate over all headers', () => { 150 | const headers = createHeaders({ 151 | 'Content-Type': 'application/json', 152 | Accept: 'text/html', 153 | 'X-Custom': 'custom-value' 154 | }); 155 | 156 | const result = {}; 157 | headers.forEach((value, key) => { 158 | result[key] = value; 159 | }); 160 | 161 | expect(result).to.deep.equal({ 162 | 'content-type': 'application/json', 163 | accept: 'text/html', 164 | 'x-custom': 'custom-value' 165 | }); 166 | }); 167 | 168 | it('should respect thisArg parameter', () => { 169 | const headers = createHeaders({ 'Content-Type': 'application/json' }); 170 | const context = { value: 'context' }; 171 | 172 | headers.forEach(function iterator() { 173 | expect(this).to.equal(context); 174 | }, context); 175 | }); 176 | }); 177 | }); 178 | 179 | describe('Iterable Interface', () => { 180 | it('should implement entries() iterator', () => { 181 | const headers = createHeaders({ 182 | 'Content-Type': 'application/json', 183 | Accept: 'text/html' 184 | }); 185 | 186 | const entries = Array.from(headers.entries()); 187 | expect(entries).to.deep.include(['content-type', 'application/json']); 188 | expect(entries).to.deep.include(['accept', 'text/html']); 189 | }); 190 | 191 | it('should implement keys() iterator', () => { 192 | const headers = createHeaders({ 193 | 'Content-Type': 'application/json', 194 | Accept: 'text/html' 195 | }); 196 | 197 | const keys = Array.from(headers.keys()); 198 | expect(keys).to.include('content-type'); 199 | expect(keys).to.include('accept'); 200 | }); 201 | 202 | it('should implement values() iterator', () => { 203 | const headers = createHeaders({ 204 | 'Content-Type': 'application/json', 205 | Accept: 'text/html' 206 | }); 207 | 208 | const values = Array.from(headers.values()); 209 | expect(values).to.include('application/json'); 210 | expect(values).to.include('text/html'); 211 | }); 212 | 213 | it('should be iterable with Symbol.iterator', () => { 214 | const headers = createHeaders({ 215 | 'Content-Type': 'application/json', 216 | Accept: 'text/html' 217 | }); 218 | 219 | const entries = Array.from(headers); 220 | expect(entries).to.deep.include(['content-type', 'application/json']); 221 | expect(entries).to.deep.include(['accept', 'text/html']); 222 | }); 223 | }); 224 | 225 | describe('Property Operations', () => { 226 | it('should delete properties in a case-insensitive manner', () => { 227 | const headers = createHeaders({ 'Content-Type': 'application/json' }); 228 | 229 | delete headers['content-type']; 230 | expect('Content-Type' in headers).to.be.false; 231 | }); 232 | 233 | it('should list all header keys with Object.keys', () => { 234 | const headers = createHeaders({ 235 | 'content-type': 'application/json', 236 | accept: 'text/html' 237 | }); 238 | 239 | const keys = Object.keys(headers); 240 | expect(keys).to.include('content-type'); 241 | expect(keys).to.include('accept'); 242 | expect(keys.length).to.equal(2); 243 | }); 244 | }); 245 | }); 246 | -------------------------------------------------------------------------------- /test/lib/http-mock.test-d.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage as NodeRequest, OutgoingMessage as NodeResponse } from 'http'; 2 | // eslint-disable-next-line import/no-unresolved 3 | import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd'; 4 | // eslint-disable-next-line import/no-unresolved 5 | import { Request as ExpressRequest, Response as ExpressResponse } from 'express'; 6 | 7 | import { createMocks, createRequest, createResponse, MockRequest, MockResponse, Mocks } from '../../lib/http-mock'; 8 | 9 | expectType>(createRequest()); 10 | expectNotType>(createRequest()); 11 | expectAssignable(createRequest()); 12 | expectAssignable(createRequest()); 13 | 14 | expectType>(createRequest()); 15 | expectNotType>(createRequest()); 16 | expectAssignable(createRequest()); 17 | expectNotAssignable(createRequest()); 18 | 19 | expectType>(createResponse()); 20 | expectNotType>(createResponse()); 21 | expectAssignable(createResponse()); 22 | expectAssignable(createResponse()); 23 | 24 | expectType>(createResponse()); 25 | expectNotType>(createResponse()); 26 | expectAssignable(createResponse()); 27 | expectNotAssignable(createResponse()); 28 | 29 | expectType>(createMocks()); 30 | // eslint-disable-next-line no-undef 31 | expectType>(createMocks()); 32 | -------------------------------------------------------------------------------- /test/lib/mockEventEmitter.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | 3 | const { expect } = chai; 4 | 5 | const MockEventEmitter = require('../../lib/mockEventEmitter'); 6 | 7 | let mockEventEmitter; 8 | 9 | describe('mockEventEmitter', () => { 10 | before(() => { 11 | mockEventEmitter = new MockEventEmitter(); 12 | }); 13 | 14 | it('should be a function', () => { 15 | expect(MockEventEmitter).to.be.a('function'); 16 | }); 17 | 18 | it('should be an object factory', () => { 19 | expect(mockEventEmitter).to.be.a('object'); 20 | expect(mockEventEmitter).to.be.an.instanceof(MockEventEmitter); 21 | }); 22 | 23 | it('should expose "MockEventEmitter" prototype', () => { 24 | expect(mockEventEmitter).to.have.property('addListener'); 25 | expect(mockEventEmitter.addListener).to.be.a('function'); 26 | 27 | expect(mockEventEmitter).to.have.property('on'); 28 | expect(mockEventEmitter.on).to.be.a('function'); 29 | 30 | expect(mockEventEmitter).to.have.property('once'); 31 | expect(mockEventEmitter.once).to.be.a('function'); 32 | 33 | expect(mockEventEmitter).to.have.property('removeListener'); 34 | expect(mockEventEmitter.removeListener).to.be.a('function'); 35 | 36 | expect(mockEventEmitter).to.have.property('removeAllListeners'); 37 | expect(mockEventEmitter.removeAllListeners).to.be.a('function'); 38 | 39 | expect(mockEventEmitter).to.have.property('setMaxListeners'); 40 | expect(mockEventEmitter.setMaxListeners).to.be.a('function'); 41 | 42 | expect(mockEventEmitter).to.have.property('listeners'); 43 | expect(mockEventEmitter.listeners).to.be.a('function'); 44 | 45 | expect(mockEventEmitter).to.have.property('emit'); 46 | expect(mockEventEmitter.emit).to.be.a('function'); 47 | 48 | expect(mockEventEmitter).to.have.property('prependListener'); 49 | expect(mockEventEmitter.prependListener).to.be.a('function'); 50 | }); 51 | 52 | it('should return undefined when methods called', () => { 53 | expect(mockEventEmitter.addListener()).to.be.an('undefined'); 54 | expect(mockEventEmitter.on()).to.be.an('undefined'); 55 | expect(mockEventEmitter.once()).to.be.an('undefined'); 56 | expect(mockEventEmitter.removeListener()).to.be.an('undefined'); 57 | expect(mockEventEmitter.removeAllListeners()).to.be.an('undefined'); 58 | expect(mockEventEmitter.setMaxListeners()).to.be.an('undefined'); 59 | expect(mockEventEmitter.listeners()).to.be.an('undefined'); 60 | expect(mockEventEmitter.emit()).to.be.an('undefined'); 61 | expect(mockEventEmitter.prependListener()).to.be.an('undefined'); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/lib/mockExpressResponse.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | 3 | const { expect } = chai; 4 | const sinonChai = require('sinon-chai'); 5 | 6 | chai.use(sinonChai); 7 | 8 | const mockExpressResponse = require('../../lib/express/mock-express').response; 9 | 10 | describe('mockExpressResponse', () => { 11 | it('should expose .createResponse()', () => { 12 | expect(mockExpressResponse.createResponse).to.be.a('function'); 13 | }); 14 | 15 | describe('.createResponse()', () => { 16 | let response; 17 | 18 | before(() => { 19 | response = mockExpressResponse.createResponse(); 20 | }); 21 | 22 | it('should return an object', () => { 23 | expect(response).to.be.an('object'); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/lib/mockRequest.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as url from 'url'; 3 | import * as querystring from 'querystring'; 4 | import parseRange from 'range-parser'; 5 | import { EventEmitter } from 'events'; 6 | import { IncomingMessage } from 'http'; 7 | 8 | import * as mockRequest from '../../lib/http-mock'; 9 | 10 | describe('mockRequest', () => { 11 | it('should expose .createRequest()', () => { 12 | expect(mockRequest.createRequest).to.be.a('function'); 13 | }); 14 | 15 | describe('.createRequest()', () => { 16 | describe('without options', () => { 17 | it('should have event emitter prototype functions', () => { 18 | const request = mockRequest.createRequest(); 19 | expect(request.on).to.be.a('function'); 20 | expect(request.once).to.be.a('function'); 21 | expect(request.emit).to.be.a('function'); 22 | }); 23 | 24 | it('should return an object', () => { 25 | const request = mockRequest.createRequest(); 26 | expect(request).to.be.an('object'); 27 | }); 28 | 29 | it('should be an EventEmitter', () => { 30 | const request = mockRequest.createRequest(); 31 | expect(request).to.be.an.instanceof(EventEmitter); 32 | }); 33 | 34 | it('should expose Express Request object methods', () => { 35 | const request = mockRequest.createRequest(); 36 | expect(request).to.have.property('get'); 37 | expect(request.get).to.be.a('function'); 38 | 39 | expect(request).to.have.property('header'); 40 | expect(request.header).to.be.a('function'); 41 | 42 | expect(request).to.have.property('param'); 43 | expect(request.param).to.be.a('function'); 44 | }); 45 | 46 | it('should initialize with default options', () => { 47 | const request = mockRequest.createRequest(); 48 | expect(request.method).to.equal('GET'); 49 | expect(request.url).to.equal(''); 50 | expect(request.originalUrl).to.equal(request.url); 51 | expect(request.baseUrl).to.equal(request.url); 52 | expect(request.path).to.equal(''); 53 | expect(request.params).to.deep.equal({}); 54 | expect(request.session).to.be.a('undefined'); 55 | expect(request.cookies).to.deep.equal({}); 56 | expect(request.signedCookies).to.be.a('undefined'); 57 | expect(request.headers).to.be.an('object'); 58 | expect(Object.keys(request.headers).length).to.equal(0); 59 | expect(request.body).to.deep.equal({}); 60 | expect(request.query).to.deep.equal({}); 61 | expect(request.files).to.deep.equal({}); 62 | expect(request.ip).to.equal('127.0.0.1'); 63 | expect(request.ips).to.deep.equal([request.ip]); 64 | }); 65 | }); 66 | 67 | describe('with options', () => { 68 | it('should set .method to options.method', () => { 69 | const options: mockRequest.RequestOptions = { 70 | method: 'POST' 71 | }; 72 | 73 | const request = mockRequest.createRequest(options); 74 | expect(request.method).to.equal(options.method); 75 | }); 76 | 77 | it('should set .url to options.url', () => { 78 | const options = { 79 | url: '/this/is/a/url' 80 | }; 81 | 82 | const request = mockRequest.createRequest(options); 83 | expect(request.url).to.equal(options.url); 84 | expect(request.originalUrl).to.equal(options.url); 85 | expect(request.baseUrl).to.equal(options.url); 86 | }); 87 | 88 | it('should set .url automatically', () => { 89 | const options = { 90 | path: '/this/is/a/path' 91 | }; 92 | 93 | const expectedUrl = '/this/is/a/path'; 94 | 95 | const request = mockRequest.createRequest(options); 96 | expect(request.url).to.equal(expectedUrl); 97 | }); 98 | 99 | it('should set .baseUrl to options.baseUrl', () => { 100 | const options = { 101 | baseUrl: '/this' 102 | }; 103 | 104 | const request = mockRequest.createRequest(options); 105 | expect(request.baseUrl).to.equal(options.baseUrl); 106 | }); 107 | 108 | it('should set .originalUrl to options.originalUrl', () => { 109 | const options = { 110 | originalUrl: '/this/is/a/url' 111 | }; 112 | 113 | const request = mockRequest.createRequest(options); 114 | expect(request.originalUrl).to.equal(options.originalUrl); 115 | }); 116 | 117 | it('should set .path to options.path', () => { 118 | const options = { 119 | path: '/this/is/a/path' 120 | }; 121 | 122 | const request = mockRequest.createRequest(options); 123 | expect(request.path).to.equal(options.path); 124 | }); 125 | 126 | it('should set .path to pathname of options.url', () => { 127 | const options = { 128 | url: '/this/is/a/url' 129 | }; 130 | 131 | const request = mockRequest.createRequest(options); 132 | expect(request.path).to.equal(url.parse(options.url).pathname); 133 | }); 134 | 135 | it('should set .params to options.params', () => { 136 | const options = { 137 | params: { 138 | key1: 'value1', 139 | key2: 'value2' 140 | } 141 | }; 142 | 143 | const request = mockRequest.createRequest(options); 144 | expect(request.params).to.deep.equal(options.params); 145 | }); 146 | 147 | it('should set .session to options.session', () => { 148 | const options = { 149 | session: { 150 | key1: 'value1', 151 | key2: 'value2' 152 | } 153 | }; 154 | 155 | const request = mockRequest.createRequest(options); 156 | expect(request.session).to.deep.equal(options.session); 157 | }); 158 | 159 | it('should set .cookies to options.cookies', () => { 160 | const options = { 161 | cookies: { 162 | key1: 'value1', 163 | key2: 'value2' 164 | } 165 | }; 166 | 167 | const request = mockRequest.createRequest(options); 168 | expect(request.cookies).to.deep.equal(options.cookies); 169 | }); 170 | 171 | it('should set .signedCookies to options.signedCookies', () => { 172 | const options = { 173 | signedCookies: { 174 | key1: 'value1', 175 | key2: 'value2' 176 | } 177 | }; 178 | 179 | const request = mockRequest.createRequest(options); 180 | expect(request.signedCookies).to.deep.equal(options.signedCookies); 181 | }); 182 | 183 | it('should set .headers to options.headers', () => { 184 | const options = { 185 | headers: { 186 | key1: 'value1', 187 | key2: 'value2' 188 | } 189 | }; 190 | 191 | const request = mockRequest.createRequest(options); 192 | expect(request.headers).to.deep.equal(options.headers); 193 | }); 194 | 195 | it('should set .headers to options.headers and be accessible via get() and header() case-insensitively', () => { 196 | const options = { 197 | headers: { 198 | KEY1: 'value1', 199 | Key2: 'value2' 200 | } 201 | }; 202 | 203 | const request = mockRequest.createRequest(options); 204 | expect(request.header('KEY1')).to.equal('value1'); 205 | expect(request.get('KEY1')).to.equal('value1'); 206 | expect(request.headers.get('KEY1')).to.equal('value1'); 207 | expect(request.getHeader('KEY1')).to.equal('value1'); 208 | 209 | expect(request.header('KEY2')).to.equal('value2'); 210 | expect(request.get('KEY2')).to.equal('value2'); 211 | expect(request.headers.get('KEY2')).to.equal('value2'); 212 | expect(request.getHeader('KEY2')).to.equal('value2'); 213 | }); 214 | 215 | it('should set .headers directly and be accessible via get() and header() case-insensitively', () => { 216 | const request = mockRequest.createRequest(); 217 | request.headers.KEY1 = 'value1'; 218 | 219 | expect(request.header('KEY1')).to.equal('value1'); 220 | expect(request.get('KEY1')).to.equal('value1'); 221 | expect(request.headers.get('KEY1')).to.equal('value1'); 222 | expect(request.getHeader('KEY1')).to.equal('value1'); 223 | expect(request.headers.KEY1).to.equal('value1'); 224 | }); 225 | 226 | it('should set .body to options.body', () => { 227 | const options = { 228 | body: { 229 | key1: 'value1', 230 | key2: 'value2' 231 | } 232 | }; 233 | 234 | const request = mockRequest.createRequest(options); 235 | expect(request.body).to.deep.equal(options.body); 236 | }); 237 | 238 | it('should set .query to options.query', () => { 239 | const options = { 240 | query: { 241 | key1: 'value1', 242 | key2: 'value2' 243 | } 244 | }; 245 | 246 | const request = mockRequest.createRequest(options); 247 | expect(request.query).to.deep.equal(options.query); 248 | }); 249 | 250 | it('should set .files to options.files', () => { 251 | const options = { 252 | files: { 253 | key1: 'value1', 254 | key2: 'value2' 255 | } 256 | }; 257 | 258 | const request = mockRequest.createRequest(options); 259 | expect(request.files).to.deep.equal(options.files); 260 | }); 261 | 262 | it('should set .query to url query params when options.query not set', () => { 263 | const options = { 264 | url: '/path/to/url?key1=value1&key2=value2' 265 | }; 266 | const parsedOptions = querystring.parse(options.url.split('?')[1]); 267 | 268 | const request = mockRequest.createRequest(options); 269 | expect(request.query).to.deep.equal(parsedOptions); 270 | }); 271 | 272 | it('should accept and set non-standard options passed to it', () => { 273 | const options = { 274 | mySampleProp: 'la LA LA' 275 | }; 276 | 277 | const request = mockRequest.createRequest(options); 278 | expect(request.mySampleProp).to.equal('la LA LA'); 279 | }); 280 | 281 | it('should set .ip to options.ip', () => { 282 | const options = { 283 | ip: '192.168.1.1' 284 | }; 285 | 286 | const request = mockRequest.createRequest(options); 287 | expect(request.ip).to.equal(options.ip); 288 | }); 289 | 290 | it('should set .ips to [options.ip]', () => { 291 | const options = { 292 | ip: '192.168.1.1' 293 | }; 294 | 295 | const request = mockRequest.createRequest(options); 296 | expect(request.ips).to.deep.equal([options.ip]); 297 | }); 298 | }); 299 | 300 | it('should be able to create a Fetch API Request object', () => { 301 | const request = mockRequest.createRequest(); 302 | expect(request.bodyUsed).to.be.undefined; 303 | }); 304 | }); 305 | 306 | describe('.get()/.header()', () => { 307 | it('should return header, when set in constructor', () => { 308 | const options = { 309 | headers: { 310 | 'Content-type': 'value' 311 | } 312 | }; 313 | 314 | const request = mockRequest.createRequest(options); 315 | expect(request.get('Content-type')).to.equal('value'); 316 | expect(request.header('Content-type')).to.equal('value'); 317 | expect(request.headers.get('Content-type')).to.equal('value'); 318 | expect(request.getHeader('Content-type')).to.equal('value'); 319 | expect(request.headers['Content-type']).to.equal('value'); 320 | }); 321 | 322 | it('should return header, when set explicitly', () => { 323 | const request = mockRequest.createRequest(); 324 | 325 | request.headers['Content-type'] = 'value'; 326 | 327 | expect(request.get('Content-type')).to.equal('value'); 328 | expect(request.header('Content-type')).to.equal('value'); 329 | expect(request.headers.get('Content-type')).to.equal('value'); 330 | expect(request.getHeader('Content-type')).to.equal('value'); 331 | expect(request.headers['Content-type']).to.equal('value'); 332 | }); 333 | 334 | it('should return header, when set as an object (deprecated)', () => { 335 | const request = mockRequest.createRequest(); 336 | 337 | // setting headers as an object is officially supported by Express 338 | // @ts-expect-error 339 | request.headers = { 'Content-type': 'value' }; 340 | 341 | expect(request.get('Content-type')).to.equal('value'); 342 | expect(request.header('Content-type')).to.equal('value'); 343 | expect(request.getHeader('Content-type')).to.equal('value'); 344 | expect(request.headers['Content-type']).to.equal('value'); 345 | 346 | // request.headers.get() is not working in this case 347 | }); 348 | 349 | it('should return referer, when request as referrer', () => { 350 | const options = { 351 | headers: { 352 | referer: 'value' 353 | } 354 | }; 355 | 356 | const request = mockRequest.createRequest(options); 357 | expect(request.get('referrer')).to.equal('value'); 358 | expect(request.header('referrer')).to.equal('value'); 359 | expect(request.headers.get('referrer')).to.equal('value'); 360 | expect(request.getHeader('referrer')).to.equal('value'); 361 | 362 | // direct access keeps the original name 363 | expect(request.headers.referer).to.equal('value'); 364 | }); 365 | 366 | it('should return referrer, when request as referer', () => { 367 | const options = { 368 | headers: { 369 | referrer: 'value' 370 | } 371 | }; 372 | 373 | const request = mockRequest.createRequest(options); 374 | expect(request.get('referer')).to.equal('value'); 375 | expect(request.header('referer')).to.equal('value'); 376 | expect(request.headers.get('referer')).to.equal('value'); 377 | expect(request.getHeader('referer')).to.equal('value'); 378 | 379 | // direct access keeps the original name 380 | expect(request.headers.referrer).to.equal('value'); 381 | }); 382 | 383 | it('should not return header, when not set', () => { 384 | const request = mockRequest.createRequest(); 385 | expect(request.get('key')).to.be.a('undefined'); 386 | expect(request.header('key')).to.be.a('undefined'); 387 | expect(request.headers.get('key')).to.be.a('undefined'); 388 | expect(request.getHeader('key')).to.be.a('undefined'); 389 | expect(request.headers.key).to.be.a('undefined'); 390 | }); 391 | }); 392 | 393 | describe('.is()', () => { 394 | it('should return type, when found in content-type header', () => { 395 | const options = { 396 | headers: { 397 | 'content-type': 'text/html', 398 | 'transfer-encoding': 'chunked' 399 | } 400 | }; 401 | 402 | const request = mockRequest.createRequest(options); 403 | expect(request.is('html')).to.equal('html'); 404 | }); 405 | 406 | it('should return first matching type, given array of types', () => { 407 | const options = { 408 | headers: { 409 | 'content-type': 'text/html', 410 | 'transfer-encoding': 'chunked' 411 | } 412 | }; 413 | 414 | const request = mockRequest.createRequest(options); 415 | expect(request.is(['json', 'html', 'text'])).to.equal('html'); 416 | }); 417 | 418 | it('should return false when type not found', () => { 419 | const options = { 420 | headers: { 421 | 'content-type': 'text/html', 422 | 'transfer-encoding': 'chunked' 423 | } 424 | }; 425 | 426 | const request = mockRequest.createRequest(options); 427 | expect(request.is(['json'])).to.equal(false); 428 | }); 429 | }); 430 | 431 | describe('.accepts()', () => { 432 | it('returns type if the same as Accept header', () => { 433 | const request = mockRequest.createRequest({ headers: { accept: 'text/html' } }); 434 | expect(request.accepts('text/html')).to.equal('text/html'); 435 | expect(request.accepts('html')).to.equal('html'); 436 | }); 437 | 438 | it('returns the first matching type of an array of types', () => { 439 | const request = mockRequest.createRequest({ headers: { accept: 'text/html' } }); 440 | expect(request.accepts(['json', 'html'])).to.equal('html'); 441 | }); 442 | 443 | it('returns false when types dont match', () => { 444 | const request = mockRequest.createRequest({ headers: { accept: 'text/html' } }); 445 | expect(request.accepts(['json', 'xml'])).to.equal(false); 446 | }); 447 | }); 448 | 449 | describe('.acceptsEncodings()', () => { 450 | it('returns type if the same as Accept-Encoding header', () => { 451 | const request = mockRequest.createRequest({ headers: { 'Accept-Encoding': 'gzip' } }); 452 | expect(request.acceptsEncodings('gzip')).to.equal('gzip'); 453 | }); 454 | 455 | it('returns the first matching type of an array of types', () => { 456 | const request = mockRequest.createRequest({ headers: { 'Accept-Encoding': 'gzip' } }); 457 | expect(request.acceptsEncodings(['compress', 'gzip'])).to.equal('gzip'); 458 | }); 459 | 460 | it('returns false when types dont match', () => { 461 | const request = mockRequest.createRequest({ headers: { 'Accept-Encoding': 'gzip' } }); 462 | expect(request.acceptsEncodings(['compress', 'deflate'])).to.equal(false); 463 | }); 464 | }); 465 | 466 | describe('.acceptsCharsets()', () => { 467 | let request: mockRequest.MockRequest; 468 | 469 | beforeEach(() => { 470 | request = mockRequest.createRequest({ headers: { 'Accept-Charset': 'utf-8' } }); 471 | }); 472 | 473 | it('returns type if the same as Accept-Charset header', () => { 474 | expect(request.acceptsCharsets('utf-8')).to.equal('utf-8'); 475 | }); 476 | 477 | it('returns the first matching type of an array of types', () => { 478 | expect(request.acceptsCharsets(['iso-8859-15', 'utf-8'])).to.equal('utf-8'); 479 | }); 480 | 481 | it('returns false when types dont match', () => { 482 | expect(request.acceptsCharsets(['iso-8859-15', 'us-ascii'])).to.equal(false); 483 | }); 484 | }); 485 | 486 | describe('.acceptsLanguages()', () => { 487 | it('returns type if the same as Accept-Language header', () => { 488 | const request = mockRequest.createRequest({ headers: { 'Accept-Language': 'en-GB' } }); 489 | expect(request.acceptsLanguages('en-GB')).to.equal('en-GB'); 490 | }); 491 | 492 | it('returns the first matching type of an array of types', () => { 493 | const request = mockRequest.createRequest({ headers: { 'Accept-Language': 'en-GB' } }); 494 | expect(request.acceptsLanguages(['de-DE', 'en-GB'])).to.equal('en-GB'); 495 | }); 496 | 497 | it('returns false when types dont match', () => { 498 | const request = mockRequest.createRequest({ headers: { 'Accept-Language': 'en-GB' } }); 499 | expect(request.acceptsLanguages(['de-DE', 'fr-FR'])).to.equal(false); 500 | }); 501 | }); 502 | 503 | describe('.range()', () => { 504 | it('returns undefined if there is no Range header', () => { 505 | const request = mockRequest.createRequest(); 506 | expect(request.range(10)).to.be.an('undefined'); 507 | }); 508 | 509 | const tests = [ 510 | { 511 | // Unsatisfiable 512 | header: 'bytes=90-100', 513 | size: 10 514 | }, 515 | { 516 | // Malformed 517 | header: 'by90-100', 518 | size: 10 519 | }, 520 | { 521 | // Basic range 522 | header: 'bytes=50-55', 523 | size: 100 524 | }, 525 | { 526 | // Complex, without options 527 | header: 'bytes=50-55,0-10,5-10,56-60', 528 | size: 100 529 | }, 530 | { 531 | // With options 532 | header: 'bytes=50-55,0-10,5-10,56-60', 533 | size: 100, 534 | options: { 535 | combine: true 536 | } 537 | } 538 | ]; 539 | 540 | tests.forEach((t) => { 541 | it(`returns the result of range-parser if there is a Range header of ${t.header} using size: ${t.size}`, () => { 542 | const request = mockRequest.createRequest({ headers: { range: t.header } }); 543 | expect(request.range(t.size, t.options)).to.deep.equal(parseRange(t.size, t.header, t.options)); 544 | }); 545 | }); 546 | }); 547 | 548 | describe('.param()', () => { 549 | it('should return param, when found in params', () => { 550 | const options = { 551 | params: { 552 | key: 'value' 553 | } 554 | }; 555 | 556 | const request = mockRequest.createRequest(options); 557 | expect(request.param('key')).to.equal('value'); 558 | }); 559 | 560 | it('should return falsy param, when found in params', () => { 561 | const options = { 562 | params: { 563 | key: 0 564 | } 565 | }; 566 | 567 | const request = mockRequest.createRequest(options); 568 | expect(request.param('key')).to.equal(0); 569 | }); 570 | 571 | it('should return param, when found in body', () => { 572 | const options = { 573 | body: { 574 | key: 'value' 575 | } 576 | }; 577 | 578 | const request = mockRequest.createRequest(options); 579 | expect(request.param('key')).to.equal('value'); 580 | }); 581 | 582 | it('should return falsy param, when found in body', () => { 583 | const options = { 584 | body: { 585 | key: 0 586 | } 587 | }; 588 | 589 | const request = mockRequest.createRequest(options); 590 | expect(request.param('key')).to.equal(0); 591 | }); 592 | 593 | it('should return param, when found in query', () => { 594 | const options = { 595 | query: { 596 | key: 'value' 597 | } 598 | }; 599 | 600 | const request = mockRequest.createRequest(options); 601 | expect(request.param('key')).to.equal('value'); 602 | }); 603 | 604 | it('should return falsy param, when found in query', () => { 605 | const options = { 606 | query: { 607 | key: 0 608 | } 609 | }; 610 | 611 | const request = mockRequest.createRequest(options); 612 | expect(request.param('key')).to.equal(0); 613 | }); 614 | 615 | it('should not return param, when not found in params/body/query', () => { 616 | const request = mockRequest.createRequest(); 617 | expect(request.get('key')).to.be.a('undefined'); 618 | expect(request.header('key')).to.be.a('undefined'); 619 | expect(request.headers.get('key')).to.be.a('undefined'); 620 | expect(request.headers.key).to.be.a('undefined'); 621 | }); 622 | 623 | it('should return defaultValue, when not found in params/body/query', () => { 624 | const request = mockRequest.createRequest(); 625 | expect(request.get('key')).to.be.a('undefined'); 626 | expect(request.param('key', 'defaultValue')).to.equal('defaultValue'); 627 | }); 628 | 629 | it('should return undefined, when not found in params/body/query', () => { 630 | const request = mockRequest.createRequest(); 631 | expect(request.get('key')).to.be.a('undefined'); 632 | expect(request.param('key')).to.be.an('undefined'); 633 | }); 634 | }); 635 | 636 | describe('helper functions', () => { 637 | describe('._setParameter()', () => { 638 | it('should set param, when called with key and value', () => { 639 | const request = mockRequest.createRequest(); 640 | request._setParameter('key', 'value'); 641 | expect(request.param('key')).to.equal('value'); 642 | }); 643 | 644 | it('should unset param, when called with key and no value', () => { 645 | const request = mockRequest.createRequest(); 646 | request._setParameter('key', 'value'); 647 | request._setParameter('key'); 648 | expect(request.param('key')).to.be.a('undefined'); 649 | }); 650 | }); 651 | 652 | describe('._setSessionVariable()', () => { 653 | it('should set session constiable, when called with key and value', () => { 654 | const request = mockRequest.createRequest(); 655 | request._setSessionVariable('key', 'value'); 656 | expect(request.session.key).to.equal('value'); 657 | }); 658 | 659 | it('should unset session constiable, when called with key and no value', () => { 660 | const request = mockRequest.createRequest(); 661 | request._setSessionVariable('key', 'value'); 662 | request._setSessionVariable('key'); 663 | expect(request.param('key')).to.be.a('undefined'); 664 | }); 665 | }); 666 | 667 | describe('._setCookiesVariable()', () => { 668 | it('should set cookie, when called with key and value', () => { 669 | const request = mockRequest.createRequest(); 670 | request._setCookiesVariable('key', 'value'); 671 | expect(request.cookies.key).to.equal('value'); 672 | }); 673 | 674 | it('should unset cookie, when called with key and no value', () => { 675 | const request = mockRequest.createRequest(); 676 | request._setCookiesVariable('key', 'value'); 677 | request._setCookiesVariable('key'); 678 | expect(request.cookies.key).to.be.a('undefined'); 679 | }); 680 | }); 681 | 682 | describe('._setSignedCookiesVariable()', () => { 683 | it('should set signed cookie, when called with key and value', () => { 684 | const request = mockRequest.createRequest(); 685 | request._setSignedCookiesVariable('key', 'value'); 686 | expect(request.signedCookies.key).to.equal('value'); 687 | }); 688 | 689 | it('should unset signed cookie, when called with key and no value', () => { 690 | const request = mockRequest.createRequest(); 691 | request._setSignedCookiesVariable('key', 'value'); 692 | request._setSignedCookiesVariable('key'); 693 | expect(request.signedCookies.key).to.be.a('undefined'); 694 | }); 695 | }); 696 | 697 | describe('._setHeadersVariable()', () => { 698 | it('should set header, when called with key and value', () => { 699 | const request = mockRequest.createRequest(); 700 | request._setHeadersVariable('Key', 'value'); 701 | 702 | expect(request.get('Key')).to.equal('value'); 703 | expect(request.header('Key')).to.equal('value'); 704 | expect(request.headers.get('Key')).to.equal('value'); 705 | expect(request.getHeader('Key')).to.equal('value'); 706 | expect(request.headers.Key).to.equal('value'); 707 | }); 708 | }); 709 | 710 | describe('._setFilesVariable()', () => { 711 | it('should set file, when called with key and value', () => { 712 | const request = mockRequest.createRequest(); 713 | request._setFilesVariable('key', 'value'); 714 | expect(request.files.key).to.equal('value'); 715 | }); 716 | 717 | it('should unset file, when called with key and no value', () => { 718 | const request = mockRequest.createRequest(); 719 | request._setFilesVariable('key', 'value'); 720 | request._setFilesVariable('key'); 721 | expect(request.files.key).to.be.a('undefined'); 722 | }); 723 | }); 724 | 725 | describe('._setMethod()', () => { 726 | it('should set method, when called with value', () => { 727 | const request = mockRequest.createRequest(); 728 | const value = 'HEAD'; 729 | request._setMethod(value); 730 | expect(request.method).to.equal(value); 731 | }); 732 | 733 | it('should unset method, when called with no arguments', () => { 734 | const request = mockRequest.createRequest(); 735 | request._setMethod(); 736 | expect(request.method).to.be.a('undefined'); 737 | }); 738 | }); 739 | 740 | describe('._setURL()', () => { 741 | it('should set url, when called with value', () => { 742 | const request = mockRequest.createRequest(); 743 | const value = '/path/to/url'; 744 | request._setURL(value); 745 | expect(request.url).to.equal(value); 746 | }); 747 | 748 | it('should unset url, when called with no arguments', () => { 749 | const request = mockRequest.createRequest(); 750 | request._setURL(); 751 | expect(request.url).to.be.a('undefined'); 752 | }); 753 | }); 754 | 755 | describe('._setBaseUrl()', () => { 756 | it('should set baseUrl, when called with value', () => { 757 | const value = '/path'; 758 | const request = mockRequest.createRequest(); 759 | request._setBaseUrl(value); 760 | expect(request.baseUrl).to.equal(value); 761 | }); 762 | 763 | it('should unset baseUrl, when called with no arguments', () => { 764 | const request = mockRequest.createRequest(); 765 | request._setBaseUrl(); 766 | expect(request.baseUrl).to.be.a('undefined'); 767 | }); 768 | }); 769 | 770 | describe('._setOriginalUrl()', () => { 771 | it('should set originalUrl, when called with value', () => { 772 | const value = '/path/to/url'; 773 | const request = mockRequest.createRequest(); 774 | request._setOriginalUrl(value); 775 | expect(request.originalUrl).to.equal(value); 776 | }); 777 | 778 | it('should unset originalUrl, when called with no arguments', () => { 779 | const request = mockRequest.createRequest(); 780 | request._setOriginalUrl(); 781 | expect(request.originalUrl).to.be.a('undefined'); 782 | }); 783 | }); 784 | 785 | describe('._setBody()', () => { 786 | it('should set body, when called with value', () => { 787 | const value = { 788 | key1: 'value1', 789 | key2: 'value2' 790 | }; 791 | const request = mockRequest.createRequest(); 792 | request._setBody(value); 793 | expect(request.body).to.deep.equal(value); 794 | }); 795 | 796 | it('should unset body, when called with no arguments', () => { 797 | const request = mockRequest.createRequest(); 798 | request._setBody(); 799 | expect(request.body).to.be.a('undefined'); 800 | }); 801 | }); 802 | 803 | describe('._addBody()', () => { 804 | it('should add body constiable, when called with key and value', () => { 805 | const request = mockRequest.createRequest(); 806 | request._addBody('key', 'value'); 807 | expect(request.body.key).to.equal('value'); 808 | }); 809 | 810 | it('should unset body constiable, when called with key and no value', () => { 811 | const request = mockRequest.createRequest(); 812 | request._addBody('key', 'value'); 813 | request._addBody('key'); 814 | expect(request.body.key).to.be.a('undefined'); 815 | }); 816 | }); 817 | 818 | describe('.send()', () => { 819 | it('should trigger data and end event when string is given', (done) => { 820 | const data = [] as string[]; 821 | const request = mockRequest.createRequest(); 822 | request.on('data', (chunk: any) => { 823 | data.push(chunk); 824 | }); 825 | request.on('end', () => { 826 | const result = data.join(''); 827 | expect(result).to.equal('test data'); 828 | done(); 829 | }); 830 | request.send('test data'); 831 | }); 832 | 833 | it('should trigger data and end event when object is given', (done) => { 834 | const data = [] as string[]; 835 | const dataTosend = { key: 'value' }; 836 | const request = mockRequest.createRequest(); 837 | request.on('data', (chunk: any) => { 838 | data.push(chunk); 839 | }); 840 | request.on('end', () => { 841 | const result = data.join(''); 842 | expect(JSON.parse(result)).to.deep.equal(dataTosend); 843 | done(); 844 | }); 845 | request.send(dataTosend); 846 | }); 847 | 848 | it('should trigger data and end event when number is given', (done) => { 849 | const data = [] as string[]; 850 | const request = mockRequest.createRequest(); 851 | request.on('data', (chunk: any) => { 852 | data.push(chunk); 853 | }); 854 | request.on('end', () => { 855 | const result = data.join(''); 856 | expect(result).to.equal('35'); 857 | done(); 858 | }); 859 | request.send(35); 860 | }); 861 | 862 | it('should trigger data and end event when buffer is given', (done) => { 863 | const data = [] as string[]; 864 | const bufferdata = Buffer.from('test data'); 865 | const request = mockRequest.createRequest(); 866 | request.on('data', (chunk: any) => { 867 | data.push(chunk); 868 | }); 869 | request.on('end', () => { 870 | const result = data.join(''); 871 | expect(result).to.equal('test data'); 872 | done(); 873 | }); 874 | request.send(bufferdata); 875 | }); 876 | 877 | it('should trigger data and end event when nothing is given', (done) => { 878 | const data = [] as string[]; 879 | const request = mockRequest.createRequest(); 880 | request.on('data', (chunk: any) => { 881 | data.push(chunk); 882 | }); 883 | request.on('end', () => { 884 | const result = data.join(''); 885 | expect(result).to.equal(''); 886 | done(); 887 | }); 888 | request.send(); 889 | }); 890 | }); 891 | 892 | describe('.hostname', () => { 893 | it("should return the host's main domain", () => { 894 | const options = { 895 | headers: { 896 | host: 'tobi.ferrets.example.com:5000' 897 | } 898 | }; 899 | let request: mockRequest.MockRequest; 900 | request = mockRequest.createRequest(options); 901 | expect(request.hostname).to.equal('tobi.ferrets.example.com'); 902 | 903 | options.headers.host = 'tobi.ferrets.example.com'; 904 | request = mockRequest.createRequest(options); 905 | expect(request.hostname).to.equal('tobi.ferrets.example.com'); 906 | 907 | options.headers.host = 'example.com'; 908 | request = mockRequest.createRequest(options); 909 | expect(request.hostname).to.equal('example.com'); 910 | 911 | options.headers.host = 'example.com:8443'; 912 | request = mockRequest.createRequest(options); 913 | expect(request.hostname).to.equal('example.com'); 914 | 915 | options.headers.host = 'localhost:3000'; 916 | request = mockRequest.createRequest(options); 917 | expect(request.hostname).to.equal('localhost'); 918 | }); 919 | 920 | it('should return an empty string', () => { 921 | const options = { 922 | headers: { 923 | key1: 'key1' 924 | } 925 | }; 926 | const request = mockRequest.createRequest(options); 927 | expect(request.hostname).to.equal(''); 928 | }); 929 | 930 | it('should return an predefined hostname', () => { 931 | const options = { 932 | hostname: 'predefined.host.com', 933 | headers: { 934 | host: 'other.host' 935 | } 936 | }; 937 | const request = mockRequest.createRequest(options); 938 | expect(request.hostname).to.equal('predefined.host.com'); 939 | }); 940 | }); 941 | 942 | describe('.subdomains', () => { 943 | it('should returns the host subdomains', () => { 944 | const options = { 945 | headers: { 946 | host: 'tobi.ferrets.example.com' 947 | } 948 | }; 949 | const request = mockRequest.createRequest(options); 950 | expect(request.subdomains).to.be.an('array').that.includes('ferrets'); 951 | expect(request.subdomains).to.be.an('array').that.includes('tobi'); 952 | }); 953 | 954 | it('should returns and empty array', () => { 955 | const options: mockRequest.RequestOptions = { 956 | headers: { 957 | key1: 'key1' 958 | } 959 | }; 960 | 961 | let request: mockRequest.MockRequest; 962 | request = mockRequest.createRequest(options); 963 | expect(request.subdomains).to.be.an('array').to.have.lengthOf(0); 964 | 965 | options.headers!.host = 'example.com'; 966 | request = mockRequest.createRequest(options); 967 | expect(request.subdomains).to.be.an('array').to.have.lengthOf(0); 968 | }); 969 | }); 970 | }); 971 | 972 | describe('asyncIterator', () => { 973 | async function collect(asyncIterable: any) { 974 | const chunks = [] as string[]; 975 | for await (const chunk of asyncIterable) { 976 | chunks.push(chunk); 977 | } 978 | return chunks; 979 | } 980 | 981 | it('should iterate when sending data', async () => { 982 | const request = mockRequest.createRequest(); 983 | 984 | const chunksPromise = collect(request); 985 | request.send('test data'); 986 | 987 | const data = (await chunksPromise).join(''); 988 | expect(data).to.equal('test data'); 989 | }); 990 | 991 | it('should iterate synchronous pushes', async () => { 992 | const request = mockRequest.createRequest(); 993 | 994 | const chunksPromise = collect(request); 995 | request.emit('data', Buffer.from('foo')); 996 | request.emit('data', Buffer.from('bar')); 997 | request.emit('data', Buffer.from('baz')); 998 | request.emit('end'); 999 | 1000 | const data = (await chunksPromise).join(''); 1001 | expect(data).to.equal('foobarbaz'); 1002 | }); 1003 | 1004 | it('should ignore push after end', async () => { 1005 | const request = mockRequest.createRequest(); 1006 | 1007 | const chunksPromise = collect(request); 1008 | request.emit('data', Buffer.from('foo')); 1009 | request.emit('end'); 1010 | request.emit('data', Buffer.from('bar')); 1011 | 1012 | const data = (await chunksPromise).join(''); 1013 | expect(data).to.equal('foo'); 1014 | }); 1015 | 1016 | it('should iterate asynchronous pushes', async () => { 1017 | const request = mockRequest.createRequest(); 1018 | 1019 | const chunksPromise = collect(request); 1020 | request.emit('data', Buffer.from('foo')); 1021 | await new Promise((r) => { 1022 | setTimeout(r); 1023 | }); 1024 | request.emit('data', Buffer.from('bar')); 1025 | await new Promise((r) => { 1026 | setTimeout(r); 1027 | }); 1028 | request.emit('data', Buffer.from('baz')); 1029 | await new Promise((r) => { 1030 | setTimeout(r); 1031 | }); 1032 | request.emit('end'); 1033 | 1034 | const data = (await chunksPromise).join(''); 1035 | expect(data).to.equal('foobarbaz'); 1036 | }); 1037 | 1038 | it('should support asynchronous pushes while iterating', async () => { 1039 | const request = mockRequest.createRequest(); 1040 | 1041 | const chunksPromise = (async () => { 1042 | const extraPushes = ['3', '2', '1']; 1043 | const chunks = [] as string[]; 1044 | for await (const chunk of request) { 1045 | chunks.push(chunk); 1046 | if (extraPushes.length > 0) { 1047 | const toCreate = extraPushes.pop()!; 1048 | request.emit('data', Buffer.from(toCreate)); 1049 | await new Promise((r) => { 1050 | setTimeout(r); 1051 | }); 1052 | } 1053 | } 1054 | return chunks; 1055 | })(); 1056 | 1057 | request.emit('data', Buffer.from('foo')); 1058 | await new Promise((r) => { 1059 | setTimeout(r); 1060 | }); 1061 | request.emit('data', Buffer.from('bar')); 1062 | await new Promise((r) => { 1063 | setTimeout(r); 1064 | }); 1065 | request.emit('data', Buffer.from('baz')); 1066 | await new Promise((r) => { 1067 | setTimeout(r); 1068 | }); 1069 | request.emit('end'); 1070 | 1071 | const data = (await chunksPromise).join(''); 1072 | expect(data).to.equal('foo1bar2baz3'); 1073 | }); 1074 | 1075 | it('supports error', async () => { 1076 | const request = mockRequest.createRequest(); 1077 | 1078 | /** @type {AsyncIterator} */ 1079 | const iterator = request[Symbol.asyncIterator](); 1080 | const error = new Error('Test error'); 1081 | 1082 | const nextPromise = iterator.next(); 1083 | request.emit('error', error); 1084 | 1085 | try { 1086 | await nextPromise; 1087 | expect.fail(); 1088 | } catch (e) { 1089 | expect(e).to.equal(error); 1090 | } 1091 | }); 1092 | 1093 | it('supports throw', async () => { 1094 | const request = mockRequest.createRequest(); 1095 | 1096 | /** @type {AsyncIterator} */ 1097 | const iterator: AsyncIterator = request[Symbol.asyncIterator](); 1098 | const error = new Error('Test error'); 1099 | 1100 | const nextPromise = iterator.next(); 1101 | request.emit('data', Buffer.from('foo')); 1102 | await nextPromise; 1103 | 1104 | try { 1105 | await iterator.throw!(error); 1106 | expect.fail(); 1107 | } catch (e) { 1108 | expect(e).to.equal(error); 1109 | } 1110 | }); 1111 | 1112 | it('first error wins', async () => { 1113 | const request = mockRequest.createRequest(); 1114 | 1115 | /** @type {AsyncIterator} */ 1116 | const iterator: AsyncIterator = request[Symbol.asyncIterator](); 1117 | const error1 = new Error('Test error 1'); 1118 | const error2 = new Error('Test error 2'); 1119 | 1120 | const nextPromise = iterator.next(); 1121 | request.emit('error', error1); 1122 | request.emit('error', error2); 1123 | 1124 | try { 1125 | await nextPromise; 1126 | expect.fail(); 1127 | } catch (e) { 1128 | expect(e).to.equal(error1); 1129 | } 1130 | }); 1131 | 1132 | it('supports return', async () => { 1133 | const request = mockRequest.createRequest(); 1134 | 1135 | /** @type {AsyncIterator} */ 1136 | const iterator: AsyncIterator = request[Symbol.asyncIterator](); 1137 | 1138 | const result = await iterator.return!(); 1139 | expect(result.done).to.equal(true); 1140 | }); 1141 | 1142 | ['close', 'error'].forEach((event) => { 1143 | it(`discards buffer on ${event}`, async () => { 1144 | const request = mockRequest.createRequest(); 1145 | 1146 | const chunksPromise = (async () => { 1147 | const chunks = [] as string[]; 1148 | try { 1149 | for await (const data of request) { 1150 | chunks.push(data); 1151 | } 1152 | } catch (e) { 1153 | // Ignore 1154 | } 1155 | return chunks; 1156 | })(); 1157 | 1158 | request.emit('data', Buffer.from('foo')); 1159 | await new Promise((r) => { 1160 | setTimeout(r); 1161 | }); 1162 | request.emit('data', Buffer.from('bar')); 1163 | request.emit(event, event === 'error' ? new Error('Test error') : undefined); 1164 | request.emit('data', Buffer.from('baz')); 1165 | 1166 | const data = (await chunksPromise).join(''); 1167 | expect(data).to.equal('foo'); 1168 | }); 1169 | }); 1170 | 1171 | it('emits custom event after creation', async () => { 1172 | const request = mockRequest.createRequest(); 1173 | 1174 | request.on('async_iterator', () => { 1175 | request.emit('data', Buffer.from('foo')); 1176 | request.emit('data', Buffer.from('bar')); 1177 | request.emit('data', Buffer.from('baz')); 1178 | request.emit('end'); 1179 | }); 1180 | 1181 | const data = (await collect(request)).join(''); 1182 | expect(data).to.equal('foobarbaz'); 1183 | }); 1184 | 1185 | if (typeof global.Request === 'function') { 1186 | it('can be fed to a Fetch API Request body', async () => { 1187 | // TODO: what is the purpose of this test? 1188 | const request = mockRequest.createRequest(); 1189 | 1190 | const webRequest = new Request('http://example.com', { 1191 | method: 'POST', 1192 | headers: { 1193 | 'Content-Type': 'application/json' 1194 | }, 1195 | // @ts-ignore:next-line 1196 | body: request, 1197 | // @ts-ignore:next-line 1198 | duplex: 'half' 1199 | }); 1200 | 1201 | request.on('async_iterator', () => { 1202 | request.emit('data', Buffer.from('{ "foo": "b')); 1203 | request.emit('data', Buffer.from('ar" }')); 1204 | request.emit('end'); 1205 | }); 1206 | 1207 | const webRequestJson = await webRequest.json(); 1208 | expect(webRequestJson).to.deep.equal({ foo: 'bar' }); 1209 | }); 1210 | } 1211 | }); 1212 | }); 1213 | -------------------------------------------------------------------------------- /test/lib/mockWritableStream.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | 3 | const { expect } = chai; 4 | 5 | const MockWritableStream = require('../../lib/mockWritableStream'); 6 | 7 | let mockWritableStream; 8 | 9 | describe('mockWritableStream', () => { 10 | before(() => { 11 | mockWritableStream = new MockWritableStream(); 12 | }); 13 | 14 | it('should be a function', () => { 15 | expect(MockWritableStream).to.be.a('function'); 16 | }); 17 | 18 | it('should be an object factory', () => { 19 | expect(mockWritableStream).to.be.a('object'); 20 | expect(mockWritableStream).to.be.an.instanceof(MockWritableStream); 21 | }); 22 | 23 | it('should expose "MockWritableStream" prototype', () => { 24 | expect(mockWritableStream).to.have.property('end'); 25 | expect(mockWritableStream.end).to.be.a('function'); 26 | 27 | expect(mockWritableStream).to.have.property('destroy'); 28 | expect(mockWritableStream.destroy).to.be.a('function'); 29 | 30 | expect(mockWritableStream).to.have.property('destroySoon'); 31 | expect(mockWritableStream.destroySoon).to.be.a('function'); 32 | }); 33 | 34 | it('should return undefined when methods called', () => { 35 | expect(mockWritableStream.end()).to.be.an('undefined'); 36 | expect(mockWritableStream.destroy()).to.be.an('undefined'); 37 | expect(mockWritableStream.destroySoon()).to.be.an('undefined'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/lib/node-http-mock.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const httpMock = require('../../lib/http-mock'); 3 | 4 | const { expect } = chai; 5 | 6 | describe('http-mock', () => { 7 | it('should export .createRequest()', () => { 8 | expect(httpMock.createRequest).to.be.a('function'); 9 | }); 10 | 11 | it('should export .createResponse()', () => { 12 | expect(httpMock.createResponse).to.be.a('function'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | "noEmit": true /* Disable emitting files from a compilation. */, 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 83 | 84 | /* Type Checking */ 85 | "strict": true /* Enable all strict type-checking options. */, 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": ["lib/**/*"] 110 | } 111 | --------------------------------------------------------------------------------