diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..cdb36c1b466 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[{*.js,*.json,*.yml}] +indent_size = 2 +indent_style = space diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000000..62562b74a3b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +coverage +node_modules diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000000..587169c5330 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,8 @@ +root: true + +rules: + eol-last: error + eqeqeq: [error, allow-null] + indent: [error, 2, { MemberExpression: "off", SwitchCase: 1 }] + no-trailing-spaces: error + no-unused-vars: [error, { vars: all, args: none, ignoreRestSiblings: true }] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..d6e1b168ec1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,161 @@ +name: ci + +on: +- pull_request +- push + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + name: + - Node.js 0.10 + - Node.js 0.12 + - io.js 1.x + - io.js 2.x + - io.js 3.x + - Node.js 4.x + - Node.js 5.x + - Node.js 6.x + - Node.js 7.x + - Node.js 8.x + - Node.js 9.x + - Node.js 10.x + - Node.js 11.x + - Node.js 12.x + - Node.js 13.x + - Node.js 14.x + + include: + - name: Node.js 0.10 + node-version: "0.10" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: Node.js 0.12 + node-version: "0.12" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 1.x + node-version: "1.8" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 2.x + node-version: "2.5" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 3.x + node-version: "3.3" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: Node.js 4.x + node-version: "4.9" + npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + + - name: Node.js 5.x + node-version: "5.12" + npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + + - name: Node.js 6.x + node-version: "6.17" + npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 + + - name: Node.js 7.x + node-version: "7.10" + npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 + + - name: Node.js 8.x + node-version: "8.17" + npm-i: mocha@7.2.0 + + - name: Node.js 9.x + node-version: "9.11" + npm-i: mocha@7.2.0 + + - name: Node.js 10.x + node-version: "10.24" + npm-i: mocha@8.4.0 + + - name: Node.js 11.x + node-version: "11.15" + npm-i: mocha@8.4.0 + + - name: Node.js 12.x + node-version: "12.22" + + - name: Node.js 13.x + node-version: "13.14" + + - name: Node.js 14.x + node-version: "14.19" + + steps: + - uses: actions/checkout@v2 + + - name: Install Node.js ${{ matrix.node-version }} + shell: bash -eo pipefail -l {0} + run: | + nvm install --default ${{ matrix.node-version }} + dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" + + - name: Configure npm + run: | + npm config set loglevel error + npm config set shrinkwrap false + + - name: Install npm module(s) ${{ matrix.npm-i }} + run: npm install --save-dev ${{ matrix.npm-i }} + if: matrix.npm-i != '' + + - name: Remove non-test dependencies + run: npm rm --silent --save-dev connect-redis + + - name: Setup Node.js version-specific dependencies + shell: bash + run: | + # eslint for linting + # - remove on Node.js < 10 + if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then + node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ + grep -E '^eslint(-|$)' | \ + sort -r | \ + xargs -n1 npm rm --silent --save-dev + fi + + - name: Install Node.js dependencies + run: npm install + + - name: List environment + id: list_env + shell: bash + run: | + echo "node@$(node -v)" + echo "npm@$(npm -v)" + npm -s ls ||: + (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }' + + - name: Run tests + shell: bash + run: npm run test-ci + + - name: Lint code + if: steps.list_env.outputs.eslint != '' + run: npm run lint + + - name: Collect code coverage + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + flag-name: run-${{ matrix.test_number }} + parallel: true + + coverage: + needs: test + runs-on: ubuntu-latest + steps: + - name: Upload code coverage + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/.gitignore b/.gitignore index 9723e60591d..3a673d9cc09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,15 @@ -# OS X -.DS_Store* -Icon? -._* - -# Windows -Thumbs.db -ehthumbs.db -Desktop.ini - -# Linux -.directory -*~ - - # npm node_modules +package-lock.json *.log *.gz - # Coveralls +.nyc_output coverage # Benchmarking benchmarks/graphs + +# ignore additional files using core.excludesFile +# https://git-scm.com/docs/gitignore diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1230c7e2f95..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -language: node_js -node_js: - - "0.10" - - "0.12" - - "1.8" - - "2.5" - - "3.3" - - "4.8" - - "5.12" - - "6.10" - - "7.6" -matrix: - include: - - node_js: "8.0" - env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" - allow_failures: - # Allow the nightly installs to fail - - env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" -sudo: false -cache: - directories: - - node_modules -before_install: - # Remove all non-test dependencies - - "npm rm --save-dev connect-redis" - - # Update Node.js modules - - "test ! -d node_modules || npm prune" - - "test ! -d node_modules || npm rebuild" -script: "npm run-script test-ci" -after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls" diff --git a/Charter.md b/Charter.md new file mode 100644 index 00000000000..f9647cb734d --- /dev/null +++ b/Charter.md @@ -0,0 +1,92 @@ +# Express Charter + +## Section 0: Guiding Principles + +The Express project is part of the OpenJS Foundation which operates +transparently, openly, collaboratively, and ethically. +Project proposals, timelines, and status must not merely be open, but +also easily visible to outsiders. + +## Section 1: Scope + +Express is a http web server framework with a simple and expressive API +which is highly aligned with Node.js core. We aim to be the best in +class for writing performant, spec compliant, and powerful web servers +in Node.js. As one of the oldest and most popular web frameworks in +the ecosystem, we have an important place for new users and experts +alike. + +### 1.1: In-scope + +Express is made of many modules spread between three GitHub Orgs: + +- [expressjs](http://github.com/expressjs/): Top level middleware and + libraries +- [pillarjs](http://github.com/pillarjs/): Components which make up + Express but can also be used for other web frameworks +- [jshttp](http://github.com/jshttp/): Low level http libraries + +### 1.2: Out-of-Scope + +Section Intentionally Left Blank + +## Section 2: Relationship with OpenJS Foundation CPC. + +Technical leadership for the projects within the OpenJS Foundation is +delegated to the projects through their project charters by the OpenJS +Cross Project Council (CPC). In the case of the Express project, it is +delegated to the Express Technical Committee ("TC"). + +This Technical Committee is in charge of both the day-to-day operations +of the project, as well as its technical management. This charter can +be amended by the TC requiring at least two approvals and a minimum two +week comment period for other TC members or CPC members to object. Any +changes the CPC wishes to propose will be considered a priority but +will follow the same process. + +### 2.1 Other Formal Project Relationships + +Section Intentionally Left Blank + +## Section 3: Express Governing Body + +The Express project is managed by the Technical Committee ("TC"). +Members can be added to the TC at any time. Any committer can nominate +another committer to the TC and the TC uses its standard consensus +seeking process to evaluate whether or not to add this new member. +Members who do not participate consistently at the level of a majority +of the other members are expected to resign. + +## Section 4: Roles & Responsibilities + +The Express TC manages all aspects of both the technical and community +parts of the project. Members of the TC should attend the regular +meetings when possible, and be available for discussion of time +sensitive or important issues. + +### Section 4.1 Project Operations & Management + +Section Intentionally Left Blank + +### Section 4.2: Decision-making, Voting, and/or Elections + +The Express TC uses a "consensus seeking" process for issues that are +escalated to the TC. The group tries to find a resolution that has no +open objections among TC members. If a consensus cannot be reached +that has no objections then a majority wins vote is called. It is also +expected that the majority of decisions made by the TC are via a +consensus seeking process and that voting is only used as a last-resort. + +Resolution may involve returning the issue to committers with +suggestions on how to move forward towards a consensus. It is not +expected that a meeting of the TC will resolve all issues on its +agenda during that meeting and may prefer to continue the discussion +happening among the committers. + +### Section 4.3: Other Project Roles + +Section Intentionally Left Blank + +## Section 5: Definitions + +Section Intentionally Left Blank diff --git a/Code-Of-Conduct.md b/Code-Of-Conduct.md new file mode 100644 index 00000000000..bbb8996a659 --- /dev/null +++ b/Code-Of-Conduct.md @@ -0,0 +1,139 @@ +# Contributor Covenant Code of Conduct + +As a member of the Open JS Foundation, Express has adopted the +[Contributor Covenant 2.0][cc-20-doc]. + +If an issue arises and you cannot resolve it directly with the parties +involved, you can report it to the Express project TC through the following +email: express-coc@lists.openjsf.org + +In addition, the OpenJS Foundation maintains a Code of Conduct Panel (CoCP). +This is a foundation-wide team established to manage escalation when a reporter +believes that a report to a member project or the CPC has not been properly +handled. In order to escalate to the CoCP send an email to +coc-escalation@lists.openjsf.org. + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances + of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders 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, and will communicate reasons for +moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail +address, posting via an official social media account, or acting as an +appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +express-coc@lists.openjsf.org. All complaints will be reviewed and +investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited +interaction with those enforcing the Code of Conduct, is allowed during this +period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +project community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant, version 2.0](cc-20-doc). + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +[cc-20-doc]: https://www.contributor-covenant.org/version/2/0/code_of_conduct/ + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Collaborator-Guide.md b/Collaborator-Guide.md index 7c0d265dd0a..3c73307d61b 100644 --- a/Collaborator-Guide.md +++ b/Collaborator-Guide.md @@ -1,3 +1,4 @@ +# Express Collaborator Guide ## Website Issues @@ -6,7 +7,7 @@ Open issues for the expressjs.com website in https://github.com/expressjs/expres ## PRs and Code contributions * Tests must pass. -* Follow the [JavaScript Standard Style](http://standardjs.com/). +* Follow the [JavaScript Standard Style](http://standardjs.com/) and `npm run lint`. * If you fix a bug, add a test. ## Branches @@ -21,13 +22,15 @@ a future release of Express. 1. [Create an issue](https://github.com/expressjs/express/issues/new) for the bug you want to fix or the feature that you want to add. -2. Create your own [fork](https://github.com/expressjs/express) on github, then +2. Create your own [fork](https://github.com/expressjs/express) on GitHub, then checkout your fork. 3. Write your code in your local copy. It's good practice to create a branch for each new issue you work on, although not compulsory. 4. To run the test suite, first install the dependencies by running `npm install`, then run `npm test`. -5. If the tests pass, you can commit your changes to your fork and then create +5. Ensure your code is linted by running `npm run lint` -- fix any issue you + see listed. +6. If the tests pass, you can commit your changes to your fork and then create a pull request from there. Make sure to reference your issue from the pull request comments by including the issue number e.g. `#123`. diff --git a/Contributing.md b/Contributing.md index 214e9070126..485dee597e1 100644 --- a/Contributing.md +++ b/Contributing.md @@ -12,14 +12,15 @@ contributors can be involved in decision making. * A **Contributor** is any individual creating or commenting on an issue or pull request. * A **Committer** is a subset of contributors who have been given write access to the repository. -* A **TC (Technical Committee)** is a group of committers representing the required technical +* A **TC (Technical Committee)** is a group of committers representing the required technical expertise to resolve rare disputes. +* A **Triager** is a subset of contributors who have been given triage access to the repository. -# Logging Issues +## Logging Issues Log an issue for any question or problem you might have. When in doubt, log an issue, and any additional policies about what to include will be provided in the responses. The only -exception is security dislosures which should be sent privately. +exception is security disclosures which should be sent privately. Committers may direct you to another repository, ask for additional clarifications, and add appropriate metadata before the issue is addressed. @@ -27,7 +28,7 @@ add appropriate metadata before the issue is addressed. Please be courteous and respectful. Every participant is expected to follow the project's Code of Conduct. -# Contributions +## Contributions Any change to resources in this repository must be through pull requests. This applies to all changes to documentation, code, binary files, etc. Even long term committers and TC members must use @@ -36,27 +37,56 @@ pull requests. No pull request can be merged without being reviewed. For non-trivial contributions, pull requests should sit for at least 36 hours to ensure that -contributors in other timezones have time to review. Consideration should also be given to -weekends and other holiday periods to ensure active committers all have reasonable time to +contributors in other timezones have time to review. Consideration should also be given to +weekends and other holiday periods to ensure active committers all have reasonable time to become involved in the discussion and review process if they wish. The default for each contribution is that it is accepted once no committer has an objection. -During review committers may also request that a specific contributor who is most versed in a -particular area gives a "LGTM" before the PR can be merged. There is no additional "sign off" -process for contributions to land. Once all issues brought by committers are addressed it can +During a review, committers may also request that a specific contributor who is most versed in a +particular area gives a "LGTM" before the PR can be merged. There is no additional "sign off" +process for contributions to land. Once all issues brought by committers are addressed it can be landed by any committer. -In the case of an objection being raised in a pull request by another committer, all involved -committers should seek to arrive at a consensus by way of addressing concerns being expressed +In the case of an objection being raised in a pull request by another committer, all involved +committers should seek to arrive at a consensus by way of addressing concerns being expressed by discussion, compromise on the proposed change, or withdrawal of the proposed change. If a contribution is controversial and committers cannot agree about how to get it to land or if it should land then it should be escalated to the TC. TC members should regularly -discuss pending contributions in order to find a resolution. It is expected that only a -small minority of issues be brought to the TC for resolution and that discussion and +discuss pending contributions in order to find a resolution. It is expected that only a +small minority of issues be brought to the TC for resolution and that discussion and compromise among committers be the default resolution mechanism. -# Becoming a Committer +## Becoming a Triager + +Anyone can become a triager! Read more about the process of being a triager in +[the triage process document](Triager-Guide.md). + +[Open an issue in `expressjs/express` repo](https://github.com/expressjs/express/issues/new) +to request the triage role. State that you have read and agree to the +[Code of Conduct](Code-Of-Conduct.md) and details of the role. + +Here is an example issue content you can copy and paste: + +``` +Title: Request triager role for + +I have read and understood the project's Code of Conduct. +I also have read and understood the process and best practices around Express triaging. + +I request for a triager role for the following GitHub organizations: + +jshttp +pillarjs +express +``` + +Once you have opened your issue, a member of the TC will add you to the `triage` team in +the organizations requested. They will then close the issue. + +Happy triaging! + +## Becoming a Committer All contributors who land a non-trivial contribution should be on-boarded in a timely manner, and added as a committer, and be given write access to the repository. @@ -64,22 +94,20 @@ and added as a committer, and be given write access to the repository. Committers are expected to follow this policy and continue to send pull requests, go through proper review, and have other committers merge their pull requests. -# TC Process +## TC Process -The TC uses a "consensus seeking" process for issues that are escalated to the TC. +The TC uses a "consensus seeking" process for issues that are escalated to the TC. The group tries to find a resolution that has no open objections among TC members. If a consensus cannot be reached that has no objections then a majority wins vote -is called. It is also expected that the majority of decisions made by the TC are via +is called. It is also expected that the majority of decisions made by the TC are via a consensus seeking process and that voting is only used as a last-resort. -Resolution may involve returning the issue to committers with suggestions on how to -move forward towards a consensus. It is not expected that a meeting of the TC +Resolution may involve returning the issue to committers with suggestions on how to +move forward towards a consensus. It is not expected that a meeting of the TC will resolve all issues on its agenda during that meeting and may prefer to continue the discussion happening among the committers. Members can be added to the TC at any time. Any committer can nominate another committer to the TC and the TC uses its standard consensus seeking process to evaluate whether or -not to add this new member. Members who do not participate consistently at the level of +not to add this new member. Members who do not participate consistently at the level of a majority of the other members are expected to resign. - - diff --git a/History.md b/History.md index 5bfd5690a53..9f3f876512d 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,280 @@ +4.17.3 / 2022-02-16 +=================== + + * deps: accepts@~1.3.8 + - deps: mime-types@~2.1.34 + - deps: negotiator@0.6.3 + * deps: body-parser@1.19.2 + - deps: bytes@3.1.2 + - deps: qs@6.9.7 + - deps: raw-body@2.4.3 + * deps: cookie@0.4.2 + * deps: qs@6.9.7 + * Fix handling of `__proto__` keys + * pref: remove unnecessary regexp for trust proxy + +4.17.2 / 2021-12-16 +=================== + + * Fix handling of `undefined` in `res.jsonp` + * Fix handling of `undefined` when `"json escape"` is enabled + * Fix incorrect middleware execution with unanchored `RegExp`s + * Fix `res.jsonp(obj, status)` deprecation message + * Fix typo in `res.is` JSDoc + * deps: body-parser@1.19.1 + - deps: bytes@3.1.1 + - deps: http-errors@1.8.1 + - deps: qs@6.9.6 + - deps: raw-body@2.4.2 + - deps: safe-buffer@5.2.1 + - deps: type-is@~1.6.18 + * deps: content-disposition@0.5.4 + - deps: safe-buffer@5.2.1 + * deps: cookie@0.4.1 + - Fix `maxAge` option to reject invalid values + * deps: proxy-addr@~2.0.7 + - Use `req.socket` over deprecated `req.connection` + - deps: forwarded@0.2.0 + - deps: ipaddr.js@1.9.1 + * deps: qs@6.9.6 + * deps: safe-buffer@5.2.1 + * deps: send@0.17.2 + - deps: http-errors@1.8.1 + - deps: ms@2.1.3 + - pref: ignore empty http tokens + * deps: serve-static@1.14.2 + - deps: send@0.17.2 + * deps: setprototypeof@1.2.0 + +4.17.1 / 2019-05-25 +=================== + + * Revert "Improve error message for `null`/`undefined` to `res.status`" + +4.17.0 / 2019-05-16 +=================== + + * Add `express.raw` to parse bodies into `Buffer` + * Add `express.text` to parse bodies into string + * Improve error message for non-strings to `res.sendFile` + * Improve error message for `null`/`undefined` to `res.status` + * Support multiple hosts in `X-Forwarded-Host` + * deps: accepts@~1.3.7 + * deps: body-parser@1.19.0 + - Add encoding MIK + - Add petabyte (`pb`) support + - Fix parsing array brackets after index + - deps: bytes@3.1.0 + - deps: http-errors@1.7.2 + - deps: iconv-lite@0.4.24 + - deps: qs@6.7.0 + - deps: raw-body@2.4.0 + - deps: type-is@~1.6.17 + * deps: content-disposition@0.5.3 + * deps: cookie@0.4.0 + - Add `SameSite=None` support + * deps: finalhandler@~1.1.2 + - Set stricter `Content-Security-Policy` header + - deps: parseurl@~1.3.3 + - deps: statuses@~1.5.0 + * deps: parseurl@~1.3.3 + * deps: proxy-addr@~2.0.5 + - deps: ipaddr.js@1.9.0 + * deps: qs@6.7.0 + - Fix parsing array brackets after index + * deps: range-parser@~1.2.1 + * deps: send@0.17.1 + - Set stricter CSP header in redirect & error responses + - deps: http-errors@~1.7.2 + - deps: mime@1.6.0 + - deps: ms@2.1.1 + - deps: range-parser@~1.2.1 + - deps: statuses@~1.5.0 + - perf: remove redundant `path.normalize` call + * deps: serve-static@1.14.1 + - Set stricter CSP header in redirect response + - deps: parseurl@~1.3.3 + - deps: send@0.17.1 + * deps: setprototypeof@1.1.1 + * deps: statuses@~1.5.0 + - Add `103 Early Hints` + * deps: type-is@~1.6.18 + - deps: mime-types@~2.1.24 + - perf: prevent internal `throw` on invalid type + +4.16.4 / 2018-10-10 +=================== + + * Fix issue where `"Request aborted"` may be logged in `res.sendfile` + * Fix JSDoc for `Router` constructor + * deps: body-parser@1.18.3 + - Fix deprecation warnings on Node.js 10+ + - Fix stack trace for strict json parse error + - deps: depd@~1.1.2 + - deps: http-errors@~1.6.3 + - deps: iconv-lite@0.4.23 + - deps: qs@6.5.2 + - deps: raw-body@2.3.3 + - deps: type-is@~1.6.16 + * deps: proxy-addr@~2.0.4 + - deps: ipaddr.js@1.8.0 + * deps: qs@6.5.2 + * deps: safe-buffer@5.1.2 + +4.16.3 / 2018-03-12 +=================== + + * deps: accepts@~1.3.5 + - deps: mime-types@~2.1.18 + * deps: depd@~1.1.2 + - perf: remove argument reassignment + * deps: encodeurl@~1.0.2 + - Fix encoding `%` as last character + * deps: finalhandler@1.1.1 + - Fix 404 output for bad / missing pathnames + - deps: encodeurl@~1.0.2 + - deps: statuses@~1.4.0 + * deps: proxy-addr@~2.0.3 + - deps: ipaddr.js@1.6.0 + * deps: send@0.16.2 + - Fix incorrect end tag in default error & redirects + - deps: depd@~1.1.2 + - deps: encodeurl@~1.0.2 + - deps: statuses@~1.4.0 + * deps: serve-static@1.13.2 + - Fix incorrect end tag in redirects + - deps: encodeurl@~1.0.2 + - deps: send@0.16.2 + * deps: statuses@~1.4.0 + * deps: type-is@~1.6.16 + - deps: mime-types@~2.1.18 + +4.16.2 / 2017-10-09 +=================== + + * Fix `TypeError` in `res.send` when given `Buffer` and `ETag` header set + * perf: skip parsing of entire `X-Forwarded-Proto` header + +4.16.1 / 2017-09-29 +=================== + + * deps: send@0.16.1 + * deps: serve-static@1.13.1 + - Fix regression when `root` is incorrectly set to a file + - deps: send@0.16.1 + +4.16.0 / 2017-09-28 +=================== + + * Add `"json escape"` setting for `res.json` and `res.jsonp` + * Add `express.json` and `express.urlencoded` to parse bodies + * Add `options` argument to `res.download` + * Improve error message when autoloading invalid view engine + * Improve error messages when non-function provided as middleware + * Skip `Buffer` encoding when not generating ETag for small response + * Use `safe-buffer` for improved Buffer API + * deps: accepts@~1.3.4 + - deps: mime-types@~2.1.16 + * deps: content-type@~1.0.4 + - perf: remove argument reassignment + - perf: skip parameter parsing when no parameters + * deps: etag@~1.8.1 + - perf: replace regular expression with substring + * deps: finalhandler@1.1.0 + - Use `res.headersSent` when available + * deps: parseurl@~1.3.2 + - perf: reduce overhead for full URLs + - perf: unroll the "fast-path" `RegExp` + * deps: proxy-addr@~2.0.2 + - Fix trimming leading / trailing OWS in `X-Forwarded-For` + - deps: forwarded@~0.1.2 + - deps: ipaddr.js@1.5.2 + - perf: reduce overhead when no `X-Forwarded-For` header + * deps: qs@6.5.1 + - Fix parsing & compacting very deep objects + * deps: send@0.16.0 + - Add 70 new types for file extensions + - Add `immutable` option + - Fix missing `` in default error & redirects + - Set charset as "UTF-8" for .js and .json + - Use instance methods on steam to check for listeners + - deps: mime@1.4.1 + - perf: improve path validation speed + * deps: serve-static@1.13.0 + - Add 70 new types for file extensions + - Add `immutable` option + - Set charset as "UTF-8" for .js and .json + - deps: send@0.16.0 + * deps: setprototypeof@1.1.0 + * deps: utils-merge@1.0.1 + * deps: vary@~1.1.2 + - perf: improve header token parsing speed + * perf: re-use options object when generating ETags + * perf: remove dead `.charset` set in `res.jsonp` + +4.15.5 / 2017-09-24 +=================== + + * deps: debug@2.6.9 + * deps: finalhandler@~1.0.6 + - deps: debug@2.6.9 + - deps: parseurl@~1.3.2 + * deps: fresh@0.5.2 + - Fix handling of modified headers with invalid dates + - perf: improve ETag match loop + - perf: improve `If-None-Match` token parsing + * deps: send@0.15.6 + - Fix handling of modified headers with invalid dates + - deps: debug@2.6.9 + - deps: etag@~1.8.1 + - deps: fresh@0.5.2 + - perf: improve `If-Match` token parsing + * deps: serve-static@1.12.6 + - deps: parseurl@~1.3.2 + - deps: send@0.15.6 + - perf: improve slash collapsing + +4.15.4 / 2017-08-06 +=================== + + * deps: debug@2.6.8 + * deps: depd@~1.1.1 + - Remove unnecessary `Buffer` loading + * deps: finalhandler@~1.0.4 + - deps: debug@2.6.8 + * deps: proxy-addr@~1.1.5 + - Fix array argument being altered + - deps: ipaddr.js@1.4.0 + * deps: qs@6.5.0 + * deps: send@0.15.4 + - deps: debug@2.6.8 + - deps: depd@~1.1.1 + - deps: http-errors@~1.6.2 + * deps: serve-static@1.12.4 + - deps: send@0.15.4 + +4.15.3 / 2017-05-16 +=================== + + * Fix error when `res.set` cannot add charset to `Content-Type` + * deps: debug@2.6.7 + - Fix `DEBUG_MAX_ARRAY_LENGTH` + - deps: ms@2.0.0 + * deps: finalhandler@~1.0.3 + - Fix missing `` in HTML document + - deps: debug@2.6.7 + * deps: proxy-addr@~1.1.4 + - deps: ipaddr.js@1.3.0 + * deps: send@0.15.3 + - deps: debug@2.6.7 + - deps: ms@2.0.0 + * deps: serve-static@1.12.3 + - deps: send@0.15.3 + * deps: type-is@~1.6.15 + - deps: mime-types@~2.1.15 + * deps: vary@~1.1.1 + - perf: hoist regular expression + 4.15.2 / 2017-03-06 =================== @@ -121,7 +398,7 @@ - Fix including type extensions in parameters in `Accept` parsing - Fix parsing `Accept` parameters with quoted equals - Fix parsing `Accept` parameters with quoted semicolons - - Many performance improvments + - Many performance improvements - deps: mime-types@~2.1.11 - deps: negotiator@0.6.1 * deps: content-type@~1.0.2 @@ -136,7 +413,7 @@ - perf: enable strict mode - perf: hoist regular expression - perf: use for loop in parse - - perf: use string concatination for serialization + - perf: use string concatenation for serialization * deps: finalhandler@0.5.0 - Change invalid or non-numeric status code to 500 - Overwrite status message to match set status code @@ -146,7 +423,7 @@ * deps: proxy-addr@~1.1.2 - Fix accepting various invalid netmasks - Fix IPv6-mapped IPv4 validation edge cases - - IPv4 netmasks must be contingous + - IPv4 netmasks must be contiguous - IPv6 addresses cannot be used as a netmask - deps: ipaddr.js@1.1.1 * deps: qs@6.2.0 @@ -924,13 +1201,13 @@ - deps: negotiator@0.4.6 * deps: debug@1.0.2 * deps: send@0.4.3 - - Do not throw un-catchable error on file open race condition + - Do not throw uncatchable error on file open race condition - Use `escape-html` for HTML escaping - deps: debug@1.0.2 - deps: finished@1.2.2 - deps: fresh@0.2.2 * deps: serve-static@1.2.3 - - Do not throw un-catchable error on file open race condition + - Do not throw uncatchable error on file open race condition - deps: send@0.4.3 4.4.2 / 2014-06-09 @@ -1810,7 +2087,7 @@ - deps: serve-static@1.2.3 * deps: debug@1.0.2 * deps: send@0.4.3 - - Do not throw un-catchable error on file open race condition + - Do not throw uncatchable error on file open race condition - Use `escape-html` for HTML escaping - deps: debug@1.0.2 - deps: finished@1.2.2 @@ -2995,7 +3272,7 @@ Shaw] * Updated haml submodule * Changed ETag; removed inode, modified time only * Fixed LF to CRLF for setting multiple cookies - * Fixed cookie complation; values are now urlencoded + * Fixed cookie compilation; values are now urlencoded * Fixed cookies parsing; accepts quoted values and url escaped cookies 0.11.0 / 2010-05-06 @@ -3190,7 +3467,7 @@ Shaw] * Added "plot" format option for Profiler (for gnuplot processing) * Added request number to Profiler plugin - * Fixed binary encoding for multi-part file uploads, was previously defaulting to UTF8 + * Fixed binary encoding for multipart file uploads, was previously defaulting to UTF8 * Fixed issue with routes not firing when not files are present. Closes #184 * Fixed process.Promise -> events.Promise @@ -3236,7 +3513,7 @@ Shaw] * Updated sample chat app to show messages on load * Updated libxmljs parseString -> parseHtmlString * Fixed `make init` to work with older versions of git - * Fixed specs can now run independent specs for those who cant build deps. Closes #127 + * Fixed specs can now run independent specs for those who can't build deps. Closes #127 * Fixed issues introduced by the node url module changes. Closes 126. * Fixed two assertions failing due to Collection#keys() returning strings * Fixed faulty Collection#toArray() spec due to keys() returning strings diff --git a/Readme.md b/Readme.md index 0ddc68fdc9a..b60d588c413 100644 --- a/Readme.md +++ b/Readme.md @@ -4,13 +4,13 @@ [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] - [![Linux Build][travis-image]][travis-url] + [![Linux Build][ci-image]][ci-url] [![Windows Build][appveyor-image]][appveyor-url] [![Test Coverage][coveralls-image]][coveralls-url] ```js -var express = require('express') -var app = express() +const express = require('express') +const app = express() app.get('/', function (req, res) { res.send('Hello World') @@ -21,10 +21,25 @@ app.listen(3000) ## Installation +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). + +Before installing, [download and install Node.js](https://nodejs.org/en/download/). +Node.js 0.10 or higher is required. + +If this is a brand new project, make sure to create a `package.json` first with +the [`npm init` command](https://docs.npmjs.com/creating-a-package-json-file). + +Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + ```bash $ npm install express ``` +Follow [our installing guide](http://expressjs.com/en/starter/installing.html) +for more information. + ## Features * Robust routing @@ -39,15 +54,14 @@ $ npm install express * [Website and Documentation](http://expressjs.com/) - [[website repo](https://github.com/expressjs/expressjs.com)] * [#express](https://webchat.freenode.net/?channels=express) on freenode IRC - * [Github Organization](https://github.com/expressjs) for Official Middleware & Modules + * [GitHub Organization](https://github.com/expressjs) for Official Middleware & Modules * Visit the [Wiki](https://github.com/expressjs/express/wiki) * [Google Group](https://groups.google.com/group/express-js) for discussion * [Gitter](https://gitter.im/expressjs/express) for support and discussion - * [Русскоязычная документация](http://jsman.ru/express/) **PROTIP** Be sure to read [Migrating from 3.x to 4.x](https://github.com/expressjs/express/wiki/Migrating-from-3.x-to-4.x) as well as [New features in 4.x](https://github.com/expressjs/express/wiki/New-features-in-4.x). -###Security Issues +### Security Issues If you discover a security vulnerability in Express, please see [Security Policies and Procedures](Security.md). @@ -79,10 +93,12 @@ $ npm install $ npm start ``` + View the website at: http://localhost:3000 + ## Philosophy The Express philosophy is to provide small, robust tooling for HTTP servers, making - it a great solution for single page applications, web sites, hybrids, or public + it a great solution for single page applications, websites, hybrids, or public HTTP APIs. Express does not force you to use any specific ORM or template engine. With support for over @@ -114,11 +130,15 @@ $ npm install $ npm test ``` +## Contributing + +[Contributing Guide](Contributing.md) + ## People -The original author of Express is [TJ Holowaychuk](https://github.com/tj) [![TJ's Gratipay][gratipay-image-visionmedia]][gratipay-url-visionmedia] +The original author of Express is [TJ Holowaychuk](https://github.com/tj) -The current lead maintainer is [Douglas Christopher Wilson](https://github.com/dougwilson) [![Doug's Gratipay][gratipay-image-dougwilson]][gratipay-url-dougwilson] +The current lead maintainer is [Douglas Christopher Wilson](https://github.com/dougwilson) [List of all contributors](https://github.com/expressjs/express/graphs/contributors) @@ -126,17 +146,13 @@ The current lead maintainer is [Douglas Christopher Wilson](https://github.com/d [MIT](LICENSE) +[ci-image]: https://img.shields.io/github/workflow/status/expressjs/express/ci/master.svg?label=linux +[ci-url]: https://github.com/expressjs/express/actions?query=workflow%3Aci [npm-image]: https://img.shields.io/npm/v/express.svg [npm-url]: https://npmjs.org/package/express [downloads-image]: https://img.shields.io/npm/dm/express.svg -[downloads-url]: https://npmjs.org/package/express -[travis-image]: https://img.shields.io/travis/expressjs/express/master.svg?label=linux -[travis-url]: https://travis-ci.org/expressjs/express +[downloads-url]: https://npmcharts.com/compare/express?minimal=true [appveyor-image]: https://img.shields.io/appveyor/ci/dougwilson/express/master.svg?label=windows [appveyor-url]: https://ci.appveyor.com/project/dougwilson/express [coveralls-image]: https://img.shields.io/coveralls/expressjs/express/master.svg [coveralls-url]: https://coveralls.io/r/expressjs/express?branch=master -[gratipay-image-visionmedia]: https://img.shields.io/gratipay/visionmedia.svg -[gratipay-url-visionmedia]: https://gratipay.com/visionmedia/ -[gratipay-image-dougwilson]: https://img.shields.io/gratipay/dougwilson.svg -[gratipay-url-dougwilson]: https://gratipay.com/dougwilson/ diff --git a/Security.md b/Security.md index cbc5f16cde5..858dfffc5bc 100644 --- a/Security.md +++ b/Security.md @@ -16,6 +16,10 @@ contributions. Report security bugs by emailing the lead maintainer in the Readme.md file. +To ensure the timely response to your report, please ensure that the entirety +of the report is contained within the email body and not solely behind a web +link or an attachment. + The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating the next steps in handling your report. After the initial reply to your report, the security team will diff --git a/Triager-Guide.md b/Triager-Guide.md new file mode 100644 index 00000000000..a2909ef30db --- /dev/null +++ b/Triager-Guide.md @@ -0,0 +1,63 @@ +# Express Triager Guide + +## Issue Triage Process + +When a new issue or pull request is opened the issue will be labeled with `needs triage`. +If a triage team member is available they can help make sure all the required information +is provided. Depending on the issue or PR there are several next labels they can add for further +classification: + +* `needs triage`: This can be kept if the triager is unsure which next steps to take +* `awaiting more info`: If more info has been requested from the author, apply this label. +* `question`: User questions that do not appear to be bugs or enhancements. +* `discuss`: Topics for discussion. Might end in an `enhancement` or `question` label. +* `bug`: Issues that present a reasonable conviction there is a reproducible bug. +* `enhancement`: Issues that are found to be a reasonable candidate feature additions. + +In all cases, issues may be closed by maintainers if they don't receive a timely response when +further information is sought, or when additional questions are asked. + +## Approaches and Best Practices for getting into triage contributions + +Review the organization's [StatusBoard](https://expressjs.github.io/statusboard/), +pay special attention to these columns: stars, watchers, open issues, and contributors. +This gives you a general idea about the criticality and health of the repository. +Pick a few projects based on that criteria, your interests, and skills (existing or aspiring). + +Review the project's contribution guideline if present. In a nutshell, +commit to the community's standards and values. Review the +documentation, for most of the projects it is just the README.md, and +make sure you understand the key APIs, semantics, configurations, and use cases. + +It might be helpful to write your own test apps to re-affirm your +understanding of the key functions. This may identify some gaps in +documentation, record those as they might be good PR's to open. +Skim through the issue backlog; identify low hanging issues and mostly new ones. +From those, attempt to recreate issues based on the OP description and +ask questions if required. No question is a bad question! + +## Removal of Triage Role + +There are a few cases where members can be removed as triagers: + +- Breaking the CoC or project contributor guidelines +- Abuse or misuse of the role as deemed by the TC +- Lack of participation for more than 6 months + +If any of these happen we will discuss as a part of the triage portion of the regular TC meetings. +If you have questions feel free to reach out to any of the TC members. + +## Other Helpful Hints: + +- Everyone is welcome to attend the [Express Technical Committee Meetings](https://github.com/expressjs/discussions#expressjs-tc-meetings), and as a triager, it might help to get a better idea of what's happening with the project. +- When exploring the module's functionality there are a few helpful steps: + - Turn on `DEBUG=*` (see https://www.npmjs.com/package/debug) to get detailed log information + - It is also a good idea to do live debugging to follow the control flow, try using `node --inspect` + - It is a good idea to make at least one pass of reading through the entire source +- When reviewing the list of open issues there are some common types and suggested actions: + - New/unattended issues or simple questions: A good place to start + - Hard bugs & ongoing discussions: always feel free to chime in and help + - Issues that imply gaps in the documentation: open PRs with changes or help the user to do so +- For recurring issues, it is helpful to create functional examples to demonstrate (publish as gists or a repo) +- Review and identify the maintainers. If necessary, at-mention one or more of them if you are unsure what to do +- Make sure all your interactions are professional, welcoming, and respectful to the parties involved. diff --git a/appveyor.yml b/appveyor.yml index b4bb184dd8f..db54a3fdb04 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,21 +5,94 @@ environment: - nodejs_version: "1.8" - nodejs_version: "2.5" - nodejs_version: "3.3" - - nodejs_version: "4.8" + - nodejs_version: "4.9" - nodejs_version: "5.12" - - nodejs_version: "6.10" - - nodejs_version: "7.6" + - nodejs_version: "6.17" + - nodejs_version: "7.10" + - nodejs_version: "8.17" + - nodejs_version: "9.11" + - nodejs_version: "10.24" + - nodejs_version: "11.15" + - nodejs_version: "12.22" + - nodejs_version: "13.14" + - nodejs_version: "14.19" cache: - node_modules install: - - ps: Install-Product node $env:nodejs_version - - npm rm --save-dev connect-redis - - if exist node_modules npm prune - - if exist node_modules npm rebuild + # Install Node.js + - ps: >- + try { Install-Product node $env:nodejs_version -ErrorAction Stop } + catch { Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) } + # Configure npm + - ps: | + npm config set loglevel error + npm config set shrinkwrap false + # Remove all non-test dependencies + - ps: | + # Remove example dependencies + npm rm --silent --save-dev connect-redis + # Remove lint dependencies + cmd.exe /c "node -pe `"Object.keys(require('./package').devDependencies).join('\n')`"" | ` + sls "^eslint(-|$)" | ` + %{ npm rm --silent --save-dev $_ } + # Setup Node.js version-specific dependencies + - ps: | + # mocha for testing + # - use 3.x for Node.js < 4 + # - use 5.x for Node.js < 6 + # - use 6.x for Node.js < 8 + # - use 7.x for Node.js < 10 + # - use 8.x for Node.js < 12 + if ([int]$env:nodejs_version.split(".")[0] -lt 4) { + npm install --silent --save-dev mocha@3.5.3 + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) { + npm install --silent --save-dev mocha@5.2.0 + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) { + npm install --silent --save-dev mocha@6.2.2 + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 10) { + npm install --silent --save-dev mocha@7.2.0 + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 12) { + npm install --silent --save-dev mocha@8.4.0 + } + - ps: | + # nyc for test coverage + # - use 10.3.2 for Node.js < 4 + # - use 11.9.0 for Node.js < 6 + # - use 14.1.1 for Node.js < 8 + if ([int]$env:nodejs_version.split(".")[0] -lt 4) { + npm install --silent --save-dev nyc@10.3.2 + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) { + npm install --silent --save-dev nyc@11.9.0 + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) { + npm install --silent --save-dev nyc@14.1.1 + } + - ps: | + # supertest for http calls + # - use 2.0.0 for Node.js < 4 + # - use 3.4.2 for Node.js < 6 + # - use 6.1.6 for Node.js < 8 + if ([int]$env:nodejs_version.split(".")[0] -lt 4) { + npm install --silent --save-dev supertest@2.0.0 + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) { + npm install --silent --save-dev supertest@3.4.2 + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) { + npm install --silent --save-dev supertest@6.1.6 + } + # Update Node.js modules + - ps: | + # Prune & rebuild node_modules + if (Test-Path -Path node_modules) { + npm prune + npm rebuild + } + # Install Node.js modules - npm install build: off test_script: - - node --version - - npm --version + # Output version data + - ps: | + node --version + npm --version + # Run test script - npm run test-ci version: "{build}" diff --git a/benchmarks/middleware.js b/benchmarks/middleware.js index 3aa7a8b4ac7..df4df2c5ac5 100644 --- a/benchmarks/middleware.js +++ b/benchmarks/middleware.js @@ -1,5 +1,4 @@ -var http = require('http'); var express = require('..'); var app = express(); @@ -14,10 +13,8 @@ while (n--) { }); } -var body = new Buffer('Hello World'); - app.use(function(req, res, next){ - res.send(body); + res.send('Hello World') }); app.listen(3333); diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000000..c19ed30a25c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,30 @@ +# Express examples + +This page contains list of examples using Express. + +- [auth](./auth) - Authentication with login and password +- [content-negotiation](./content-negotiation) - HTTP content negotiation +- [cookie-sessions](./cookie-sessions) - Working with cookie-based sessions +- [cookies](./cookies) - Working with cookies +- [downloads](./downloads) - Transferring files to client +- [ejs](./ejs) - Working with Embedded JavaScript templating (ejs) +- [error-pages](./error-pages) - Creating error pages +- [error](./error) - Working with error middleware +- [hello-world](./hello-world) - Simple request handler +- [markdown](./markdown) - Markdown as template engine +- [multi-router](./multi-router) - Working with multiple Express routers +- [multipart](./multipart) - Accepting multipart-encoded forms +- [mvc](./mvc) - MVC-style controllers +- [online](./online) - Tracking online user activity with `online` and `redis` packages +- [params](./params) - Working with route parameters +- [resource](./resource) - Multiple HTTP operations on the same resource +- [route-map](./route-map) - Organizing routes using a map +- [route-middleware](./route-middleware) - Working with route middleware +- [route-separation](./route-separation) - Organizing routes per each resource +- [search](./search) - Search API +- [session](./session) - User sessions +- [static-files](./static-files) - Serving static files +- [vhost](./vhost) - Working with virtual hosts +- [view-constructor](./view-constructor) - Rendering views dynamically +- [view-locals](./view-locals) - Saving data in request object between middleware calls +- [web-service](./web-service) - Simple API service diff --git a/examples/auth/index.js b/examples/auth/index.js index 084fba79a97..36205d0f994 100644 --- a/examples/auth/index.js +++ b/examples/auth/index.js @@ -1,9 +1,10 @@ +'use strict' + /** * Module dependencies. */ var express = require('../..'); -var bodyParser = require('body-parser'); var hash = require('pbkdf2-password')() var path = require('path'); var session = require('express-session'); @@ -17,7 +18,7 @@ app.set('views', path.join(__dirname, 'views')); // middleware -app.use(bodyParser.urlencoded({ extended: false })); +app.use(express.urlencoded({ extended: false })) app.use(session({ resave: false, // don't save session if unmodified saveUninitialized: false, // don't create session until something stored @@ -60,14 +61,14 @@ function authenticate(name, pass, fn) { if (!module.parent) console.log('authenticating %s:%s', name, pass); var user = users[name]; // query the db for the given username - if (!user) return fn(new Error('cannot find user')); + if (!user) return fn(null, null) // apply the same algorithm to the POSTed password, applying // the hash against the pass / salt, if there is a match we // found the user hash({ password: pass, salt: user.salt }, function (err, pass, salt, hash) { if (err) return fn(err); - if (hash == user.hash) return fn(null, user); - fn(new Error('invalid password')); + if (hash === user.hash) return fn(null, user) + fn(null, null) }); } @@ -100,8 +101,9 @@ app.get('/login', function(req, res){ res.render('login'); }); -app.post('/login', function(req, res){ +app.post('/login', function (req, res, next) { authenticate(req.body.username, req.body.password, function(err, user){ + if (err) return next(err) if (user) { // Regenerate session when signing in // to prevent fixation diff --git a/examples/auth/views/head.ejs b/examples/auth/views/head.ejs index 0a919f49296..65386267d0d 100644 --- a/examples/auth/views/head.ejs +++ b/examples/auth/views/head.ejs @@ -1,6 +1,8 @@ + + <%= title %> + + +

<%= title %>

+ <% users.forEach(function(user) { %> +
  • <%= user.name %> is a <% user.age %> year old <%= user.species %>
  • + <% }); %> + + diff --git a/examples/web-service/index.js b/examples/web-service/index.js index 694e121d91b..a2cd2cb7f90 100644 --- a/examples/web-service/index.js +++ b/examples/web-service/index.js @@ -1,3 +1,5 @@ +'use strict' + /** * Module dependencies. */ @@ -8,7 +10,7 @@ var app = module.exports = express(); // create an error with .status. we // can then use the property in our -// custom error handler (Connect repects this prop as well) +// custom error handler (Connect respects this prop as well) function error(status, msg) { var err = new Error(msg); @@ -32,7 +34,7 @@ app.use('/api', function(req, res, next){ if (!key) return next(error(400, 'api key required')); // key is invalid - if (!~apiKeys.indexOf(key)) return next(error(401, 'invalid api key')); + if (apiKeys.indexOf(key) === -1) return next(error(401, 'invalid api key')) // all good, store req.key for route access req.key = key; @@ -49,19 +51,19 @@ var apiKeys = ['foo', 'bar', 'baz']; // these two objects will serve as our faux database var repos = [ - { name: 'express', url: 'http://github.com/expressjs/express' } - , { name: 'stylus', url: 'http://github.com/learnboost/stylus' } - , { name: 'cluster', url: 'http://github.com/learnboost/cluster' } + { name: 'express', url: 'https://github.com/expressjs/express' }, + { name: 'stylus', url: 'https://github.com/learnboost/stylus' }, + { name: 'cluster', url: 'https://github.com/learnboost/cluster' } ]; var users = [ - { name: 'tobi' } + { name: 'tobi' } , { name: 'loki' } , { name: 'jane' } ]; var userRepos = { - tobi: [repos[0], repos[1]] + tobi: [repos[0], repos[1]] , loki: [repos[1]] , jane: [repos[2]] }; @@ -69,14 +71,17 @@ var userRepos = { // we now can assume the api key is valid, // and simply expose the data +// example: http://localhost:3000/api/users/?api-key=foo app.get('/api/users', function(req, res, next){ res.send(users); }); +// example: http://localhost:3000/api/repos/?api-key=foo app.get('/api/repos', function(req, res, next){ res.send(repos); }); +// example: http://localhost:3000/api/user/tobi/repos/?api-key=foo app.get('/api/user/:name/repos', function(req, res, next){ var name = req.params.name; var user = userRepos[name]; @@ -102,7 +107,7 @@ app.use(function(err, req, res, next){ // invoke next() and do not respond. app.use(function(req, res){ res.status(404); - res.send({ error: "Lame, can't find that" }); + res.send({ error: "Sorry, can't find that" }) }); /* istanbul ignore next */ diff --git a/lib/application.js b/lib/application.js index 21a81ee9efe..e65ba588959 100644 --- a/lib/application.js +++ b/lib/application.js @@ -28,7 +28,7 @@ var deprecate = require('depd')('express'); var flatten = require('array-flatten'); var merge = require('utils-merge'); var resolve = require('path').resolve; -var setPrototyeOf = require('setprototypeof') +var setPrototypeOf = require('setprototypeof') var slice = Array.prototype.slice; /** @@ -95,10 +95,10 @@ app.defaultConfiguration = function defaultConfiguration() { } // inherit protos - setPrototyeOf(this.request, parent.request) - setPrototyeOf(this.response, parent.response) - setPrototyeOf(this.engines, parent.engines) - setPrototyeOf(this.settings, parent.settings) + setPrototypeOf(this.request, parent.request) + setPrototypeOf(this.response, parent.response) + setPrototypeOf(this.engines, parent.engines) + setPrototypeOf(this.settings, parent.settings) }); // setup locals @@ -207,7 +207,7 @@ app.use = function use(fn) { var fns = flatten(slice.call(arguments, offset)); if (fns.length === 0) { - throw new TypeError('app.use() requires middleware functions'); + throw new TypeError('app.use() requires a middleware function') } // setup router @@ -228,8 +228,8 @@ app.use = function use(fn) { router.use(path, function mounted_app(req, res, next) { var orig = req.app; fn.handle(req, res, function (err) { - setPrototyeOf(req, orig.request) - setPrototyeOf(res, orig.response) + setPrototypeOf(req, orig.request) + setPrototypeOf(res, orig.response) next(err); }); }); @@ -276,7 +276,7 @@ app.route = function route(path) { * In this case EJS provides a `.renderFile()` method with * the same signature that Express expects: `(path, options, callback)`, * though note that it aliases this method as `ejs.__express` internally - * so if you're using ".ejs" extensions you dont need to do anything. + * so if you're using ".ejs" extensions you don't need to do anything. * * Some template engines do not follow this convention, the * [Consolidate.js](https://github.com/tj/consolidate.js) @@ -338,7 +338,7 @@ app.param = function param(name, fn) { * Assign `setting` to `val`, or return `setting`'s value. * * app.set('foo', 'bar'); - * app.get('foo'); + * app.set('foo'); * // => "bar" * * Mounted servers inherit their parent server's settings. diff --git a/lib/express.js b/lib/express.js index 187e4e2d7ca..d188a16db70 100644 --- a/lib/express.js +++ b/lib/express.js @@ -12,6 +12,7 @@ * Module dependencies. */ +var bodyParser = require('body-parser') var EventEmitter = require('events').EventEmitter; var mixin = require('merge-descriptors'); var proto = require('./application'); @@ -74,16 +75,18 @@ exports.Router = Router; * Expose middleware */ +exports.json = bodyParser.json exports.query = require('./middleware/query'); +exports.raw = bodyParser.raw exports.static = require('serve-static'); +exports.text = bodyParser.text +exports.urlencoded = bodyParser.urlencoded /** * Replace removed middleware with an appropriate error message. */ -[ - 'json', - 'urlencoded', +var removedMiddlewares = [ 'bodyParser', 'compress', 'cookieSession', @@ -100,8 +103,10 @@ exports.static = require('serve-static'); 'directory', 'limit', 'multipart', - 'staticCache', -].forEach(function (name) { + 'staticCache' +] + +removedMiddlewares.forEach(function (name) { Object.defineProperty(exports, name, { get: function () { throw new Error('Most middleware (like ' + name + ') is no longer bundled with Express and must be installed separately. Please see https://github.com/senchalabs/connect#middleware.'); diff --git a/lib/middleware/init.js b/lib/middleware/init.js index 328c4a863d9..dfd042747bd 100644 --- a/lib/middleware/init.js +++ b/lib/middleware/init.js @@ -13,7 +13,7 @@ * @private */ -var setPrototyeOf = require('setprototypeof') +var setPrototypeOf = require('setprototypeof') /** * Initialization middleware, exposing the @@ -32,8 +32,8 @@ exports.init = function(app){ res.req = req; req.next = next; - setPrototyeOf(req, app.request) - setPrototyeOf(res, app.response) + setPrototypeOf(req, app.request) + setPrototypeOf(res, app.response) res.locals = res.locals || Object.create(null); diff --git a/lib/middleware/query.js b/lib/middleware/query.js index 5f76f8458f0..7e9166947af 100644 --- a/lib/middleware/query.js +++ b/lib/middleware/query.js @@ -12,6 +12,7 @@ * Module dependencies. */ +var merge = require('utils-merge') var parseUrl = require('parseurl'); var qs = require('qs'); @@ -22,7 +23,7 @@ var qs = require('qs'); */ module.exports = function query(options) { - var opts = Object.create(options || null); + var opts = merge({}, options) var queryparse = qs.parse; if (typeof options === 'function') { diff --git a/lib/request.js b/lib/request.js index 3432e6776fe..3f1eeca6c1a 100644 --- a/lib/request.js +++ b/lib/request.js @@ -251,7 +251,7 @@ req.param = function param(name, defaultValue) { /** * Check if the incoming request contains the "Content-Type" - * header field, and it contains the give mime `type`. + * header field, and it contains the given mime `type`. * * Examples: * @@ -315,8 +315,12 @@ defineGetter(req, 'protocol', function protocol(){ // Note: X-Forwarded-Proto is normally only ever a // single value, but this is to be safe. - proto = this.get('X-Forwarded-Proto') || proto; - return proto.split(/\s*,\s*/)[0]; + var header = this.get('X-Forwarded-Proto') || proto + var index = header.indexOf(',') + + return index !== -1 + ? header.substring(0, index).trim() + : header.trim() }); /** @@ -426,6 +430,10 @@ defineGetter(req, 'hostname', function hostname(){ if (!host || !trust(this.connection.remoteAddress, 0)) { host = this.get('Host'); + } else if (host.indexOf(',') !== -1) { + // Note: X-Forwarded-Host is normally only ever a + // single value, but this is to be safe. + host = host.substring(0, host.indexOf(',')).trimRight() } if (!host) return; diff --git a/lib/response.js b/lib/response.js index 6aefe1b1782..ba02008522d 100644 --- a/lib/response.js +++ b/lib/response.js @@ -12,6 +12,7 @@ * @private */ +var Buffer = require('safe-buffer').Buffer var contentDisposition = require('content-disposition'); var deprecate = require('depd')('express'); var encodeUrl = require('encodeurl'); @@ -95,7 +96,7 @@ res.links = function(links){ * * Examples: * - * res.send(new Buffer('wahoo')); + * res.send(Buffer.from('wahoo')); * res.send({ some: 'json' }); * res.send('

    some html

    '); * @@ -106,7 +107,6 @@ res.links = function(links){ res.send = function send(body) { var chunk = body; var encoding; - var len; var req = this.req; var type; @@ -171,23 +171,33 @@ res.send = function send(body) { } } + // determine if ETag should be generated + var etagFn = app.get('etag fn') + var generateETag = !this.get('ETag') && typeof etagFn === 'function' + // populate Content-Length + var len if (chunk !== undefined) { - if (!Buffer.isBuffer(chunk)) { - // convert chunk to Buffer; saves later double conversions - chunk = new Buffer(chunk, encoding); + if (Buffer.isBuffer(chunk)) { + // get length of Buffer + len = chunk.length + } else if (!generateETag && chunk.length < 1000) { + // just calculate length when no ETag + small chunk + len = Buffer.byteLength(chunk, encoding) + } else { + // convert chunk to Buffer and calculate + chunk = Buffer.from(chunk, encoding) encoding = undefined; + len = chunk.length } - len = chunk.length; this.set('Content-Length', len); } // populate ETag var etag; - var generateETag = len !== undefined && app.get('etag fn'); - if (typeof generateETag === 'function' && !this.get('ETag')) { - if ((etag = generateETag(chunk, encoding))) { + if (generateETag && len !== undefined) { + if ((etag = etagFn(chunk, encoding))) { this.set('ETag', etag); } } @@ -244,9 +254,10 @@ res.json = function json(obj) { // settings var app = this.app; + var escape = app.get('json escape') var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = stringify(val, replacer, spaces); + var body = stringify(val, replacer, spaces, escape) // content-type if (!this.get('Content-Type')) { @@ -273,9 +284,9 @@ res.jsonp = function jsonp(obj) { // allow status / body if (arguments.length === 2) { - // res.json(body, status) backwards compat + // res.jsonp(body, status) backwards compat if (typeof arguments[1] === 'number') { - deprecate('res.jsonp(obj, status): Use res.status(status).json(obj) instead'); + deprecate('res.jsonp(obj, status): Use res.status(status).jsonp(obj) instead'); this.statusCode = arguments[1]; } else { deprecate('res.jsonp(status, obj): Use res.status(status).jsonp(obj) instead'); @@ -286,9 +297,10 @@ res.jsonp = function jsonp(obj) { // settings var app = this.app; + var escape = app.get('json escape') var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = stringify(val, replacer, spaces); + var body = stringify(val, replacer, spaces, escape) var callback = this.req.query[app.get('jsonp callback name')]; // content-type @@ -304,17 +316,21 @@ res.jsonp = function jsonp(obj) { // jsonp if (typeof callback === 'string' && callback.length !== 0) { - this.charset = 'utf-8'; this.set('X-Content-Type-Options', 'nosniff'); this.set('Content-Type', 'text/javascript'); // restrict callback charset callback = callback.replace(/[^\[\]\w$.]/g, ''); - // replace chars not allowed in JavaScript that are in JSON - body = body - .replace(/\u2028/g, '\\u2028') - .replace(/\u2029/g, '\\u2029'); + if (body === undefined) { + // empty argument + body = '' + } else if (typeof body === 'string') { + // replace chars not allowed in JavaScript that are in JSON + body = body + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029') + } // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse" // the typeof check is just to reduce client error noise @@ -353,7 +369,7 @@ res.sendStatus = function sendStatus(statusCode) { * * Automatically sets the _Content-Type_ response header field. * The callback `callback(err)` is invoked when the transfer is complete - * or when an error occurs. Be sure to check `res.sentHeader` + * or when an error occurs. Be sure to check `res.headersSent` * if you wish to attempt responding, as the header and some data * may have already been transferred. * @@ -400,6 +416,10 @@ res.sendFile = function sendFile(path, options, callback) { throw new TypeError('path argument is required to res.sendFile'); } + if (typeof path !== 'string') { + throw new TypeError('path must be a string to res.sendFile') + } + // support function as second arg if (typeof options === 'function') { done = options; @@ -431,7 +451,7 @@ res.sendFile = function sendFile(path, options, callback) { * * Automatically sets the _Content-Type_ response header field. * The callback `callback(err)` is invoked when the transfer is complete - * or when an error occurs. Be sure to check `res.sentHeader` + * or when an error occurs. Be sure to check `res.headersSent` * if you wish to attempt responding, as the header and some data * may have already been transferred. * @@ -489,7 +509,7 @@ res.sendfile = function (path, options, callback) { if (err && err.code === 'EISDIR') return next(); // next() all but write errors - if (err && err.code !== 'ECONNABORT' && err.syscall !== 'write') { + if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') { next(err); } }); @@ -504,21 +524,31 @@ res.sendfile = deprecate.function(res.sendfile, * Optionally providing an alternate attachment `filename`, * and optional callback `callback(err)`. The callback is invoked * when the data transfer is complete, or when an error has - * ocurred. Be sure to check `res.headersSent` if you plan to respond. + * occurred. Be sure to check `res.headersSent` if you plan to respond. + * + * Optionally providing an `options` object to use with `res.sendFile()`. + * This function will set the `Content-Disposition` header, overriding + * any `Content-Disposition` header passed as header options in order + * to set the attachment and filename. * - * This method uses `res.sendfile()`. + * This method uses `res.sendFile()`. * * @public */ -res.download = function download(path, filename, callback) { +res.download = function download (path, filename, options, callback) { var done = callback; var name = filename; + var opts = options || null - // support function as second arg + // support function as second or third arg if (typeof filename === 'function') { done = filename; name = null; + opts = null + } else if (typeof options === 'function') { + done = options + opts = null } // set Content-Disposition when file is sent @@ -526,10 +556,26 @@ res.download = function download(path, filename, callback) { 'Content-Disposition': contentDisposition(name || path) }; + // merge user-provided headers + if (opts && opts.headers) { + var keys = Object.keys(opts.headers) + for (var i = 0; i < keys.length; i++) { + var key = keys[i] + if (key.toLowerCase() !== 'content-disposition') { + headers[key] = opts.headers[key] + } + } + } + + // merge user-provided options + opts = Object.create(opts) + opts.headers = headers + // Resolve the full path for sendFile var fullPath = resolve(path); - return this.sendFile(fullPath, { headers: headers }, done); + // send file + return this.sendFile(fullPath, opts, done) }; /** @@ -582,7 +628,7 @@ res.type = function contentType(type) { * res.send('

    hey

    '); * }, * - * 'appliation/json': function(){ + * 'application/json': function () { * res.send({ message: 'hey' }); * } * }); @@ -685,7 +731,7 @@ res.append = function append(field, val) { // concat the new and prev vals value = Array.isArray(prev) ? prev.concat(val) : Array.isArray(val) ? [prev].concat(val) - : [prev, val]; + : [prev, val] } return this.set(field, value); @@ -717,9 +763,14 @@ res.header = function header(field, val) { : String(val); // add charset to content-type - if (field.toLowerCase() === 'content-type' && !charsetRegExp.test(value)) { - var charset = mime.charsets.lookup(value.split(';')[0]); - if (charset) value += '; charset=' + charset.toLowerCase(); + if (field.toLowerCase() === 'content-type') { + if (Array.isArray(value)) { + throw new TypeError('Content-Type cannot be set to an Array'); + } + if (!charsetRegExp.test(value)) { + var charset = mime.charsets.lookup(value.split(';')[0]); + if (charset) value += '; charset=' + charset.toLowerCase(); + } } this.setHeader(field, value); @@ -772,12 +823,12 @@ res.clearCookie = function clearCookie(name, options) { * // "Remember Me" for 15 minutes * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); * - * // save as above + * // same as above * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) * * @param {String} name * @param {String|Object} value - * @param {Options} options + * @param {Object} [options] * @return {ServerResponse} for chaining * @public */ @@ -1058,14 +1109,39 @@ function sendfile(res, file, options, callback) { } /** - * Stringify JSON, like JSON.stringify, but v8 optimized. + * Stringify JSON, like JSON.stringify, but v8 optimized, with the + * ability to escape characters that can trigger HTML sniffing. + * + * @param {*} value + * @param {function} replaces + * @param {number} spaces + * @param {boolean} escape + * @returns {string} * @private */ -function stringify(value, replacer, spaces) { +function stringify (value, replacer, spaces, escape) { // v8 checks arguments.length for optimizing simple call // https://bugs.chromium.org/p/v8/issues/detail?id=4730 - return replacer || spaces + var json = replacer || spaces ? JSON.stringify(value, replacer, spaces) : JSON.stringify(value); + + if (escape && typeof json === 'string') { + json = json.replace(/[<>&]/g, function (c) { + switch (c.charCodeAt(0)) { + case 0x3c: + return '\\u003c' + case 0x3e: + return '\\u003e' + case 0x26: + return '\\u0026' + /* istanbul ignore next: unreachable default */ + default: + return c + } + }) + } + + return json } diff --git a/lib/router/index.js b/lib/router/index.js index 51db4c28ff9..fbe94acdb47 100644 --- a/lib/router/index.js +++ b/lib/router/index.js @@ -35,7 +35,7 @@ var toString = Object.prototype.toString; /** * Initialize a new `Router` with the given `options`. * - * @param {Object} options + * @param {Object} [options] * @return {Router} which is an callable function * @public */ @@ -287,6 +287,12 @@ proto.handle = function handle(req, res, out) { function trim_prefix(layer, layerError, layerPath, path) { if (layerPath.length !== 0) { + // Validate path is a prefix match + if (layerPath !== path.substr(0, layerPath.length)) { + next(layerError) + return + } + // Validate path breaks on a path separator var c = path[layerPath.length] if (c && c !== '/' && c !== '.') return next(layerError) @@ -448,14 +454,14 @@ proto.use = function use(fn) { var callbacks = flatten(slice.call(arguments, offset)); if (callbacks.length === 0) { - throw new TypeError('Router.use() requires middleware functions'); + throw new TypeError('Router.use() requires a middleware function') } for (var i = 0; i < callbacks.length; i++) { var fn = callbacks[i]; if (typeof fn !== 'function') { - throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn)); + throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn)) } // add the middleware diff --git a/lib/router/route.js b/lib/router/route.js index ea82ed29df5..178df0d5160 100644 --- a/lib/router/route.js +++ b/lib/router/route.js @@ -175,7 +175,7 @@ Route.prototype.all = function all() { if (typeof handle !== 'function') { var type = toString.call(handle); - var msg = 'Route.all() requires callback functions but got a ' + type; + var msg = 'Route.all() requires a callback function but got a ' + type throw new TypeError(msg); } @@ -198,7 +198,7 @@ methods.forEach(function(method){ if (typeof handle !== 'function') { var type = toString.call(handle); - var msg = 'Route.' + method + '() requires callback functions but got a ' + type; + var msg = 'Route.' + method + '() requires a callback function but got a ' + type throw new Error(msg); } diff --git a/lib/utils.js b/lib/utils.js index f418c5807c7..7797b068530 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -12,12 +12,12 @@ * @api private */ +var Buffer = require('safe-buffer').Buffer var contentDisposition = require('content-disposition'); var contentType = require('content-type'); var deprecate = require('depd')('express'); var flatten = require('array-flatten'); var mime = require('send').mime; -var basename = require('path').basename; var etag = require('etag'); var proxyaddr = require('proxy-addr'); var qs = require('qs'); @@ -32,13 +32,7 @@ var querystring = require('querystring'); * @api private */ -exports.etag = function (body, encoding) { - var buf = !Buffer.isBuffer(body) - ? new Buffer(body, encoding) - : body; - - return etag(buf, {weak: false}); -}; +exports.etag = createETagGenerator({ weak: false }) /** * Return weak ETag for `body`. @@ -49,13 +43,7 @@ exports.etag = function (body, encoding) { * @api private */ -exports.wetag = function wetag(body, encoding){ - var buf = !Buffer.isBuffer(body) - ? new Buffer(body, encoding) - : body; - - return etag(buf, {weak: true}); -}; +exports.wetag = createETagGenerator({ weak: true }) /** * Check if `path` looks absolute. @@ -169,6 +157,7 @@ exports.compileETag = function(val) { switch (val) { case true: + case 'weak': fn = exports.wetag; break; case false: @@ -176,9 +165,6 @@ exports.compileETag = function(val) { case 'strong': fn = exports.etag; break; - case 'weak': - fn = exports.wetag; - break; default: throw new TypeError('unknown value for etag function: ' + val); } @@ -203,6 +189,7 @@ exports.compileQueryParser = function compileQueryParser(val) { switch (val) { case true: + case 'simple': fn = querystring.parse; break; case false: @@ -211,9 +198,6 @@ exports.compileQueryParser = function compileQueryParser(val) { case 'extended': fn = parseExtendedQueryString; break; - case 'simple': - fn = querystring.parse; - break; default: throw new TypeError('unknown value for query parser function: ' + val); } @@ -244,7 +228,8 @@ exports.compileTrust = function(val) { if (typeof val === 'string') { // Support comma-separated values - val = val.split(/ *, */); + val = val.split(',') + .map(function (v) { return v.trim() }) } return proxyaddr.compile(val || []); @@ -274,6 +259,25 @@ exports.setCharset = function setCharset(type, charset) { return contentType.format(parsed); }; +/** + * Create an ETag generator function, generating ETags with + * the given options. + * + * @param {object} options + * @return {function} + * @private + */ + +function createETagGenerator (options) { + return function generateETag (body, encoding) { + var buf = !Buffer.isBuffer(body) + ? Buffer.from(body, encoding) + : body + + return etag(buf, options) + } +} + /** * Parse an extended query string with qs. * diff --git a/lib/view.js b/lib/view.js index 1728725d291..cf101caeab9 100644 --- a/lib/view.js +++ b/lib/view.js @@ -16,7 +16,6 @@ var debug = require('debug')('express:view'); var path = require('path'); var fs = require('fs'); -var utils = require('./utils'); /** * Module variables. @@ -77,7 +76,15 @@ function View(name, options) { // load engine var mod = this.ext.substr(1) debug('require "%s"', mod) - opts.engines[this.ext] = require(mod).__express + + // default engine export + var fn = require(mod).__express + + if (typeof fn !== 'function') { + throw new Error('Module "' + mod + '" does not provide a view engine.') + } + + opts.engines[this.ext] = fn } // store loaded engine diff --git a/package.json b/package.json index ef49b40d425..2fb6ebaab0b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "express", "description": "Fast, unopinionated, minimalist web framework", - "version": "4.15.2", + "version": "4.17.3", "author": "TJ Holowaychuk ", "contributors": [ "Aaron Heckmann ", @@ -20,6 +20,7 @@ "framework", "sinatra", "web", + "http", "rest", "restful", "router", @@ -27,53 +28,56 @@ "api" ], "dependencies": { - "accepts": "~1.3.3", + "accepts": "~1.3.8", "array-flatten": "1.1.1", - "content-disposition": "0.5.2", - "content-type": "~1.0.2", - "cookie": "0.3.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", "cookie-signature": "1.0.6", - "debug": "2.6.1", - "depd": "~1.1.0", - "encodeurl": "~1.0.1", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "etag": "~1.8.0", - "finalhandler": "~1.0.0", - "fresh": "0.5.0", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "~2.3.0", - "parseurl": "~1.3.1", + "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~1.1.3", - "qs": "6.4.0", - "range-parser": "~1.2.0", - "send": "0.15.1", - "serve-static": "1.12.1", - "setprototypeof": "1.0.3", - "statuses": "~1.3.1", - "type-is": "~1.6.14", - "utils-merge": "1.0.0", - "vary": "~1.1.0" + "proxy-addr": "~2.0.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "devDependencies": { "after": "0.8.2", - "body-parser": "1.17.1", - "cookie-parser": "~1.4.3", - "ejs": "2.5.6", - "express-session": "1.15.1", - "istanbul": "0.4.5", - "marked": "0.3.6", - "method-override": "2.3.7", - "mocha": "3.2.0", - "morgan": "1.8.1", - "multiparty": "4.1.3", + "connect-redis": "3.4.2", + "cookie-parser": "1.4.6", + "cookie-session": "2.0.0", + "ejs": "3.1.6", + "eslint": "7.32.0", + "express-session": "1.17.2", + "hbs": "4.2.0", + "marked": "0.7.0", + "method-override": "3.0.0", + "mocha": "9.2.0", + "morgan": "1.10.0", + "multiparty": "4.2.3", + "nyc": "15.1.0", "pbkdf2-password": "1.2.1", - "should": "11.2.0", - "supertest": "1.2.0", - "connect-redis": "~2.4.1", - "cookie-session": "~1.2.0", - "jade": "~1.11.0", + "resolve-path": "1.4.0", + "should": "13.2.3", + "supertest": "6.2.2", "vhost": "~3.0.2" }, "engines": { @@ -87,9 +91,10 @@ "lib/" ], "scripts": { + "lint": "eslint .", "test": "mocha --require test/support/env --reporter spec --bail --check-leaks test/ test/acceptance/", - "test-ci": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --require test/support/env --reporter spec --check-leaks test/ test/acceptance/", - "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --require test/support/env --reporter dot --check-leaks test/ test/acceptance/", + "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test", "test-tap": "mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/" } } diff --git a/test/Route.js b/test/Route.js index ada54086bf0..005b4634e9a 100644 --- a/test/Route.js +++ b/test/Route.js @@ -1,10 +1,10 @@ +'use strict' var after = require('after'); var should = require('should'); var express = require('../') , Route = express.Route , methods = require('methods') - , assert = require('assert'); describe('Route', function(){ it('should work without handlers', function(done) { @@ -25,7 +25,7 @@ describe('Route', function(){ route.dispatch(req, {}, function (err) { if (err) return done(err); - should(req.called).be.ok; + should(req.called).be.ok() done(); }); }) @@ -84,7 +84,7 @@ describe('Route', function(){ route.dispatch(req, {}, function (err) { if (err) return done(err); - should(req.called).be.ok; + should(req.called).be.ok() done(); }); }) @@ -104,7 +104,7 @@ describe('Route', function(){ route.dispatch(req, {}, function (err) { if (err) return done(err); - should(req.called).be.true; + should(req.called).be.true() done(); }); }) @@ -156,7 +156,7 @@ describe('Route', function(){ }); route.dispatch(req, {}, function (err) { - should(err).be.ok; + should(err).be.ok() should(err.message).equal('foobar'); req.order.should.equal('a'); done(); @@ -182,7 +182,7 @@ describe('Route', function(){ }); route.dispatch(req, {}, function (err) { - should(err).be.ok; + should(err).be.ok() should(err.message).equal('foobar'); req.order.should.equal('a'); done(); @@ -222,7 +222,7 @@ describe('Route', function(){ }); route.dispatch(req, {}, function(err){ - should(err).be.ok; + should(err).be.ok() err.message.should.equal('boom!'); done(); }); @@ -234,7 +234,7 @@ describe('Route', function(){ route.all(function(err, req, res, next){ // this should not execute - true.should.be.false; + true.should.be.false() }); route.dispatch(req, {}, done); diff --git a/test/Router.js b/test/Router.js index 01a6e2c472b..907b9726361 100644 --- a/test/Router.js +++ b/test/Router.js @@ -1,3 +1,4 @@ +'use strict' var after = require('after'); var express = require('../') @@ -7,15 +8,12 @@ var express = require('../') describe('Router', function(){ it('should return a function with router methods', function() { - var router = Router(); - assert(typeof router == 'function'); - var router = new Router(); - assert(typeof router == 'function'); + assert(typeof router === 'function') - assert(typeof router.get == 'function'); - assert(typeof router.handle == 'function'); - assert(typeof router.use == 'function'); + assert(typeof router.get === 'function') + assert(typeof router.handle === 'function') + assert(typeof router.use === 'function') }); it('should support .use of other routers', function(done){ @@ -35,7 +33,7 @@ describe('Router', function(){ var another = new Router(); another.get('/:bar', function(req, res){ - req.params.bar.should.equal('route'); + assert.strictEqual(req.params.bar, 'route') res.end(); }); router.use('/:foo', another); @@ -47,7 +45,7 @@ describe('Router', function(){ var router = new Router(); router.use(function (req, res) { - false.should.be.true; + throw new Error('should not be called') }); router.handle({ url: '', method: 'GET' }, {}, done); @@ -88,7 +86,7 @@ describe('Router', function(){ var res = { send: function(val) { - val.should.equal('foo'); + assert.strictEqual(val, 'foo') done(); } } @@ -368,17 +366,29 @@ describe('Router', function(){ }) describe('.use', function() { - it('should require arguments', function(){ - var router = new Router(); - router.use.bind(router).should.throw(/requires middleware function/) + it('should require middleware', function () { + var router = new Router() + assert.throws(function () { router.use('/') }, /requires a middleware function/) }) - it('should not accept non-functions', function(){ - var router = new Router(); - router.use.bind(router, '/', 'hello').should.throw(/requires middleware function.*string/) - router.use.bind(router, '/', 5).should.throw(/requires middleware function.*number/) - router.use.bind(router, '/', null).should.throw(/requires middleware function.*Null/) - router.use.bind(router, '/', new Date()).should.throw(/requires middleware function.*Date/) + it('should reject string as middleware', function () { + var router = new Router() + assert.throws(function () { router.use('/', 'foo') }, /requires a middleware function but got a string/) + }) + + it('should reject number as middleware', function () { + var router = new Router() + assert.throws(function () { router.use('/', 42) }, /requires a middleware function but got a number/) + }) + + it('should reject null as middleware', function () { + var router = new Router() + assert.throws(function () { router.use('/', null) }, /requires a middleware function but got a Null/) + }) + + it('should reject Date as middleware', function () { + var router = new Router() + assert.throws(function () { router.use('/', new Date()) }, /requires a middleware function but got a Date/) }) it('should be called for any URL', function (done) { diff --git a/test/acceptance/auth.js b/test/acceptance/auth.js index 9a36ea45feb..d7838755a08 100644 --- a/test/acceptance/auth.js +++ b/test/acceptance/auth.js @@ -22,7 +22,7 @@ describe('auth', function(){ .expect(200, /
    tobi

    '); + assert.strictEqual(str, '

    tobi

    ') done(); }) }) it('should throw when the callback is missing', function(){ var app = express(); - (function(){ + assert.throws(function () { app.engine('.html', null); - }).should.throw('callback function required'); + }, /callback function required/) }) it('should work without leading "."', function(done){ @@ -43,11 +45,11 @@ describe('app', function(){ app.render('user.html', function(err, str){ if (err) return done(err); - str.should.equal('

    tobi

    '); + assert.strictEqual(str, '

    tobi

    ') done(); }) }) - + it('should work "view engine" setting', function(done){ var app = express(); @@ -58,11 +60,11 @@ describe('app', function(){ app.render('user', function(err, str){ if (err) return done(err); - str.should.equal('

    tobi

    '); + assert.strictEqual(str, '

    tobi

    ') done(); }) }) - + it('should work "view engine" with leading "."', function(done){ var app = express(); @@ -73,7 +75,7 @@ describe('app', function(){ app.render('user', function(err, str){ if (err) return done(err); - str.should.equal('

    tobi

    '); + assert.strictEqual(str, '

    tobi

    ') done(); }) }) diff --git a/test/app.head.js b/test/app.head.js index ed8499ce293..fabb98795ab 100644 --- a/test/app.head.js +++ b/test/app.head.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../'); var request = require('supertest'); @@ -26,7 +27,7 @@ describe('HEAD', function(){ }); request(app) - .get('/tobi') + .head('/tobi') .expect(200, function(err, res){ if (err) return done(err); var headers = res.headers; @@ -46,23 +47,20 @@ describe('HEAD', function(){ describe('app.head()', function(){ it('should override', function(done){ var app = express() - , called; app.head('/tobi', function(req, res){ - called = true; - res.end(''); + res.header('x-method', 'head') + res.end() }); app.get('/tobi', function(req, res){ - assert(0, 'should not call GET'); + res.header('x-method', 'get') res.send('tobi'); }); request(app) - .head('/tobi') - .expect(200, function(){ - assert(called); - done(); - }); + .head('/tobi') + .expect('x-method', 'head') + .expect(200, done) }) }) diff --git a/test/app.js b/test/app.js index 941d35ff1cc..6134717c33e 100644 --- a/test/app.js +++ b/test/app.js @@ -1,3 +1,4 @@ +'use strict' var assert = require('assert') var express = require('..') @@ -32,8 +33,8 @@ describe('app.parent', function(){ blog.use('/admin', blogAdmin); assert(!app.parent, 'app.parent'); - blog.parent.should.equal(app); - blogAdmin.parent.should.equal(blog); + assert.strictEqual(blog.parent, app) + assert.strictEqual(blogAdmin.parent, blog) }) }) @@ -48,10 +49,10 @@ describe('app.mountpath', function(){ app.use(fallback); blog.use('/admin', admin); - admin.mountpath.should.equal('/admin'); - app.mountpath.should.equal('/'); - blog.mountpath.should.equal('/blog'); - fallback.mountpath.should.equal('/'); + assert.strictEqual(admin.mountpath, '/admin') + assert.strictEqual(app.mountpath, '/') + assert.strictEqual(blog.mountpath, '/blog') + assert.strictEqual(fallback.mountpath, '/') }) }) @@ -76,35 +77,56 @@ describe('app.path()', function(){ app.use('/blog', blog); blog.use('/admin', blogAdmin); - app.path().should.equal(''); - blog.path().should.equal('/blog'); - blogAdmin.path().should.equal('/blog/admin'); + assert.strictEqual(app.path(), '') + assert.strictEqual(blog.path(), '/blog') + assert.strictEqual(blogAdmin.path(), '/blog/admin') }) }) describe('in development', function(){ + before(function () { + this.env = process.env.NODE_ENV + process.env.NODE_ENV = 'development' + }) + + after(function () { + process.env.NODE_ENV = this.env + }) + it('should disable "view cache"', function(){ - process.env.NODE_ENV = 'development'; var app = express(); - app.enabled('view cache').should.be.false; - process.env.NODE_ENV = 'test'; + assert.ok(!app.enabled('view cache')) }) }) describe('in production', function(){ + before(function () { + this.env = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + }) + + after(function () { + process.env.NODE_ENV = this.env + }) + it('should enable "view cache"', function(){ - process.env.NODE_ENV = 'production'; var app = express(); - app.enabled('view cache').should.be.true; - process.env.NODE_ENV = 'test'; + assert.ok(app.enabled('view cache')) }) }) describe('without NODE_ENV', function(){ + before(function () { + this.env = process.env.NODE_ENV + process.env.NODE_ENV = '' + }) + + after(function () { + process.env.NODE_ENV = this.env + }) + it('should default to development', function(){ - process.env.NODE_ENV = ''; var app = express(); - app.get('env').should.equal('development'); - process.env.NODE_ENV = 'test'; + assert.strictEqual(app.get('env'), 'development') }) }) diff --git a/test/app.listen.js b/test/app.listen.js index b6f68578934..08eeaaa63c2 100644 --- a/test/app.listen.js +++ b/test/app.listen.js @@ -1,15 +1,11 @@ +'use strict' var express = require('../') - , request = require('supertest'); describe('app.listen()', function(){ it('should wrap with an HTTP server', function(done){ var app = express(); - app.del('/tobi', function(req, res){ - res.end('deleted tobi!'); - }); - var server = app.listen(9999, function(){ server.close(); done(); diff --git a/test/app.locals.js b/test/app.locals.js index a8b022957a2..88f83c94831 100644 --- a/test/app.locals.js +++ b/test/app.locals.js @@ -1,17 +1,19 @@ +'use strict' +var assert = require('assert') var express = require('../') - , request = require('supertest'); +var should = require('should') describe('app', function(){ describe('.locals(obj)', function(){ it('should merge locals', function(){ var app = express(); - Object.keys(app.locals).should.eql(['settings']); + should(Object.keys(app.locals)).eql(['settings']) app.locals.user = 'tobi'; app.locals.age = 2; - Object.keys(app.locals).should.eql(['settings', 'user', 'age']); - app.locals.user.should.equal('tobi'); - app.locals.age.should.equal(2); + should(Object.keys(app.locals)).eql(['settings', 'user', 'age']) + assert.strictEqual(app.locals.user, 'tobi') + assert.strictEqual(app.locals.age, 2) }) }) @@ -20,8 +22,8 @@ describe('app', function(){ var app = express(); app.set('title', 'House of Manny'); var obj = app.locals.settings; - obj.should.have.property('env', 'test'); - obj.should.have.property('title', 'House of Manny'); + should(obj).have.property('env', 'test') + should(obj).have.property('title', 'House of Manny') }) }) }) diff --git a/test/app.options.js b/test/app.options.js index 9c88abafe5e..fdfd38c8a28 100644 --- a/test/app.options.js +++ b/test/app.options.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); diff --git a/test/app.param.js b/test/app.param.js index c7a375418cd..8893851f9d5 100644 --- a/test/app.param.js +++ b/test/app.param.js @@ -1,4 +1,6 @@ +'use strict' +var assert = require('assert') var express = require('../') , request = require('supertest'); @@ -8,7 +10,7 @@ describe('app', function(){ var app = express(); app.param(function(name, regexp){ - if (Object.prototype.toString.call(regexp) == '[object RegExp]') { // See #1557 + if (Object.prototype.toString.call(regexp) === '[object RegExp]') { // See #1557 return function(req, res, next, val){ var captures; if (captures = regexp.exec(String(val))) { @@ -40,7 +42,7 @@ describe('app', function(){ it('should fail if not given fn', function(){ var app = express(); - app.param.bind(app, ':name', 'bob').should.throw(); + assert.throws(app.param.bind(app, ':name', 'bob')) }) }) @@ -57,24 +59,22 @@ describe('app', function(){ app.get('/post/:id', function(req, res){ var id = req.params.id; - id.should.be.a.Number; - res.send('' + id); + res.send((typeof id) + ':' + id) }); app.get('/user/:uid', function(req, res){ var id = req.params.id; - id.should.be.a.Number; - res.send('' + id); + res.send((typeof id) + ':' + id) }); request(app) - .get('/user/123') - .expect(200, '123', function (err) { - if (err) return done(err) - request(app) - .get('/post/123') - .expect('123', done); - }) + .get('/user/123') + .expect(200, 'number:123', function (err) { + if (err) return done(err) + request(app) + .get('/post/123') + .expect('number:123', done) + }) }) }) @@ -91,13 +91,12 @@ describe('app', function(){ app.get('/user/:id', function(req, res){ var id = req.params.id; - id.should.be.a.Number; - res.send('' + id); + res.send((typeof id) + ':' + id) }); request(app) - .get('/user/123') - .expect('123', done); + .get('/user/123') + .expect(200, 'number:123', done) }) it('should only call once per request', function(done) { @@ -185,7 +184,7 @@ describe('app', function(){ }); app.param('user', function(req, res, next, user) { - next(new Error('invalid invokation')); + next(new Error('invalid invocation')) }); app.post('/:user', function(req, res, next) { diff --git a/test/app.render.js b/test/app.render.js index 729b1c97cc8..9d202acfdda 100644 --- a/test/app.render.js +++ b/test/app.render.js @@ -1,3 +1,4 @@ +'use strict' var assert = require('assert') var express = require('..'); @@ -13,7 +14,7 @@ describe('app', function(){ app.render(path.join(__dirname, 'fixtures', 'user.tmpl'), function (err, str) { if (err) return done(err); - str.should.equal('

    tobi

    '); + assert.strictEqual(str, '

    tobi

    ') done(); }) }) @@ -26,7 +27,7 @@ describe('app', function(){ app.render(path.join(__dirname, 'fixtures', 'user'), function (err, str) { if (err) return done(err); - str.should.equal('

    tobi

    '); + assert.strictEqual(str, '

    tobi

    ') done(); }) }) @@ -39,7 +40,7 @@ describe('app', function(){ app.render('user.tmpl', function (err, str) { if (err) return done(err); - str.should.equal('

    tobi

    '); + assert.strictEqual(str, '

    tobi

    ') done(); }) }) @@ -52,7 +53,7 @@ describe('app', function(){ app.render('blog/post', function (err, str) { if (err) return done(err); - str.should.equal('

    blog post

    '); + assert.strictEqual(str, '

    blog post

    ') done(); }) }) @@ -72,8 +73,8 @@ describe('app', function(){ app.set('view', View); app.render('something', function(err, str){ - err.should.be.ok; - err.message.should.equal('err!'); + assert.ok(err) + assert.strictEqual(err.message, 'err!') done(); }) }) @@ -97,12 +98,10 @@ describe('app', function(){ app.set('views', path.join(__dirname, 'fixtures')) - app.render('user.tmpl', function (err, str) { - // nextTick to prevent cyclic - process.nextTick(function(){ - err.message.should.match(/Cannot read property '[^']+' of undefined/); - done(); - }); + app.render('user.tmpl', function (err) { + assert.ok(err) + assert.equal(err.name, 'RenderError') + done() }) }) }) @@ -115,7 +114,7 @@ describe('app', function(){ app.render('email.tmpl', function (err, str) { if (err) return done(err); - str.should.equal('

    This is an email

    '); + assert.strictEqual(str, '

    This is an email

    ') done(); }) }) @@ -130,7 +129,7 @@ describe('app', function(){ app.render('email', function(err, str){ if (err) return done(err); - str.should.equal('

    This is an email

    '); + assert.strictEqual(str, '

    This is an email

    ') done(); }) }) @@ -145,7 +144,7 @@ describe('app', function(){ app.render('user.tmpl', function (err, str) { if (err) return done(err); - str.should.equal('

    tobi

    '); + assert.strictEqual(str, '

    tobi

    ') done(); }) }) @@ -163,7 +162,7 @@ describe('app', function(){ app.render('user.tmpl', function (err, str) { if (err) return done(err); - str.should.equal('tobi'); + assert.strictEqual(str, 'tobi') done(); }) }) @@ -180,7 +179,7 @@ describe('app', function(){ app.render('name.tmpl', function (err, str) { if (err) return done(err); - str.should.equal('

    tobi

    '); + assert.strictEqual(str, '

    tobi

    ') done(); }) }) @@ -221,7 +220,7 @@ describe('app', function(){ app.render('something', function(err, str){ if (err) return done(err); - str.should.equal('abstract engine'); + assert.strictEqual(str, 'abstract engine') done(); }) }) @@ -247,12 +246,12 @@ describe('app', function(){ app.render('something', function(err, str){ if (err) return done(err); - count.should.equal(1); - str.should.equal('abstract engine'); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') app.render('something', function(err, str){ if (err) return done(err); - count.should.equal(2); - str.should.equal('abstract engine'); + assert.strictEqual(count, 2) + assert.strictEqual(str, 'abstract engine') done(); }) }) @@ -277,12 +276,12 @@ describe('app', function(){ app.render('something', function(err, str){ if (err) return done(err); - count.should.equal(1); - str.should.equal('abstract engine'); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') app.render('something', function(err, str){ if (err) return done(err); - count.should.equal(1); - str.should.equal('abstract engine'); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') done(); }) }) @@ -300,7 +299,7 @@ describe('app', function(){ app.render('user.tmpl', { user: user }, function (err, str) { if (err) return done(err); - str.should.equal('

    tobi

    '); + assert.strictEqual(str, '

    tobi

    ') done(); }) }) @@ -313,7 +312,7 @@ describe('app', function(){ app.render('user.tmpl', {}, function (err, str) { if (err) return done(err); - str.should.equal('

    tobi

    '); + assert.strictEqual(str, '

    tobi

    ') done(); }) }) @@ -327,7 +326,7 @@ describe('app', function(){ app.render('user.tmpl', { user: jane }, function (err, str) { if (err) return done(err); - str.should.equal('

    jane

    '); + assert.strictEqual(str, '

    jane

    ') done(); }) }) @@ -352,12 +351,12 @@ describe('app', function(){ app.render('something', {cache: true}, function(err, str){ if (err) return done(err); - count.should.equal(1); - str.should.equal('abstract engine'); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') app.render('something', {cache: true}, function(err, str){ if (err) return done(err); - count.should.equal(1); - str.should.equal('abstract engine'); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') done(); }) }) diff --git a/test/app.request.js b/test/app.request.js index 728043a5a34..4930af84c25 100644 --- a/test/app.request.js +++ b/test/app.request.js @@ -1,4 +1,6 @@ +'use strict' +var after = require('after') var express = require('../') , request = require('supertest'); @@ -19,5 +21,123 @@ describe('app', function(){ .get('/foo?name=tobi') .expect('name=tobi', done); }) + + it('should only extend for the referenced app', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.request.foobar = function () { + return 'tobi' + } + + app1.get('/', function (req, res) { + res.send(req.foobar()) + }) + + app2.get('/', function (req, res) { + res.send(req.foobar()) + }) + + request(app1) + .get('/') + .expect(200, 'tobi', cb) + + request(app2) + .get('/') + .expect(500, /(?:not a function|has no method)/, cb) + }) + + it('should inherit to sub apps', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.request.foobar = function () { + return 'tobi' + } + + app1.use('/sub', app2) + + app1.get('/', function (req, res) { + res.send(req.foobar()) + }) + + app2.get('/', function (req, res) { + res.send(req.foobar()) + }) + + request(app1) + .get('/') + .expect(200, 'tobi', cb) + + request(app1) + .get('/sub') + .expect(200, 'tobi', cb) + }) + + it('should allow sub app to override', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.request.foobar = function () { + return 'tobi' + } + + app2.request.foobar = function () { + return 'loki' + } + + app1.use('/sub', app2) + + app1.get('/', function (req, res) { + res.send(req.foobar()) + }) + + app2.get('/', function (req, res) { + res.send(req.foobar()) + }) + + request(app1) + .get('/') + .expect(200, 'tobi', cb) + + request(app1) + .get('/sub') + .expect(200, 'loki', cb) + }) + + it('should not pollute parent app', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.request.foobar = function () { + return 'tobi' + } + + app2.request.foobar = function () { + return 'loki' + } + + app1.use('/sub', app2) + + app1.get('/sub/foo', function (req, res) { + res.send(req.foobar()) + }) + + app2.get('/', function (req, res) { + res.send(req.foobar()) + }) + + request(app1) + .get('/sub') + .expect(200, 'loki', cb) + + request(app1) + .get('/sub/foo') + .expect(200, 'tobi', cb) + }) }) }) diff --git a/test/app.response.js b/test/app.response.js index c6ea77c820a..5fb69f6275a 100644 --- a/test/app.response.js +++ b/test/app.response.js @@ -1,4 +1,6 @@ +'use strict' +var after = require('after') var express = require('../') , request = require('supertest'); @@ -20,25 +22,122 @@ describe('app', function(){ .expect('HEY', done); }) - it('should not be influenced by other app protos', function(done){ - var app = express() - , app2 = express(); + it('should only extend for the referenced app', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) - app.response.shout = function(str){ - this.send(str.toUpperCase()); - }; + app1.response.shout = function (str) { + this.send(str.toUpperCase()) + } - app2.response.shout = function(str){ - this.send(str); - }; + app1.get('/', function (req, res) { + res.shout('foo') + }) - app.use(function(req, res){ - res.shout('hey'); - }); + app2.get('/', function (req, res) { + res.shout('foo') + }) - request(app) - .get('/') - .expect('HEY', done); + request(app1) + .get('/') + .expect(200, 'FOO', cb) + + request(app2) + .get('/') + .expect(500, /(?:not a function|has no method)/, cb) + }) + + it('should inherit to sub apps', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.response.shout = function (str) { + this.send(str.toUpperCase()) + } + + app1.use('/sub', app2) + + app1.get('/', function (req, res) { + res.shout('foo') + }) + + app2.get('/', function (req, res) { + res.shout('foo') + }) + + request(app1) + .get('/') + .expect(200, 'FOO', cb) + + request(app1) + .get('/sub') + .expect(200, 'FOO', cb) + }) + + it('should allow sub app to override', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.response.shout = function (str) { + this.send(str.toUpperCase()) + } + + app2.response.shout = function (str) { + this.send(str + '!') + } + + app1.use('/sub', app2) + + app1.get('/', function (req, res) { + res.shout('foo') + }) + + app2.get('/', function (req, res) { + res.shout('foo') + }) + + request(app1) + .get('/') + .expect(200, 'FOO', cb) + + request(app1) + .get('/sub') + .expect(200, 'foo!', cb) + }) + + it('should not pollute parent app', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.response.shout = function (str) { + this.send(str.toUpperCase()) + } + + app2.response.shout = function (str) { + this.send(str + '!') + } + + app1.use('/sub', app2) + + app1.get('/sub/foo', function (req, res) { + res.shout('foo') + }) + + app2.get('/', function (req, res) { + res.shout('foo') + }) + + request(app1) + .get('/sub') + .expect(200, 'foo!', cb) + + request(app1) + .get('/sub/foo') + .expect(200, 'FOO', cb) }) }) }) diff --git a/test/app.route.js b/test/app.route.js index 75e5e0b8421..eaf8a120515 100644 --- a/test/app.route.js +++ b/test/app.route.js @@ -1,3 +1,5 @@ +'use strict' + var express = require('../'); var request = require('supertest'); diff --git a/test/app.router.js b/test/app.router.js index 95680f9139c..3069a22c772 100644 --- a/test/app.router.js +++ b/test/app.router.js @@ -1,3 +1,4 @@ +'use strict' var after = require('after'); var express = require('../') @@ -39,24 +40,19 @@ describe('app.router', function(){ it('should include ' + method.toUpperCase(), function(done){ var app = express(); - var calls = []; app[method]('/foo', function(req, res){ - if ('head' == method) { - res.end(); - } else { - res.end(method); - } + res.send(method) }); request(app) [method]('/foo') - .expect('head' == method ? '' : method, done); + .expect(200, done) }) it('should reject numbers for app.' + method, function(){ var app = express(); - app[method].bind(app, '/', 3).should.throw(/Number/); + assert.throws(app[method].bind(app, '/', 3), /Number/) }) }); @@ -157,15 +153,12 @@ describe('app.router', function(){ app.use(function(req, res, next){ calls.push('after'); - res.end(); + res.json(calls) }); request(app) .get('/') - .end(function(res){ - calls.should.eql(['before', 'GET /', 'after']) - done(); - }) + .expect(200, ['before', 'GET /', 'after'], done) }) describe('when given a regexp', function(){ @@ -194,6 +187,35 @@ describe('app.router', function(){ .get('/user/10/edit') .expect('editing user 10', done); }) + + it('should ensure regexp matches path prefix', function (done) { + var app = express() + var p = [] + + app.use(/\/api.*/, function (req, res, next) { + p.push('a') + next() + }) + app.use(/api/, function (req, res, next) { + p.push('b') + next() + }) + app.use(/\/test/, function (req, res, next) { + p.push('c') + next() + }) + app.use(function (req, res) { + res.end() + }) + + request(app) + .get('/test/api/1234') + .expect(200, function (err) { + if (err) return done(err) + assert.deepEqual(p, ['c']) + done() + }) + }) }) describe('case sensitivity', function(){ @@ -575,7 +597,7 @@ describe('app.router', function(){ .expect('/user/tobi.json', done) }) - it('should decore the capture', function (done) { + it('should decode the capture', function (done) { var app = express() app.get('*', function (req, res) { @@ -615,18 +637,19 @@ describe('app.router', function(){ it('should work cross-segment', function(done){ var app = express(); + var cb = after(2, done) app.get('/api*', function(req, res){ res.send(req.params[0]); }); request(app) - .get('/api') - .expect('', function(){ - request(app) + .get('/api') + .expect(200, '', cb) + + request(app) .get('/api/hey') - .expect('/hey', done); - }); + .expect(200, '/hey', cb) }) it('should allow naming', function(done){ @@ -842,36 +865,38 @@ describe('app.router', function(){ describe('.:name', function(){ it('should denote a format', function(done){ var app = express(); + var cb = after(2, done) app.get('/:name.:format', function(req, res){ res.end(req.params.name + ' as ' + req.params.format); }); request(app) - .get('/foo.json') - .expect('foo as json', function(){ - request(app) + .get('/foo.json') + .expect(200, 'foo as json', cb) + + request(app) .get('/foo') - .expect(404, done); - }); + .expect(404, cb) }) }) describe('.:name?', function(){ it('should denote an optional format', function(done){ var app = express(); + var cb = after(2, done) app.get('/:name.:format?', function(req, res){ res.end(req.params.name + ' as ' + (req.params.format || 'html')); }); request(app) - .get('/foo') - .expect('foo as html', function(){ - request(app) + .get('/foo') + .expect(200, 'foo as html', cb) + + request(app) .get('/foo.json') - .expect('foo as json', done); - }); + .expect(200, 'foo as json', done) }) }) @@ -896,15 +921,12 @@ describe('app.router', function(){ app.get('/foo', function(req, res, next){ calls.push('/foo 2'); - res.end('done'); + res.json(calls) }); request(app) .get('/foo') - .expect('done', function(){ - calls.should.eql(['/foo/:bar?', '/foo', '/foo 2']); - done(); - }) + .expect(200, ['/foo/:bar?', '/foo', '/foo 2'], done) }) }) @@ -987,15 +1009,15 @@ describe('app.router', function(){ }); app.use(function(err, req, res, next){ - res.end(err.message); + res.json({ + calls: calls, + error: err.message + }) }) request(app) .get('/foo') - .expect('fail', function(){ - calls.should.eql(['/foo/:bar?', '/foo']); - done(); - }) + .expect(200, { calls: ['/foo/:bar?', '/foo'], error: 'fail' }, done) }) it('should call handler in same route, if exists', function(done){ @@ -1084,6 +1106,6 @@ describe('app.router', function(){ it('should be chainable', function(){ var app = express(); - app.get('/', function(){}).should.equal(app); + assert.strictEqual(app.get('/', function () {}), app) }) }) diff --git a/test/app.routes.error.js b/test/app.routes.error.js index 7c49d50ffe2..56081b31127 100644 --- a/test/app.routes.error.js +++ b/test/app.routes.error.js @@ -1,3 +1,6 @@ +'use strict' + +var assert = require('assert') var express = require('../') , request = require('supertest'); @@ -34,20 +37,20 @@ describe('app', function(){ next(); }, function(err, req, res, next){ b = true; - err.message.should.equal('fabricated error'); + assert.strictEqual(err.message, 'fabricated error') next(err); }, function(err, req, res, next){ c = true; - err.message.should.equal('fabricated error'); + assert.strictEqual(err.message, 'fabricated error') next(); }, function(err, req, res, next){ d = true; next(); }, function(req, res){ - a.should.be.false; - b.should.be.true; - c.should.be.true; - d.should.be.false; + assert.ok(!a) + assert.ok(b) + assert.ok(c) + assert.ok(!d) res.send(204); }); diff --git a/test/app.use.js b/test/app.use.js index b2031e4c56c..fd9b1751a31 100644 --- a/test/app.use.js +++ b/test/app.use.js @@ -1,5 +1,7 @@ +'use strict' var after = require('after'); +var assert = require('assert') var express = require('..'); var request = require('supertest'); @@ -9,7 +11,7 @@ describe('app', function(){ , app = express(); blog.on('mount', function(arg){ - arg.should.equal(app); + assert.strictEqual(arg, app) done(); }); @@ -36,6 +38,7 @@ describe('app', function(){ var blog = express() , forum = express() , app = express(); + var cb = after(2, done) blog.get('/', function(req, res){ res.end('blog'); @@ -49,12 +52,12 @@ describe('app', function(){ app.use('/forum', forum); request(app) - .get('/blog') - .expect('blog', function(){ - request(app) + .get('/blog') + .expect(200, 'blog', cb) + + request(app) .get('/forum') - .expect('forum', done); - }); + .expect(200, 'forum', done) }) it('should set the child\'s .parent', function(){ @@ -62,7 +65,7 @@ describe('app', function(){ , app = express(); app.use('/blog', blog); - blog.parent.should.equal(app); + assert.strictEqual(blog.parent, app) }) it('should support dynamic routes', function(done){ @@ -101,11 +104,11 @@ describe('app', function(){ }); blog.once('mount', function (parent) { - parent.should.equal(app); + assert.strictEqual(parent, app) cb(); }); other.once('mount', function (parent) { - parent.should.equal(app); + assert.strictEqual(parent, app) cb(); }); @@ -253,17 +256,29 @@ describe('app', function(){ }) describe('.use(path, middleware)', function(){ - it('should reject missing functions', function () { - var app = express(); - app.use.bind(app, '/').should.throw(/requires middleware function/); + it('should require middleware', function () { + var app = express() + assert.throws(function () { app.use('/') }, /requires a middleware function/) }) - it('should reject non-functions as middleware', function () { - var app = express(); - app.use.bind(app, '/', 'hi').should.throw(/requires middleware function.*string/); - app.use.bind(app, '/', 5).should.throw(/requires middleware function.*number/); - app.use.bind(app, '/', null).should.throw(/requires middleware function.*Null/); - app.use.bind(app, '/', new Date()).should.throw(/requires middleware function.*Date/); + it('should reject string as middleware', function () { + var app = express() + assert.throws(function () { app.use('/', 'foo') }, /requires a middleware function but got a string/) + }) + + it('should reject number as middleware', function () { + var app = express() + assert.throws(function () { app.use('/', 42) }, /requires a middleware function but got a number/) + }) + + it('should reject null as middleware', function () { + var app = express() + assert.throws(function () { app.use('/', null) }, /requires a middleware function but got a Null/) + }) + + it('should reject Date as middleware', function () { + var app = express() + assert.throws(function () { app.use('/', new Date()) }, /requires a middleware function but got a Date/) }) it('should strip path from req.url', function (done) { diff --git a/test/config.js b/test/config.js index e298e76a5c5..8386a4471c3 100644 --- a/test/config.js +++ b/test/config.js @@ -1,3 +1,4 @@ +'use strict' var assert = require('assert'); var express = require('..'); @@ -49,7 +50,7 @@ describe('config', function () { var app = express(); assert.strictEqual(app.get('foo'), undefined); }) - + it('should otherwise return the value', function(){ var app = express(); app.set('foo', 'bar'); @@ -125,7 +126,7 @@ describe('config', function () { assert.strictEqual(app.get('tobi'), true); }) }) - + describe('.disable()', function(){ it('should set the value to false', function(){ var app = express(); @@ -133,26 +134,26 @@ describe('config', function () { assert.strictEqual(app.get('tobi'), false); }) }) - + describe('.enabled()', function(){ it('should default to false', function(){ var app = express(); assert.strictEqual(app.enabled('foo'), false); }) - + it('should return true when set', function(){ var app = express(); app.set('foo', 'bar'); assert.strictEqual(app.enabled('foo'), true); }) }) - + describe('.disabled()', function(){ it('should default to true', function(){ var app = express(); assert.strictEqual(app.disabled('foo'), true); }) - + it('should return false when set', function(){ var app = express(); app.set('foo', 'bar'); diff --git a/test/exports.js b/test/exports.js index d34a7b1cf3e..98d79365943 100644 --- a/test/exports.js +++ b/test/exports.js @@ -1,23 +1,50 @@ +'use strict' +var assert = require('assert') var express = require('../'); var request = require('supertest'); var should = require('should'); describe('exports', function(){ it('should expose Router', function(){ - express.Router.should.be.a.Function; + express.Router.should.be.a.Function() + }) + + it('should expose json middleware', function () { + assert.equal(typeof express.json, 'function') + assert.equal(express.json.length, 1) + }) + + it('should expose raw middleware', function () { + assert.equal(typeof express.raw, 'function') + assert.equal(express.raw.length, 1) + }) + + it('should expose static middleware', function () { + assert.equal(typeof express.static, 'function') + assert.equal(express.static.length, 2) + }) + + it('should expose text middleware', function () { + assert.equal(typeof express.text, 'function') + assert.equal(express.text.length, 1) + }) + + it('should expose urlencoded middleware', function () { + assert.equal(typeof express.urlencoded, 'function') + assert.equal(express.urlencoded.length, 1) }) it('should expose the application prototype', function(){ - express.application.set.should.be.a.Function; + express.application.set.should.be.a.Function() }) it('should expose the request prototype', function(){ - express.request.accepts.should.be.a.Function; + express.request.accepts.should.be.a.Function() }) it('should expose the response prototype', function(){ - express.response.send.should.be.a.Function; + express.response.send.should.be.a.Function() }) it('should permit modifying the .application prototype', function(){ diff --git a/test/express.json.js b/test/express.json.js new file mode 100644 index 00000000000..53a39565a9b --- /dev/null +++ b/test/express.json.js @@ -0,0 +1,665 @@ +'use strict' + +var assert = require('assert') +var Buffer = require('safe-buffer').Buffer +var express = require('..') +var request = require('supertest') + +describe('express.json()', function () { + it('should parse JSON', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should handle Content-Length: 0', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .set('Content-Length', '0') + .expect(200, '{}', done) + }) + + it('should handle empty message-body', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .set('Transfer-Encoding', 'chunked') + .expect(200, '{}', done) + }) + + it('should handle no message-body', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .unset('Transfer-Encoding') + .expect(200, '{}', done) + }) + + it('should 400 when invalid content-length', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.headers['content-length'] = '20' // bad length + next() + }) + + app.use(express.json()) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"str":') + .expect(400, /content length/, done) + }) + + it('should handle duplicated middleware', function (done) { + var app = express() + + app.use(express.json()) + app.use(express.json()) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + describe('when JSON is invalid', function () { + before(function () { + this.app = createApp() + }) + + it('should 400 for bad token', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{:') + .expect(400, parseError('{:'), done) + }) + + it('should 400 for incomplete', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user"') + .expect(400, parseError('{"user"'), done) + }) + + it('should error with type = "entity.parse.failed"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'type') + .send(' {"user"') + .expect(400, 'entity.parse.failed', done) + }) + + it('should include original body on error object', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'body') + .send(' {"user"') + .expect(400, ' {"user"', done) + }) + }) + + describe('with limit option', function () { + it('should 413 when over limit with Content-Length', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: '1kb' })) + .post('/') + .set('Content-Type', 'application/json') + .set('Content-Length', '1034') + .send(JSON.stringify({ str: buf.toString() })) + .expect(413, done) + }) + + it('should error with type = "entity.too.large"', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: '1kb' })) + .post('/') + .set('Content-Type', 'application/json') + .set('Content-Length', '1034') + .set('X-Error-Property', 'type') + .send(JSON.stringify({ str: buf.toString() })) + .expect(413, 'entity.too.large', done) + }) + + it('should 413 when over limit with chunked encoding', function (done) { + var buf = Buffer.alloc(1024, '.') + var server = createApp({ limit: '1kb' }) + var test = request(server).post('/') + test.set('Content-Type', 'application/json') + test.set('Transfer-Encoding', 'chunked') + test.write('{"str":') + test.write('"' + buf.toString() + '"}') + test.expect(413, done) + }) + + it('should accept number of bytes', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: 1024 })) + .post('/') + .set('Content-Type', 'application/json') + .send(JSON.stringify({ str: buf.toString() })) + .expect(413, done) + }) + + it('should not change when options altered', function (done) { + var buf = Buffer.alloc(1024, '.') + var options = { limit: '1kb' } + var server = createApp(options) + + options.limit = '100kb' + + request(server) + .post('/') + .set('Content-Type', 'application/json') + .send(JSON.stringify({ str: buf.toString() })) + .expect(413, done) + }) + + it('should not hang response', function (done) { + var buf = Buffer.alloc(10240, '.') + var server = createApp({ limit: '8kb' }) + var test = request(server).post('/') + test.set('Content-Type', 'application/json') + test.write(buf) + test.write(buf) + test.write(buf) + test.expect(413, done) + }) + }) + + describe('with inflate option', function () { + describe('when false', function () { + before(function () { + this.app = createApp({ inflate: false }) + }) + + it('should not accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(415, 'content encoding unsupported', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ inflate: true }) + }) + + it('should accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + }) + }) + + describe('with strict option', function () { + describe('when undefined', function () { + before(function () { + this.app = createApp() + }) + + it('should 400 on primitives', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('true') + .expect(400, parseError('#rue').replace('#', 't'), done) + }) + }) + + describe('when false', function () { + before(function () { + this.app = createApp({ strict: false }) + }) + + it('should parse primitives', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('true') + .expect(200, 'true', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ strict: true }) + }) + + it('should not parse primitives', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('true') + .expect(400, parseError('#rue').replace('#', 't'), done) + }) + + it('should not parse primitives with leading whitespaces', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send(' true') + .expect(400, parseError(' #rue').replace('#', 't'), done) + }) + + it('should allow leading whitespaces in JSON', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send(' { "user": "tobi" }') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should error with type = "entity.parse.failed"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'type') + .send('true') + .expect(400, 'entity.parse.failed', done) + }) + + it('should include correct message in stack trace', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'stack') + .send('true') + .expect(400) + .expect(shouldContainInBody(parseError('#rue').replace('#', 't'))) + .end(done) + }) + }) + }) + + describe('with type option', function () { + describe('when "application/vnd.api+json"', function () { + before(function () { + this.app = createApp({ type: 'application/vnd.api+json' }) + }) + + it('should parse JSON for custom type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/vnd.api+json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should ignore standard type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{}', done) + }) + }) + + describe('when ["application/json", "application/vnd.api+json"]', function () { + before(function () { + this.app = createApp({ + type: ['application/json', 'application/vnd.api+json'] + }) + }) + + it('should parse JSON for "application/json"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should parse JSON for "application/vnd.api+json"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/vnd.api+json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should ignore "application/x-json"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-json') + .send('{"user":"tobi"}') + .expect(200, '{}', done) + }) + }) + + describe('when a function', function () { + it('should parse when truthy value returned', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return req.headers['content-type'] === 'application/vnd.api+json' + } + + request(app) + .post('/') + .set('Content-Type', 'application/vnd.api+json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should work without content-type', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return true + } + + var test = request(app).post('/') + test.write('{"user":"tobi"}') + test.expect(200, '{"user":"tobi"}', done) + }) + + it('should not invoke without a body', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + throw new Error('oops!') + } + + request(app) + .get('/') + .expect(404, done) + }) + }) + }) + + describe('with verify option', function () { + it('should assert value if function', function () { + assert.throws(createApp.bind(null, { verify: 'lol' }), + /TypeError: option verify must be function/) + }) + + it('should error from verify', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('["tobi"]') + .expect(403, 'no arrays', done) + }) + + it('should error with type = "entity.verify.failed"', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'type') + .send('["tobi"]') + .expect(403, 'entity.verify.failed', done) + }) + + it('should allow custom codes', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] !== 0x5b) return + var err = new Error('no arrays') + err.status = 400 + throw err + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('["tobi"]') + .expect(400, 'no arrays', done) + }) + + it('should allow custom type', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] !== 0x5b) return + var err = new Error('no arrays') + err.type = 'foo.bar' + throw err + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'type') + .send('["tobi"]') + .expect(403, 'foo.bar', done) + }) + + it('should include original body on error object', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'body') + .send('["tobi"]') + .expect(403, '["tobi"]', done) + }) + + it('should allow pass-through', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should work with different charsets', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/json; charset=utf-16') + test.write(Buffer.from('feff007b0022006e0061006d00650022003a00228bba0022007d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should 415 on unknown charset prior to verify', function (done) { + var app = createApp({ verify: function (req, res, buf) { + throw new Error('unexpected verify call') + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/json; charset=x-bogus') + test.write(Buffer.from('00000000', 'hex')) + test.expect(415, 'unsupported charset "X-BOGUS"', done) + }) + }) + + describe('charset', function () { + before(function () { + this.app = createApp() + }) + + it('should parse utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=utf-8') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should parse utf-16', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=utf-16') + test.write(Buffer.from('feff007b0022006e0061006d00650022003a00228bba0022007d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should parse when content-length != char length', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=utf-8') + test.set('Content-Length', '13') + test.write(Buffer.from('7b2274657374223a22c3a5227d', 'hex')) + test.expect(200, '{"test":"å"}', done) + }) + + it('should default to utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should fail on unknown charset', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=koi8-r') + test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex')) + test.expect(415, 'unsupported charset "KOI8-R"', done) + }) + + it('should error with type = "charset.unsupported"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=koi8-r') + test.set('X-Error-Property', 'type') + test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex')) + test.expect(415, 'charset.unsupported', done) + }) + }) + + describe('encoding', function () { + before(function () { + this.app = createApp({ limit: '1kb' }) + }) + + it('should parse without encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support identity encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'identity') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support gzip encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support deflate encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'deflate') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('789cab56ca4bcc4d55b2527ab16e97522d00274505ac', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should be case-insensitive', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'GZIP') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should 415 on unknown encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, 'unsupported content encoding "nulls"', done) + }) + + it('should error with type = "encoding.unsupported"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'application/json') + test.set('X-Error-Property', 'type') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, 'encoding.unsupported', done) + }) + + it('should 400 on malformed encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56cc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(400, done) + }) + + it('should 413 when inflated value exceeds limit', function (done) { + // gzip'd data exceeds 1kb, but deflated below 1kb + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bedc1010d000000c2a0f74f6d0f071400000000000000', 'hex')) + test.write(Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex')) + test.write(Buffer.from('0000000000000000004f0625b3b71650c30000', 'hex')) + test.expect(413, done) + }) + }) +}) + +function createApp (options) { + var app = express() + + app.use(express.json(options)) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send(String(err[req.headers['x-error-property'] || 'message'])) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + return app +} + +function parseError (str) { + try { + JSON.parse(str); throw new SyntaxError('strict violation') + } catch (e) { + return e.message + } +} + +function shouldContainInBody (str) { + return function (res) { + assert.ok(res.text.indexOf(str) !== -1, + 'expected \'' + res.text + '\' to contain \'' + str + '\'') + } +} diff --git a/test/express.raw.js b/test/express.raw.js new file mode 100644 index 00000000000..cbd0736e7cb --- /dev/null +++ b/test/express.raw.js @@ -0,0 +1,388 @@ +'use strict' + +var assert = require('assert') +var Buffer = require('safe-buffer').Buffer +var express = require('..') +var request = require('supertest') + +describe('express.raw()', function () { + before(function () { + this.app = createApp() + }) + + it('should parse application/octet-stream', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is tobi') + .expect(200, { buf: '746865207573657220697320746f6269' }, done) + }) + + it('should 400 when invalid content-length', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.headers['content-length'] = '20' // bad length + next() + }) + + app.use(express.raw()) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('stuff') + .expect(400, /content length/, done) + }) + + it('should handle Content-Length: 0', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .set('Content-Length', '0') + .expect(200, { buf: '' }, done) + }) + + it('should handle empty message-body', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .set('Transfer-Encoding', 'chunked') + .send('') + .expect(200, { buf: '' }, done) + }) + + it('should handle duplicated middleware', function (done) { + var app = express() + + app.use(express.raw()) + app.use(express.raw()) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is tobi') + .expect(200, { buf: '746865207573657220697320746f6269' }, done) + }) + + describe('with limit option', function () { + it('should 413 when over limit with Content-Length', function (done) { + var buf = Buffer.alloc(1028, '.') + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.set('Content-Length', '1028') + test.write(buf) + test.expect(413, done) + }) + + it('should 413 when over limit with chunked encoding', function (done) { + var buf = Buffer.alloc(1028, '.') + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.set('Transfer-Encoding', 'chunked') + test.write(buf) + test.expect(413, done) + }) + + it('should accept number of bytes', function (done) { + var buf = Buffer.alloc(1028, '.') + var app = createApp({ limit: 1024 }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(buf) + test.expect(413, done) + }) + + it('should not change when options altered', function (done) { + var buf = Buffer.alloc(1028, '.') + var options = { limit: '1kb' } + var app = createApp(options) + + options.limit = '100kb' + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(buf) + test.expect(413, done) + }) + + it('should not hang response', function (done) { + var buf = Buffer.alloc(10240, '.') + var app = createApp({ limit: '8kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(buf) + test.write(buf) + test.write(buf) + test.expect(413, done) + }) + }) + + describe('with inflate option', function () { + describe('when false', function () { + before(function () { + this.app = createApp({ inflate: false }) + }) + + it('should not accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(415, 'content encoding unsupported', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ inflate: true }) + }) + + it('should accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + }) + }) + + describe('with type option', function () { + describe('when "application/vnd+octets"', function () { + before(function () { + this.app = createApp({ type: 'application/vnd+octets' }) + }) + + it('should parse for custom type', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/vnd+octets') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should ignore standard type', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, '{}', done) + }) + }) + + describe('when ["application/octet-stream", "application/vnd+octets"]', function () { + before(function () { + this.app = createApp({ + type: ['application/octet-stream', 'application/vnd+octets'] + }) + }) + + it('should parse "application/octet-stream"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should parse "application/vnd+octets"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/vnd+octets') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should ignore "application/x-foo"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-foo') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, '{}', done) + }) + }) + + describe('when a function', function () { + it('should parse when truthy value returned', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return req.headers['content-type'] === 'application/vnd.octet' + } + + var test = request(app).post('/') + test.set('Content-Type', 'application/vnd.octet') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should work without content-type', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return true + } + + var test = request(app).post('/') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should not invoke without a body', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + throw new Error('oops!') + } + + request(app) + .get('/') + .expect(404, done) + }) + }) + }) + + describe('with verify option', function () { + it('should assert value is function', function () { + assert.throws(createApp.bind(null, { verify: 'lol' }), + /TypeError: option verify must be function/) + }) + + it('should error from verify', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x00) throw new Error('no leading null') + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(403, 'no leading null', done) + }) + + it('should allow custom codes', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] !== 0x00) return + var err = new Error('no leading null') + err.status = 400 + throw err + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(400, 'no leading null', done) + }) + + it('should allow pass-through', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x00) throw new Error('no leading null') + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('0102', 'hex')) + test.expect(200, { buf: '0102' }, done) + }) + }) + + describe('charset', function () { + before(function () { + this.app = createApp() + }) + + it('should ignore charset', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream; charset=utf-8') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, { buf: '6e616d6520697320e8aeba' }, done) + }) + }) + + describe('encoding', function () { + before(function () { + this.app = createApp({ limit: '10kb' }) + }) + + it('should parse without encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should support identity encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'identity') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should support gzip encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should support deflate encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'deflate') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('789ccb4bcc4db57db16e17001068042f', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should be case-insensitive', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'GZIP') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should fail on unknown encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, 'unsupported content encoding "nulls"', done) + }) + }) +}) + +function createApp (options) { + var app = express() + + app.use(express.raw(options)) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send(String(err[req.headers['x-error-property'] || 'message'])) + }) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + return app +} diff --git a/test/express.static.js b/test/express.static.js new file mode 100644 index 00000000000..245fd5929cc --- /dev/null +++ b/test/express.static.js @@ -0,0 +1,814 @@ +'use strict' + +var assert = require('assert') +var Buffer = require('safe-buffer').Buffer +var express = require('..') +var path = require('path') +var request = require('supertest') +var utils = require('./support/utils') + +var fixtures = path.join(__dirname, '/fixtures') +var relative = path.relative(process.cwd(), fixtures) + +var skipRelative = ~relative.indexOf('..') || path.resolve(relative) === relative + +describe('express.static()', function () { + describe('basic operations', function () { + before(function () { + this.app = createApp() + }) + + it('should require root path', function () { + assert.throws(express.static.bind(), /root path required/) + }) + + it('should require root path to be string', function () { + assert.throws(express.static.bind(null, 42), /root path.*string/) + }) + + it('should serve static files', function (done) { + request(this.app) + .get('/todo.txt') + .expect(200, '- groceries', done) + }) + + it('should support nesting', function (done) { + request(this.app) + .get('/users/tobi.txt') + .expect(200, 'ferret', done) + }) + + it('should set Content-Type', function (done) { + request(this.app) + .get('/todo.txt') + .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect(200, done) + }) + + it('should set Last-Modified', function (done) { + request(this.app) + .get('/todo.txt') + .expect('Last-Modified', /\d{2} \w{3} \d{4}/) + .expect(200, done) + }) + + it('should default max-age=0', function (done) { + request(this.app) + .get('/todo.txt') + .expect('Cache-Control', 'public, max-age=0') + .expect(200, done) + }) + + it('should support urlencoded pathnames', function (done) { + request(this.app) + .get('/%25%20of%20dogs.txt') + .expect(200, '20%', done) + }) + + it('should not choke on auth-looking URL', function (done) { + request(this.app) + .get('//todo@txt') + .expect(404, 'Not Found', done) + }) + + it('should support index.html', function (done) { + request(this.app) + .get('/users/') + .expect(200) + .expect('Content-Type', /html/) + .expect('

    tobi, loki, jane

    ', done) + }) + + it('should support ../', function (done) { + request(this.app) + .get('/users/../todo.txt') + .expect(200, '- groceries', done) + }) + + it('should support HEAD', function (done) { + request(this.app) + .head('/todo.txt') + .expect(200) + .expect(utils.shouldNotHaveBody()) + .end(done) + }) + + it('should skip POST requests', function (done) { + request(this.app) + .post('/todo.txt') + .expect(404, 'Not Found', done) + }) + + it('should support conditional requests', function (done) { + var app = this.app + + request(app) + .get('/todo.txt') + .end(function (err, res) { + if (err) throw err + request(app) + .get('/todo.txt') + .set('If-None-Match', res.headers.etag) + .expect(304, done) + }) + }) + + it('should support precondition checks', function (done) { + request(this.app) + .get('/todo.txt') + .set('If-Match', '"foo"') + .expect(412, done) + }) + + it('should serve zero-length files', function (done) { + request(this.app) + .get('/empty.txt') + .expect(200, '', done) + }) + + it('should ignore hidden files', function (done) { + request(this.app) + .get('/.name') + .expect(404, 'Not Found', done) + }) + }); + + (skipRelative ? describe.skip : describe)('current dir', function () { + before(function () { + this.app = createApp('.') + }) + + it('should be served with "."', function (done) { + var dest = relative.split(path.sep).join('/') + request(this.app) + .get('/' + dest + '/todo.txt') + .expect(200, '- groceries', done) + }) + }) + + describe('acceptRanges', function () { + describe('when false', function () { + it('should not include Accept-Ranges', function (done) { + request(createApp(fixtures, { 'acceptRanges': false })) + .get('/nums.txt') + .expect(utils.shouldNotHaveHeader('Accept-Ranges')) + .expect(200, '123456789', done) + }) + + it('should ignore Rage request header', function (done) { + request(createApp(fixtures, { 'acceptRanges': false })) + .get('/nums.txt') + .set('Range', 'bytes=0-3') + .expect(utils.shouldNotHaveHeader('Accept-Ranges')) + .expect(utils.shouldNotHaveHeader('Content-Range')) + .expect(200, '123456789', done) + }) + }) + + describe('when true', function () { + it('should include Accept-Ranges', function (done) { + request(createApp(fixtures, { 'acceptRanges': true })) + .get('/nums.txt') + .expect('Accept-Ranges', 'bytes') + .expect(200, '123456789', done) + }) + + it('should obey Rage request header', function (done) { + request(createApp(fixtures, { 'acceptRanges': true })) + .get('/nums.txt') + .set('Range', 'bytes=0-3') + .expect('Accept-Ranges', 'bytes') + .expect('Content-Range', 'bytes 0-3/9') + .expect(206, '1234', done) + }) + }) + }) + + describe('cacheControl', function () { + describe('when false', function () { + it('should not include Cache-Control', function (done) { + request(createApp(fixtures, { 'cacheControl': false })) + .get('/nums.txt') + .expect(utils.shouldNotHaveHeader('Cache-Control')) + .expect(200, '123456789', done) + }) + + it('should ignore maxAge', function (done) { + request(createApp(fixtures, { 'cacheControl': false, 'maxAge': 12000 })) + .get('/nums.txt') + .expect(utils.shouldNotHaveHeader('Cache-Control')) + .expect(200, '123456789', done) + }) + }) + + describe('when true', function () { + it('should include Cache-Control', function (done) { + request(createApp(fixtures, { 'cacheControl': true })) + .get('/nums.txt') + .expect('Cache-Control', 'public, max-age=0') + .expect(200, '123456789', done) + }) + }) + }) + + describe('extensions', function () { + it('should be not be enabled by default', function (done) { + request(createApp(fixtures)) + .get('/todo') + .expect(404, done) + }) + + it('should be configurable', function (done) { + request(createApp(fixtures, { 'extensions': 'txt' })) + .get('/todo') + .expect(200, '- groceries', done) + }) + + it('should support disabling extensions', function (done) { + request(createApp(fixtures, { 'extensions': false })) + .get('/todo') + .expect(404, done) + }) + + it('should support fallbacks', function (done) { + request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] })) + .get('/todo') + .expect(200, '
  • groceries
  • ', done) + }) + + it('should 404 if nothing found', function (done) { + request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] })) + .get('/bob') + .expect(404, done) + }) + }) + + describe('fallthrough', function () { + it('should default to true', function (done) { + request(createApp()) + .get('/does-not-exist') + .expect(404, 'Not Found', done) + }) + + describe('when true', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': true }) + }) + + it('should fall-through when OPTIONS request', function (done) { + request(this.app) + .options('/todo.txt') + .expect(404, 'Not Found', done) + }) + + it('should fall-through when URL malformed', function (done) { + request(this.app) + .get('/%') + .expect(404, 'Not Found', done) + }) + + it('should fall-through when traversing past root', function (done) { + request(this.app) + .get('/users/../../todo.txt') + .expect(404, 'Not Found', done) + }) + + it('should fall-through when URL too long', function (done) { + var app = express() + var root = fixtures + Array(10000).join('/foobar') + + app.use(express.static(root, { 'fallthrough': true })) + app.use(function (req, res, next) { + res.sendStatus(404) + }) + + request(app) + .get('/') + .expect(404, 'Not Found', done) + }) + + describe('with redirect: true', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': true }) + }) + + it('should fall-through when directory', function (done) { + request(this.app) + .get('/pets/') + .expect(404, 'Not Found', done) + }) + + it('should redirect when directory without slash', function (done) { + request(this.app) + .get('/pets') + .expect(301, /Redirecting/, done) + }) + }) + + describe('with redirect: false', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': false }) + }) + + it('should fall-through when directory', function (done) { + request(this.app) + .get('/pets/') + .expect(404, 'Not Found', done) + }) + + it('should fall-through when directory without slash', function (done) { + request(this.app) + .get('/pets') + .expect(404, 'Not Found', done) + }) + }) + }) + + describe('when false', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': false }) + }) + + it('should 405 when OPTIONS request', function (done) { + request(this.app) + .options('/todo.txt') + .expect('Allow', 'GET, HEAD') + .expect(405, done) + }) + + it('should 400 when URL malformed', function (done) { + request(this.app) + .get('/%') + .expect(400, /BadRequestError/, done) + }) + + it('should 403 when traversing past root', function (done) { + request(this.app) + .get('/users/../../todo.txt') + .expect(403, /ForbiddenError/, done) + }) + + it('should 404 when URL too long', function (done) { + var app = express() + var root = fixtures + Array(10000).join('/foobar') + + app.use(express.static(root, { 'fallthrough': false })) + app.use(function (req, res, next) { + res.sendStatus(404) + }) + + request(app) + .get('/') + .expect(404, /ENAMETOOLONG/, done) + }) + + describe('with redirect: true', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': true }) + }) + + it('should 404 when directory', function (done) { + request(this.app) + .get('/pets/') + .expect(404, /NotFoundError|ENOENT/, done) + }) + + it('should redirect when directory without slash', function (done) { + request(this.app) + .get('/pets') + .expect(301, /Redirecting/, done) + }) + }) + + describe('with redirect: false', function () { + before(function () { + this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': false }) + }) + + it('should 404 when directory', function (done) { + request(this.app) + .get('/pets/') + .expect(404, /NotFoundError|ENOENT/, done) + }) + + it('should 404 when directory without slash', function (done) { + request(this.app) + .get('/pets') + .expect(404, /NotFoundError|ENOENT/, done) + }) + }) + }) + }) + + describe('hidden files', function () { + before(function () { + this.app = createApp(fixtures, { 'dotfiles': 'allow' }) + }) + + it('should be served when dotfiles: "allow" is given', function (done) { + request(this.app) + .get('/.name') + .expect(200) + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + }) + + describe('immutable', function () { + it('should default to false', function (done) { + request(createApp(fixtures)) + .get('/nums.txt') + .expect('Cache-Control', 'public, max-age=0', done) + }) + + it('should set immutable directive in Cache-Control', function (done) { + request(createApp(fixtures, { 'immutable': true, 'maxAge': '1h' })) + .get('/nums.txt') + .expect('Cache-Control', 'public, max-age=3600, immutable', done) + }) + }) + + describe('lastModified', function () { + describe('when false', function () { + it('should not include Last-Modified', function (done) { + request(createApp(fixtures, { 'lastModified': false })) + .get('/nums.txt') + .expect(utils.shouldNotHaveHeader('Last-Modified')) + .expect(200, '123456789', done) + }) + }) + + describe('when true', function () { + it('should include Last-Modified', function (done) { + request(createApp(fixtures, { 'lastModified': true })) + .get('/nums.txt') + .expect('Last-Modified', /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/) + .expect(200, '123456789', done) + }) + }) + }) + + describe('maxAge', function () { + it('should accept string', function (done) { + request(createApp(fixtures, { 'maxAge': '30d' })) + .get('/todo.txt') + .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 30)) + .expect(200, done) + }) + + it('should be reasonable when infinite', function (done) { + request(createApp(fixtures, { 'maxAge': Infinity })) + .get('/todo.txt') + .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 365)) + .expect(200, done) + }) + }) + + describe('redirect', function () { + before(function () { + this.app = express() + this.app.use(function (req, res, next) { + req.originalUrl = req.url = + req.originalUrl.replace(/\/snow(\/|$)/, '/snow \u2603$1') + next() + }) + this.app.use(express.static(fixtures)) + }) + + it('should redirect directories', function (done) { + request(this.app) + .get('/users') + .expect('Location', '/users/') + .expect(301, done) + }) + + it('should include HTML link', function (done) { + request(this.app) + .get('/users') + .expect('Location', '/users/') + .expect(301, //, done) + }) + + it('should redirect directories with query string', function (done) { + request(this.app) + .get('/users?name=john') + .expect('Location', '/users/?name=john') + .expect(301, done) + }) + + it('should not redirect to protocol-relative locations', function (done) { + request(this.app) + .get('//users') + .expect('Location', '/users/') + .expect(301, done) + }) + + it('should ensure redirect URL is properly encoded', function (done) { + request(this.app) + .get('/snow') + .expect('Location', '/snow%20%E2%98%83/') + .expect('Content-Type', /html/) + .expect(301, />Redirecting to \/snow%20%E2%98%83\/<\/a>tobi') + .expect(200, '"tobi"', done) + }) + + it('should ignore standard type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/plain') + .send('user is tobi') + .expect(200, '{}', done) + }) + }) + + describe('when ["text/html", "text/plain"]', function () { + before(function () { + this.app = createApp({ type: ['text/html', 'text/plain'] }) + }) + + it('should parse "text/html"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/html') + .send('tobi') + .expect(200, '"tobi"', done) + }) + + it('should parse "text/plain"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/plain') + .send('tobi') + .expect(200, '"tobi"', done) + }) + + it('should ignore "text/xml"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/xml') + .send('tobi') + .expect(200, '{}', done) + }) + }) + + describe('when a function', function () { + it('should parse when truthy value returned', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return req.headers['content-type'] === 'text/vnd.something' + } + + request(app) + .post('/') + .set('Content-Type', 'text/vnd.something') + .send('user is tobi') + .expect(200, '"user is tobi"', done) + }) + + it('should work without content-type', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return true + } + + var test = request(app).post('/') + test.write('user is tobi') + test.expect(200, '"user is tobi"', done) + }) + + it('should not invoke without a body', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + throw new Error('oops!') + } + + request(app) + .get('/') + .expect(404, done) + }) + }) + }) + + describe('with verify option', function () { + it('should assert value is function', function () { + assert.throws(createApp.bind(null, { verify: 'lol' }), + /TypeError: option verify must be function/) + }) + + it('should error from verify', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x20) throw new Error('no leading space') + } }) + + request(app) + .post('/') + .set('Content-Type', 'text/plain') + .send(' user is tobi') + .expect(403, 'no leading space', done) + }) + + it('should allow custom codes', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] !== 0x20) return + var err = new Error('no leading space') + err.status = 400 + throw err + } }) + + request(app) + .post('/') + .set('Content-Type', 'text/plain') + .send(' user is tobi') + .expect(400, 'no leading space', done) + }) + + it('should allow pass-through', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x20) throw new Error('no leading space') + } }) + + request(app) + .post('/') + .set('Content-Type', 'text/plain') + .send('user is tobi') + .expect(200, '"user is tobi"', done) + }) + + it('should 415 on unknown charset prior to verify', function (done) { + var app = createApp({ verify: function (req, res, buf) { + throw new Error('unexpected verify call') + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'text/plain; charset=x-bogus') + test.write(Buffer.from('00000000', 'hex')) + test.expect(415, 'unsupported charset "X-BOGUS"', done) + }) + }) + + describe('charset', function () { + before(function () { + this.app = createApp() + }) + + it('should parse utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain; charset=utf-8') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should parse codepage charsets', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain; charset=koi8-r') + test.write(Buffer.from('6e616d6520697320cec5d4', 'hex')) + test.expect(200, '"name is нет"', done) + }) + + it('should parse when content-length != char length', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain; charset=utf-8') + test.set('Content-Length', '11') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should default to utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should 415 on unknown charset', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain; charset=x-bogus') + test.write(Buffer.from('00000000', 'hex')) + test.expect(415, 'unsupported charset "X-BOGUS"', done) + }) + }) + + describe('encoding', function () { + before(function () { + this.app = createApp({ limit: '10kb' }) + }) + + it('should parse without encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should support identity encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'identity') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should support gzip encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should support deflate encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'deflate') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('789ccb4bcc4d55c82c5678b16e17001a6f050e', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should be case-insensitive', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'GZIP') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + it('should fail on unknown encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, 'unsupported content encoding "nulls"', done) + }) + }) +}) + +function createApp (options) { + var app = express() + + app.use(express.text(options)) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send(err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + return app +} diff --git a/test/express.urlencoded.js b/test/express.urlencoded.js new file mode 100644 index 00000000000..340eb74316c --- /dev/null +++ b/test/express.urlencoded.js @@ -0,0 +1,735 @@ +'use strict' + +var assert = require('assert') +var Buffer = require('safe-buffer').Buffer +var express = require('..') +var request = require('supertest') + +describe('express.urlencoded()', function () { + before(function () { + this.app = createApp() + }) + + it('should parse x-www-form-urlencoded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should 400 when invalid content-length', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.headers['content-length'] = '20' // bad length + next() + }) + + app.use(express.urlencoded()) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('str=') + .expect(400, /content length/, done) + }) + + it('should handle Content-Length: 0', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Content-Length', '0') + .send('') + .expect(200, '{}', done) + }) + + it('should handle empty message-body', function (done) { + request(createApp({ limit: '1kb' })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Transfer-Encoding', 'chunked') + .send('') + .expect(200, '{}', done) + }) + + it('should handle duplicated middleware', function (done) { + var app = express() + + app.use(express.urlencoded()) + app.use(express.urlencoded()) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should parse extended syntax', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user[name][first]=Tobi') + .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) + }) + + describe('with extended option', function () { + describe('when false', function () { + before(function () { + this.app = createApp({ extended: false }) + }) + + it('should not parse extended syntax', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user[name][first]=Tobi') + .expect(200, '{"user[name][first]":"Tobi"}', done) + }) + + it('should parse multiple key instances', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=Tobi&user=Loki') + .expect(200, '{"user":["Tobi","Loki"]}', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ extended: true }) + }) + + it('should parse multiple key instances', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=Tobi&user=Loki') + .expect(200, '{"user":["Tobi","Loki"]}', done) + }) + + it('should parse extended syntax', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user[name][first]=Tobi') + .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) + }) + + it('should parse parameters with dots', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user.name=Tobi') + .expect(200, '{"user.name":"Tobi"}', done) + }) + + it('should parse fully-encoded extended syntax', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user%5Bname%5D%5Bfirst%5D=Tobi') + .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) + }) + + it('should parse array index notation', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('foo[0]=bar&foo[1]=baz') + .expect(200, '{"foo":["bar","baz"]}', done) + }) + + it('should parse array index notation with large array', function (done) { + var str = 'f[0]=0' + + for (var i = 1; i < 500; i++) { + str += '&f[' + i + ']=' + i.toString(16) + } + + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(str) + .expect(function (res) { + var obj = JSON.parse(res.text) + assert.strictEqual(Object.keys(obj).length, 1) + assert.strictEqual(Array.isArray(obj.f), true) + assert.strictEqual(obj.f.length, 500) + }) + .expect(200, done) + }) + + it('should parse array of objects syntax', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('foo[0][bar]=baz&foo[0][fizz]=buzz&foo[]=done!') + .expect(200, '{"foo":[{"bar":"baz","fizz":"buzz"},"done!"]}', done) + }) + + it('should parse deep object', function (done) { + var str = 'foo' + + for (var i = 0; i < 500; i++) { + str += '[p]' + } + + str += '=bar' + + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(str) + .expect(function (res) { + var obj = JSON.parse(res.text) + assert.strictEqual(Object.keys(obj).length, 1) + assert.strictEqual(typeof obj.foo, 'object') + + var depth = 0 + var ref = obj.foo + while ((ref = ref.p)) { depth++ } + assert.strictEqual(depth, 500) + }) + .expect(200, done) + }) + }) + }) + + describe('with inflate option', function () { + describe('when false', function () { + before(function () { + this.app = createApp({ inflate: false }) + }) + + it('should not accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(415, 'content encoding unsupported', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ inflate: true }) + }) + + it('should accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + }) + }) + + describe('with limit option', function () { + it('should 413 when over limit with Content-Length', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: '1kb' })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Content-Length', '1028') + .send('str=' + buf.toString()) + .expect(413, done) + }) + + it('should 413 when over limit with chunked encoding', function (done) { + var buf = Buffer.alloc(1024, '.') + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.set('Transfer-Encoding', 'chunked') + test.write('str=') + test.write(buf.toString()) + test.expect(413, done) + }) + + it('should accept number of bytes', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: 1024 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('str=' + buf.toString()) + .expect(413, done) + }) + + it('should not change when options altered', function (done) { + var buf = Buffer.alloc(1024, '.') + var options = { limit: '1kb' } + var app = createApp(options) + + options.limit = '100kb' + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('str=' + buf.toString()) + .expect(413, done) + }) + + it('should not hang response', function (done) { + var buf = Buffer.alloc(10240, '.') + var app = createApp({ limit: '8kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(buf) + test.write(buf) + test.write(buf) + test.expect(413, done) + }) + }) + + describe('with parameterLimit option', function () { + describe('with extended: false', function () { + it('should reject 0', function () { + assert.throws(createApp.bind(null, { extended: false, parameterLimit: 0 }), + /TypeError: option parameterLimit must be a positive number/) + }) + + it('should reject string', function () { + assert.throws(createApp.bind(null, { extended: false, parameterLimit: 'beep' }), + /TypeError: option parameterLimit must be a positive number/) + }) + + it('should 413 if over limit', function (done) { + request(createApp({ extended: false, parameterLimit: 10 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(11)) + .expect(413, /too many parameters/, done) + }) + + it('should error with type = "parameters.too.many"', function (done) { + request(createApp({ extended: false, parameterLimit: 10 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('X-Error-Property', 'type') + .send(createManyParams(11)) + .expect(413, 'parameters.too.many', done) + }) + + it('should work when at the limit', function (done) { + request(createApp({ extended: false, parameterLimit: 10 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(10)) + .expect(expectKeyCount(10)) + .expect(200, done) + }) + + it('should work if number is floating point', function (done) { + request(createApp({ extended: false, parameterLimit: 10.1 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(11)) + .expect(413, /too many parameters/, done) + }) + + it('should work with large limit', function (done) { + request(createApp({ extended: false, parameterLimit: 5000 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(5000)) + .expect(expectKeyCount(5000)) + .expect(200, done) + }) + + it('should work with Infinity limit', function (done) { + request(createApp({ extended: false, parameterLimit: Infinity })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(10000)) + .expect(expectKeyCount(10000)) + .expect(200, done) + }) + }) + + describe('with extended: true', function () { + it('should reject 0', function () { + assert.throws(createApp.bind(null, { extended: true, parameterLimit: 0 }), + /TypeError: option parameterLimit must be a positive number/) + }) + + it('should reject string', function () { + assert.throws(createApp.bind(null, { extended: true, parameterLimit: 'beep' }), + /TypeError: option parameterLimit must be a positive number/) + }) + + it('should 413 if over limit', function (done) { + request(createApp({ extended: true, parameterLimit: 10 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(11)) + .expect(413, /too many parameters/, done) + }) + + it('should error with type = "parameters.too.many"', function (done) { + request(createApp({ extended: true, parameterLimit: 10 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('X-Error-Property', 'type') + .send(createManyParams(11)) + .expect(413, 'parameters.too.many', done) + }) + + it('should work when at the limit', function (done) { + request(createApp({ extended: true, parameterLimit: 10 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(10)) + .expect(expectKeyCount(10)) + .expect(200, done) + }) + + it('should work if number is floating point', function (done) { + request(createApp({ extended: true, parameterLimit: 10.1 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(11)) + .expect(413, /too many parameters/, done) + }) + + it('should work with large limit', function (done) { + request(createApp({ extended: true, parameterLimit: 5000 })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(5000)) + .expect(expectKeyCount(5000)) + .expect(200, done) + }) + + it('should work with Infinity limit', function (done) { + request(createApp({ extended: true, parameterLimit: Infinity })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(createManyParams(10000)) + .expect(expectKeyCount(10000)) + .expect(200, done) + }) + }) + }) + + describe('with type option', function () { + describe('when "application/vnd.x-www-form-urlencoded"', function () { + before(function () { + this.app = createApp({ type: 'application/vnd.x-www-form-urlencoded' }) + }) + + it('should parse for custom type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/vnd.x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should ignore standard type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '{}', done) + }) + }) + + describe('when ["urlencoded", "application/x-pairs"]', function () { + before(function () { + this.app = createApp({ + type: ['urlencoded', 'application/x-pairs'] + }) + }) + + it('should parse "application/x-www-form-urlencoded"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should parse "application/x-pairs"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-pairs') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should ignore application/x-foo', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-foo') + .send('user=tobi') + .expect(200, '{}', done) + }) + }) + + describe('when a function', function () { + it('should parse when truthy value returned', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return req.headers['content-type'] === 'application/vnd.something' + } + + request(app) + .post('/') + .set('Content-Type', 'application/vnd.something') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should work without content-type', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return true + } + + var test = request(app).post('/') + test.write('user=tobi') + test.expect(200, '{"user":"tobi"}', done) + }) + + it('should not invoke without a body', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + throw new Error('oops!') + } + + request(app) + .get('/') + .expect(404, done) + }) + }) + }) + + describe('with verify option', function () { + it('should assert value if function', function () { + assert.throws(createApp.bind(null, { verify: 'lol' }), + /TypeError: option verify must be function/) + }) + + it('should error from verify', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x20) throw new Error('no leading space') + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(' user=tobi') + .expect(403, 'no leading space', done) + }) + + it('should error with type = "entity.verify.failed"', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x20) throw new Error('no leading space') + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('X-Error-Property', 'type') + .send(' user=tobi') + .expect(403, 'entity.verify.failed', done) + }) + + it('should allow custom codes', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] !== 0x20) return + var err = new Error('no leading space') + err.status = 400 + throw err + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(' user=tobi') + .expect(400, 'no leading space', done) + }) + + it('should allow custom type', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] !== 0x20) return + var err = new Error('no leading space') + err.type = 'foo.bar' + throw err + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('X-Error-Property', 'type') + .send(' user=tobi') + .expect(403, 'foo.bar', done) + }) + + it('should allow pass-through', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should 415 on unknown charset prior to verify', function (done) { + var app = createApp({ verify: function (req, res, buf) { + throw new Error('unexpected verify call') + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded; charset=x-bogus') + test.write(Buffer.from('00000000', 'hex')) + test.expect(415, 'unsupported charset "X-BOGUS"', done) + }) + }) + + describe('charset', function () { + before(function () { + this.app = createApp() + }) + + it('should parse utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should parse when content-length != char length', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8') + test.set('Content-Length', '7') + test.write(Buffer.from('746573743dc3a5', 'hex')) + test.expect(200, '{"test":"å"}', done) + }) + + it('should default to utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should fail on unknown charset', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded; charset=koi8-r') + test.write(Buffer.from('6e616d653dcec5d4', 'hex')) + test.expect(415, 'unsupported charset "KOI8-R"', done) + }) + }) + + describe('encoding', function () { + before(function () { + this.app = createApp({ limit: '10kb' }) + }) + + it('should parse without encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support identity encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'identity') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support gzip encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support deflate encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'deflate') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('789ccb4bcc4db57db16e17001068042f', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should be case-insensitive', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'GZIP') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should fail on unknown encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, 'unsupported content encoding "nulls"', done) + }) + }) +}) + +function createManyParams (count) { + var str = '' + + if (count === 0) { + return str + } + + str += '0=0' + + for (var i = 1; i < count; i++) { + var n = i.toString(36) + str += '&' + n + '=' + n + } + + return str +} + +function createApp (options) { + var app = express() + + app.use(express.urlencoded(options)) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send(String(err[req.headers['x-error-property'] || 'message'])) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + return app +} + +function expectKeyCount (count) { + return function (res) { + assert.strictEqual(Object.keys(JSON.parse(res.text)).length, count) + } +} diff --git a/test/fixtures/broken.send b/test/fixtures/broken.send new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/fixtures/empty.txt b/test/fixtures/empty.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/fixtures/nums.txt b/test/fixtures/nums.txt new file mode 100644 index 00000000000..e2e107ac61a --- /dev/null +++ b/test/fixtures/nums.txt @@ -0,0 +1 @@ +123456789 \ No newline at end of file diff --git a/test/fixtures/pets/names.txt b/test/fixtures/pets/names.txt new file mode 100644 index 00000000000..91407a3e048 --- /dev/null +++ b/test/fixtures/pets/names.txt @@ -0,0 +1 @@ +tobi,loki \ No newline at end of file diff --git "a/test/fixtures/snow \342\230\203/.gitkeep" "b/test/fixtures/snow \342\230\203/.gitkeep" new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/fixtures/todo.html b/test/fixtures/todo.html new file mode 100644 index 00000000000..e7af6d7998d --- /dev/null +++ b/test/fixtures/todo.html @@ -0,0 +1 @@ +
  • groceries
  • \ No newline at end of file diff --git a/test/fixtures/todo.txt b/test/fixtures/todo.txt new file mode 100644 index 00000000000..8c3539d9461 --- /dev/null +++ b/test/fixtures/todo.txt @@ -0,0 +1 @@ +- groceries \ No newline at end of file diff --git a/test/fixtures/users/index.html b/test/fixtures/users/index.html new file mode 100644 index 00000000000..00a2db41f75 --- /dev/null +++ b/test/fixtures/users/index.html @@ -0,0 +1 @@ +

    tobi, loki, jane

    \ No newline at end of file diff --git a/test/fixtures/users/tobi.txt b/test/fixtures/users/tobi.txt new file mode 100644 index 00000000000..9d9529d47d7 --- /dev/null +++ b/test/fixtures/users/tobi.txt @@ -0,0 +1 @@ +ferret \ No newline at end of file diff --git a/test/middleware.basic.js b/test/middleware.basic.js index 28a4dd18f2e..19f00d9a296 100644 --- a/test/middleware.basic.js +++ b/test/middleware.basic.js @@ -1,4 +1,6 @@ +'use strict' +var assert = require('assert') var express = require('../'); var request = require('supertest'); @@ -28,11 +30,12 @@ describe('middleware', function(){ }); }); - request(app.listen()) + request(app) .get('/') .set('Content-Type', 'application/json') .send('{"foo":"bar"}') .expect('Content-Type', 'application/json') + .expect(function () { assert.deepEqual(calls, ['one', 'two']) }) .expect(200, '{"foo":"bar"}', done) }) }) diff --git a/test/mocha.opts b/test/mocha.opts index 24d45f5902f..1e065ec52d9 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,2 @@ --require should --slow 20 ---growl diff --git a/test/regression.js b/test/regression.js index 5d4509ed6fb..4e99b306948 100644 --- a/test/regression.js +++ b/test/regression.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); diff --git a/test/req.accepts.js b/test/req.accepts.js index 0df4780e221..2066fb51859 100644 --- a/test/req.accepts.js +++ b/test/req.accepts.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); diff --git a/test/req.acceptsCharset.js b/test/req.acceptsCharset.js index 0d0ed8b5e41..6dbab439b7e 100644 --- a/test/req.acceptsCharset.js +++ b/test/req.acceptsCharset.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); @@ -18,8 +19,8 @@ describe('req', function(){ }) }) - describe('when Accept-Charset is not present', function(){ - it('should return true when present', function(done){ + describe('when Accept-Charset is present', function () { + it('should return true', function (done) { var app = express(); app.use(function(req, res, next){ diff --git a/test/req.acceptsCharsets.js b/test/req.acceptsCharsets.js index 2f4574c5244..360a9878a78 100644 --- a/test/req.acceptsCharsets.js +++ b/test/req.acceptsCharsets.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); @@ -18,8 +19,8 @@ describe('req', function(){ }) }) - describe('when Accept-Charset is not present', function(){ - it('should return true when present', function(done){ + describe('when Accept-Charset is present', function () { + it('should return true', function (done) { var app = express(); app.use(function(req, res, next){ diff --git a/test/req.acceptsEncoding.js b/test/req.acceptsEncoding.js index 12708fc0144..bcec2280e65 100644 --- a/test/req.acceptsEncoding.js +++ b/test/req.acceptsEncoding.js @@ -1,36 +1,39 @@ +'use strict' var express = require('../') , request = require('supertest'); describe('req', function(){ describe('.acceptsEncoding', function(){ - it('should be true if encoding accepted', function(done){ + it('should return encoding if accepted', function (done) { var app = express(); - app.use(function(req, res){ - req.acceptsEncoding('gzip').should.be.ok; - req.acceptsEncoding('deflate').should.be.ok; - res.end(); - }); + app.get('/', function (req, res) { + res.send({ + gzip: req.acceptsEncoding('gzip'), + deflate: req.acceptsEncoding('deflate') + }) + }) request(app) - .get('/') - .set('Accept-Encoding', ' gzip, deflate') - .expect(200, done); + .get('/') + .set('Accept-Encoding', ' gzip, deflate') + .expect(200, { gzip: 'gzip', deflate: 'deflate' }, done) }) it('should be false if encoding not accepted', function(done){ var app = express(); - app.use(function(req, res){ - req.acceptsEncoding('bogus').should.not.be.ok; - res.end(); - }); + app.get('/', function (req, res) { + res.send({ + bogus: req.acceptsEncoding('bogus') + }) + }) request(app) - .get('/') - .set('Accept-Encoding', ' gzip, deflate') - .expect(200, done); + .get('/') + .set('Accept-Encoding', ' gzip, deflate') + .expect(200, { bogus: false }, done) }) }) }) diff --git a/test/req.acceptsEncodings.js b/test/req.acceptsEncodings.js index c036c297691..c36934290a1 100644 --- a/test/req.acceptsEncodings.js +++ b/test/req.acceptsEncodings.js @@ -1,36 +1,39 @@ +'use strict' var express = require('../') , request = require('supertest'); describe('req', function(){ - describe('.acceptsEncodingss', function(){ - it('should be true if encoding accepted', function(done){ + describe('.acceptsEncodings', function () { + it('should return encoding if accepted', function (done) { var app = express(); - app.use(function(req, res){ - req.acceptsEncodings('gzip').should.be.ok; - req.acceptsEncodings('deflate').should.be.ok; - res.end(); - }); + app.get('/', function (req, res) { + res.send({ + gzip: req.acceptsEncoding('gzip'), + deflate: req.acceptsEncoding('deflate') + }) + }) request(app) - .get('/') - .set('Accept-Encoding', ' gzip, deflate') - .expect(200, done); + .get('/') + .set('Accept-Encoding', ' gzip, deflate') + .expect(200, { gzip: 'gzip', deflate: 'deflate' }, done) }) it('should be false if encoding not accepted', function(done){ var app = express(); - app.use(function(req, res){ - req.acceptsEncodings('bogus').should.not.be.ok; - res.end(); - }); + app.get('/', function (req, res) { + res.send({ + bogus: req.acceptsEncoding('bogus') + }) + }) request(app) - .get('/') - .set('Accept-Encoding', ' gzip, deflate') - .expect(200, done); + .get('/') + .set('Accept-Encoding', ' gzip, deflate') + .expect(200, { bogus: false }, done) }) }) }) diff --git a/test/req.acceptsLanguage.js b/test/req.acceptsLanguage.js index b14d920bd69..816be244eb1 100644 --- a/test/req.acceptsLanguage.js +++ b/test/req.acceptsLanguage.js @@ -1,52 +1,56 @@ +'use strict' var express = require('../') , request = require('supertest'); describe('req', function(){ describe('.acceptsLanguage', function(){ - it('should be true if language accepted', function(done){ + it('should return language if accepted', function (done) { var app = express(); - app.use(function(req, res){ - req.acceptsLanguage('en-us').should.be.ok; - req.acceptsLanguage('en').should.be.ok; - res.end(); - }); + app.get('/', function (req, res) { + res.send({ + 'en-us': req.acceptsLanguages('en-us'), + en: req.acceptsLanguages('en') + }) + }) request(app) - .get('/') - .set('Accept-Language', 'en;q=.5, en-us') - .expect(200, done); + .get('/') + .set('Accept-Language', 'en;q=.5, en-us') + .expect(200, { 'en-us': 'en-us', en: 'en' }, done) }) it('should be false if language not accepted', function(done){ var app = express(); - app.use(function(req, res){ - req.acceptsLanguage('es').should.not.be.ok; - res.end(); - }); + app.get('/', function (req, res) { + res.send({ + es: req.acceptsLanguages('es') + }) + }) request(app) - .get('/') - .set('Accept-Language', 'en;q=.5, en-us') - .expect(200, done); + .get('/') + .set('Accept-Language', 'en;q=.5, en-us') + .expect(200, { es: false }, done) }) describe('when Accept-Language is not present', function(){ - it('should always return true', function(done){ + it('should always return language', function (done) { var app = express(); - app.use(function(req, res){ - req.acceptsLanguage('en').should.be.ok; - req.acceptsLanguage('es').should.be.ok; - req.acceptsLanguage('jp').should.be.ok; - res.end(); - }); + app.get('/', function (req, res) { + res.send({ + en: req.acceptsLanguages('en'), + es: req.acceptsLanguages('es'), + jp: req.acceptsLanguages('jp') + }) + }) request(app) - .get('/') - .expect(200, done); + .get('/') + .expect(200, { en: 'en', es: 'es', jp: 'jp' }, done) }) }) }) diff --git a/test/req.acceptsLanguages.js b/test/req.acceptsLanguages.js index 6a9cb3366bc..e5629fbc323 100644 --- a/test/req.acceptsLanguages.js +++ b/test/req.acceptsLanguages.js @@ -1,52 +1,56 @@ +'use strict' var express = require('../') , request = require('supertest'); describe('req', function(){ describe('.acceptsLanguages', function(){ - it('should be true if language accepted', function(done){ + it('should return language if accepted', function (done) { var app = express(); - app.use(function(req, res){ - req.acceptsLanguages('en-us').should.be.ok; - req.acceptsLanguages('en').should.be.ok; - res.end(); - }); + app.get('/', function (req, res) { + res.send({ + 'en-us': req.acceptsLanguages('en-us'), + en: req.acceptsLanguages('en') + }) + }) request(app) - .get('/') - .set('Accept-Language', 'en;q=.5, en-us') - .expect(200, done); + .get('/') + .set('Accept-Language', 'en;q=.5, en-us') + .expect(200, { 'en-us': 'en-us', en: 'en' }, done) }) it('should be false if language not accepted', function(done){ var app = express(); - app.use(function(req, res){ - req.acceptsLanguages('es').should.not.be.ok; - res.end(); - }); + app.get('/', function (req, res) { + res.send({ + es: req.acceptsLanguages('es') + }) + }) request(app) - .get('/') - .set('Accept-Language', 'en;q=.5, en-us') - .expect(200, done); + .get('/') + .set('Accept-Language', 'en;q=.5, en-us') + .expect(200, { es: false }, done) }) describe('when Accept-Language is not present', function(){ - it('should always return true', function(done){ + it('should always return language', function (done) { var app = express(); - app.use(function(req, res){ - req.acceptsLanguages('en').should.be.ok; - req.acceptsLanguages('es').should.be.ok; - req.acceptsLanguages('jp').should.be.ok; - res.end(); - }); + app.get('/', function (req, res) { + res.send({ + en: req.acceptsLanguages('en'), + es: req.acceptsLanguages('es'), + jp: req.acceptsLanguages('jp') + }) + }) request(app) - .get('/') - .expect(200, done); + .get('/') + .expect(200, { en: 'en', es: 'es', jp: 'jp' }, done) }) }) }) diff --git a/test/req.baseUrl.js b/test/req.baseUrl.js index 9ac9d88029b..b70803ea8bb 100644 --- a/test/req.baseUrl.js +++ b/test/req.baseUrl.js @@ -1,3 +1,4 @@ +'use strict' var express = require('..') var request = require('supertest') diff --git a/test/req.fresh.js b/test/req.fresh.js index 1aa8fa5b217..9160e2caaf6 100644 --- a/test/req.fresh.js +++ b/test/req.fresh.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); diff --git a/test/req.get.js b/test/req.get.js index 109a2d90ce7..16589b3f059 100644 --- a/test/req.get.js +++ b/test/req.get.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest') diff --git a/test/req.host.js b/test/req.host.js index 8fa3409054f..2c051fb9791 100644 --- a/test/req.host.js +++ b/test/req.host.js @@ -1,7 +1,7 @@ +'use strict' var express = require('../') , request = require('supertest') - , assert = require('assert'); describe('req', function(){ describe('.host', function(){ diff --git a/test/req.hostname.js b/test/req.hostname.js index 65c2be81a1f..b3716b566ae 100644 --- a/test/req.hostname.js +++ b/test/req.hostname.js @@ -1,7 +1,7 @@ +'use strict' var express = require('../') , request = require('supertest') - , assert = require('assert'); describe('req', function(){ describe('.hostname', function(){ @@ -117,6 +117,56 @@ describe('req', function(){ .set('Host', 'example.com') .expect('example.com', done); }) + + describe('when multiple X-Forwarded-Host', function () { + it('should use the first value', function (done) { + var app = express() + + app.enable('trust proxy') + + app.use(function (req, res) { + res.send(req.hostname) + }) + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com, foobar.com') + .expect(200, 'example.com', done) + }) + + it('should remove OWS around comma', function (done) { + var app = express() + + app.enable('trust proxy') + + app.use(function (req, res) { + res.send(req.hostname) + }) + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com , foobar.com') + .expect(200, 'example.com', done) + }) + + it('should strip port number', function (done) { + var app = express() + + app.enable('trust proxy') + + app.use(function (req, res) { + res.send(req.hostname) + }) + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com:8080 , foobar.com:8888') + .expect(200, 'example.com', done) + }) + }) }) describe('when "trust proxy" is disabled', function(){ diff --git a/test/req.ip.js b/test/req.ip.js index 1cd255216b9..6bb3c5ac52f 100644 --- a/test/req.ip.js +++ b/test/req.ip.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); @@ -21,7 +22,7 @@ describe('req', function(){ .expect('client', done); }) - it('should return the addr after trusted proxy', function(done){ + it('should return the addr after trusted proxy based on count', function (done) { var app = express(); app.set('trust proxy', 2); @@ -36,6 +37,21 @@ describe('req', function(){ .expect('p1', done); }) + it('should return the addr after trusted proxy based on list', function (done) { + var app = express() + + app.set('trust proxy', '10.0.0.1, 10.0.0.2, 127.0.0.1, ::1') + + app.get('/', function (req, res) { + res.send(req.ip) + }) + + request(app) + .get('/') + .set('X-Forwarded-For', '10.0.0.2, 10.0.0.3, 10.0.0.1', '10.0.0.4') + .expect('10.0.0.3', done) + }) + it('should return the addr after trusted proxy, from sub app', function (done) { var app = express(); var sub = express(); diff --git a/test/req.ips.js b/test/req.ips.js index a7d464b8468..2f9a0736ea3 100644 --- a/test/req.ips.js +++ b/test/req.ips.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); diff --git a/test/req.is.js b/test/req.is.js index a2fce178673..c5904dd600a 100644 --- a/test/req.is.js +++ b/test/req.is.js @@ -1,3 +1,4 @@ +'use strict' var express = require('..') var request = require('supertest') diff --git a/test/req.param.js b/test/req.param.js index 1e827f03058..b3748c02bce 100644 --- a/test/req.param.js +++ b/test/req.param.js @@ -1,7 +1,7 @@ +'use strict' var express = require('../') , request = require('supertest') - , bodyParser = require('body-parser') describe('req', function(){ describe('.param(name, default)', function(){ @@ -34,7 +34,7 @@ describe('req', function(){ it('should check req.body', function(done){ var app = express(); - app.use(bodyParser.json()); + app.use(express.json()) app.use(function(req, res){ res.end(req.param('name')); diff --git a/test/req.path.js b/test/req.path.js index 6ad4009c7d5..3ff6177c74e 100644 --- a/test/req.path.js +++ b/test/req.path.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); diff --git a/test/req.protocol.js b/test/req.protocol.js index 453ad11ca4a..61f76356b4c 100644 --- a/test/req.protocol.js +++ b/test/req.protocol.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); diff --git a/test/req.query.js b/test/req.query.js index d3d29abd16d..6fae592dccc 100644 --- a/test/req.query.js +++ b/test/req.query.js @@ -1,4 +1,6 @@ +'use strict' +var assert = require('assert') var express = require('../') , request = require('supertest'); @@ -25,8 +27,8 @@ describe('req', function(){ var app = createApp('extended'); request(app) - .get('/?user[name]=tj') - .expect(200, '{"user":{"name":"tj"}}', done); + .get('/?foo[0][bar]=baz&foo[0][fizz]=buzz&foo[]=done!') + .expect(200, '{"foo":[{"bar":"baz","fizz":"buzz"},"done!"]}', done); }); it('should parse parameters with dots', function (done) { @@ -70,7 +72,7 @@ describe('req', function(){ }); }); - describe('when "query parser" disabled', function () { + describe('when "query parser" enabled', function () { it('should not parse complex keys', function (done) { var app = createApp(true); @@ -99,7 +101,8 @@ describe('req', function(){ describe('when "query parser" an unknown value', function () { it('should throw', function () { - createApp.bind(null, 'bogus').should.throw(/unknown value.*query parser/); + assert.throws(createApp.bind(null, 'bogus'), + /unknown value.*query parser/) }); }); }) diff --git a/test/req.range.js b/test/req.range.js index 09459d1e127..111441736eb 100644 --- a/test/req.range.js +++ b/test/req.range.js @@ -1,5 +1,5 @@ +'use strict' -var assert = require('assert'); var express = require('..'); var request = require('supertest') diff --git a/test/req.route.js b/test/req.route.js index 2947b7c3d0f..6c17fbb1c8f 100644 --- a/test/req.route.js +++ b/test/req.route.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); @@ -8,18 +9,20 @@ describe('req', function(){ var app = express(); app.get('/user/:id/:op?', function(req, res, next){ - req.route.path.should.equal('/user/:id/:op?'); + res.header('path-1', req.route.path) next(); }); app.get('/user/:id/edit', function(req, res){ - req.route.path.should.equal('/user/:id/edit'); + res.header('path-2', req.route.path) res.end(); }); request(app) - .get('/user/12/edit') - .expect(200, done); + .get('/user/12/edit') + .expect('path-1', '/user/:id/:op?') + .expect('path-2', '/user/:id/edit') + .expect(200, done) }) }) }) diff --git a/test/req.secure.js b/test/req.secure.js index 2025c8786b6..0097ed6136e 100644 --- a/test/req.secure.js +++ b/test/req.secure.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); diff --git a/test/req.signedCookies.js b/test/req.signedCookies.js index 73880b01b4c..db561951665 100644 --- a/test/req.signedCookies.js +++ b/test/req.signedCookies.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest') @@ -11,7 +12,7 @@ describe('req', function(){ app.use(cookieParser('secret')); app.use(function(req, res){ - if ('/set' == req.path) { + if (req.path === '/set') { res.cookie('obj', { foo: 'bar' }, { signed: true }); res.end(); } else { diff --git a/test/req.stale.js b/test/req.stale.js index 30c9d05d51c..cda77fa403e 100644 --- a/test/req.stale.js +++ b/test/req.stale.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); diff --git a/test/req.subdomains.js b/test/req.subdomains.js index 18e4d80ad32..e5600f2eb56 100644 --- a/test/req.subdomains.js +++ b/test/req.subdomains.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); diff --git a/test/req.xhr.js b/test/req.xhr.js index cc8754ce4cf..99cf7f1917d 100644 --- a/test/req.xhr.js +++ b/test/req.xhr.js @@ -1,74 +1,42 @@ +'use strict' var express = require('../') , request = require('supertest'); describe('req', function(){ describe('.xhr', function(){ - it('should return true when X-Requested-With is xmlhttprequest', function(done){ - var app = express(); - - app.use(function(req, res){ - req.xhr.should.be.true; - res.end(); - }); - - request(app) - .get('/') - .set('X-Requested-With', 'xmlhttprequest') - .expect(200) - .end(function(err, res){ - done(err); + before(function () { + this.app = express() + this.app.get('/', function (req, res) { + res.send(req.xhr) }) }) - it('should case-insensitive', function(done){ - var app = express(); - - app.use(function(req, res){ - req.xhr.should.be.true; - res.end(); - }); + it('should return true when X-Requested-With is xmlhttprequest', function(done){ + request(this.app) + .get('/') + .set('X-Requested-With', 'xmlhttprequest') + .expect(200, 'true', done) + }) - request(app) - .get('/') - .set('X-Requested-With', 'XMLHttpRequest') - .expect(200) - .end(function(err, res){ - done(err); - }) + it('should case-insensitive', function(done){ + request(this.app) + .get('/') + .set('X-Requested-With', 'XMLHttpRequest') + .expect(200, 'true', done) }) it('should return false otherwise', function(done){ - var app = express(); - - app.use(function(req, res){ - req.xhr.should.be.false; - res.end(); - }); - - request(app) - .get('/') - .set('X-Requested-With', 'blahblah') - .expect(200) - .end(function(err, res){ - done(err); - }) + request(this.app) + .get('/') + .set('X-Requested-With', 'blahblah') + .expect(200, 'false', done) }) it('should return false when not present', function(done){ - var app = express(); - - app.use(function(req, res){ - req.xhr.should.be.false; - res.end(); - }); - - request(app) - .get('/') - .expect(200) - .end(function(err, res){ - done(err); - }) + request(this.app) + .get('/') + .expect(200, 'false', done) }) }) }) diff --git a/test/res.append.js b/test/res.append.js index f7f1d55b3cb..5b12e35b696 100644 --- a/test/res.append.js +++ b/test/res.append.js @@ -1,3 +1,4 @@ +'use strict' var express = require('..') var request = require('supertest') diff --git a/test/res.attachment.js b/test/res.attachment.js index 662b1dd4e01..6283ded0d65 100644 --- a/test/res.attachment.js +++ b/test/res.attachment.js @@ -1,4 +1,6 @@ +'use strict' +var Buffer = require('safe-buffer').Buffer var express = require('../') , request = require('supertest'); @@ -36,7 +38,7 @@ describe('res', function(){ app.use(function(req, res){ res.attachment('/path/to/image.png'); - res.send(new Buffer(4)); + res.send(Buffer.alloc(4, '.')) }); request(app) diff --git a/test/res.clearCookie.js b/test/res.clearCookie.js index 4822057e926..fc0cfb99a3d 100644 --- a/test/res.clearCookie.js +++ b/test/res.clearCookie.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest'); diff --git a/test/res.cookie.js b/test/res.cookie.js index 4eeaaf094ad..d10e48646b6 100644 --- a/test/res.cookie.js +++ b/test/res.cookie.js @@ -1,7 +1,7 @@ +'use strict' var express = require('../') , request = require('supertest') - , cookie = require('cookie') , cookieParser = require('cookie-parser') var merge = require('utils-merge'); @@ -46,12 +46,9 @@ describe('res', function(){ }); request(app) - .get('/') - .end(function(err, res){ - var val = ['name=tobi; Path=/', 'age=1; Path=/', 'gender=%3F; Path=/']; - res.headers['set-cookie'].should.eql(val); - done(); - }) + .get('/') + .expect('Set-Cookie', 'name=tobi; Path=/,age=1; Path=/,gender=%3F; Path=/') + .expect(200, done) }) }) @@ -80,11 +77,9 @@ describe('res', function(){ }); request(app) - .get('/') - .end(function(err, res){ - res.headers['set-cookie'][0].should.not.containEql('Thu, 01 Jan 1970 00:00:01 GMT'); - done(); - }) + .get('/') + .expect('Set-Cookie', /name=tobi; Max-Age=1; Path=\/; Expires=/) + .expect(200, done) }) it('should set max-age', function(done){ @@ -108,15 +103,25 @@ describe('res', function(){ app.use(function(req, res){ res.cookie('name', 'tobi', options) - res.end(); + res.json(options) }); request(app) .get('/') - .end(function(err, res){ - options.should.eql(optionsCopy); - done(); + .expect(200, optionsCopy, done) + }) + + it('should throw an error with invalid maxAge', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { maxAge: 'foobar' }) + res.end() }) + + request(app) + .get('/') + .expect(500, /option maxAge is invalid/, done) }) }) @@ -131,13 +136,9 @@ describe('res', function(){ }); request(app) - .get('/') - .end(function(err, res){ - var val = res.headers['set-cookie'][0]; - val = cookie.parse(val.split('.')[0]); - val.user.should.equal('s:j:{"name":"tobi"}'); - done(); - }) + .get('/') + .expect('Set-Cookie', 'user=s%3Aj%3A%7B%22name%22%3A%22tobi%22%7D.K20xcwmDS%2BPb1rsD95o5Jm5SqWs1KteqdnynnB7jkTE; Path=/') + .expect(200, done) }) }) diff --git a/test/res.download.js b/test/res.download.js index 0671d8318c4..ce0ee088ba0 100644 --- a/test/res.download.js +++ b/test/res.download.js @@ -1,6 +1,8 @@ +'use strict' var after = require('after'); var assert = require('assert'); +var Buffer = require('safe-buffer').Buffer var express = require('..'); var request = require('supertest'); @@ -19,6 +21,33 @@ describe('res', function(){ .expect('Content-Disposition', 'attachment; filename="user.html"') .expect(200, '

    {{user.name}}

    ', done) }) + + it('should accept range requests', function (done) { + var app = express() + + app.get('/', function (req, res) { + res.download('test/fixtures/user.html') + }) + + request(app) + .get('/') + .expect('Accept-Ranges', 'bytes') + .expect(200, '

    {{user.name}}

    ', done) + }) + + it('should respond with requested byte range', function (done) { + var app = express() + + app.get('/', function (req, res) { + res.download('test/fixtures/user.html') + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-2') + .expect('Content-Range', 'bytes 0-2/20') + .expect(206, '

    ', done) + }) }) describe('.download(path, filename)', function(){ @@ -60,7 +89,7 @@ describe('res', function(){ var cb = after(2, done); app.use(function(req, res){ - res.download('test/fixtures/user.html', 'document', done); + res.download('test/fixtures/user.html', 'document', cb) }); request(app) @@ -71,6 +100,86 @@ describe('res', function(){ }) }) + describe('.download(path, filename, options, fn)', function () { + it('should invoke the callback', function (done) { + var app = express() + var cb = after(2, done) + var options = {} + + app.use(function (req, res) { + res.download('test/fixtures/user.html', 'document', options, done) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'text/html; charset=UTF-8') + .expect('Content-Disposition', 'attachment; filename="document"') + .end(cb) + }) + + it('should allow options to res.sendFile()', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/.name', 'document', { + dotfiles: 'allow', + maxAge: '4h' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename="document"') + .expect('Cache-Control', 'public, max-age=14400') + .expect(shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + + describe('when options.headers contains Content-Disposition', function () { + it('should be ignored', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/user.html', 'document', { + headers: { + 'Content-Type': 'text/x-custom', + 'Content-Disposition': 'inline' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'text/x-custom') + .expect('Content-Disposition', 'attachment; filename="document"') + .end(done) + }) + + it('should be ignored case-insensitively', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('test/fixtures/user.html', 'document', { + headers: { + 'content-type': 'text/x-custom', + 'content-disposition': 'inline' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'text/x-custom') + .expect('Content-Disposition', 'attachment; filename="document"') + .end(done) + }) + }) + }) + describe('on failure', function(){ it('should invoke the callback', function(done){ var app = express(); @@ -89,7 +198,6 @@ describe('res', function(){ it('should remove Content-Disposition', function(done){ var app = express() - , calls = 0; app.use(function (req, res, next) { res.download('test/fixtures/foobar.html', function(err){ @@ -106,6 +214,16 @@ describe('res', function(){ }) }) +function shouldHaveBody (buf) { + return function (res) { + var body = !Buffer.isBuffer(res.body) + ? Buffer.from(res.text) + : res.body + assert.ok(body, 'response has body') + assert.strictEqual(body.toString('hex'), buf.toString('hex')) + } +} + function shouldNotHaveHeader(header) { return function (res) { assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header); diff --git a/test/res.format.js b/test/res.format.js index 2b0dfd517e7..24e18d95528 100644 --- a/test/res.format.js +++ b/test/res.format.js @@ -1,7 +1,8 @@ +'use strict' +var after = require('after') var express = require('../') , request = require('supertest') - , utils = require('../lib/utils') , assert = require('assert'); var app1 = express(); @@ -17,9 +18,9 @@ app1.use(function(req, res, next){ }, 'application/json': function(a, b, c){ - assert(req == a); - assert(res == b); - assert(next == c); + assert(req === a) + assert(res === b) + assert(next === c) res.send({ message: 'hey' }); } }); @@ -169,21 +170,23 @@ function test(app) { .expect('hey', done); }) - it('should set the correct charset for the Content-Type', function() { + it('should set the correct charset for the Content-Type', function (done) { + var cb = after(3, done) + request(app) .get('/') .set('Accept', 'text/html') - .expect('Content-Type', 'text/html; charset=utf-8'); + .expect('Content-Type', 'text/html; charset=utf-8', cb) request(app) .get('/') .set('Accept', 'text/plain') - .expect('Content-Type', 'text/plain; charset=utf-8'); + .expect('Content-Type', 'text/plain; charset=utf-8', cb) request(app) .get('/') .set('Accept', 'application/json') - .expect('Content-Type', 'application/json'); + .expect('Content-Type', 'application/json; charset=utf-8', cb) }) it('should Vary: Accept', function(done){ diff --git a/test/res.get.js b/test/res.get.js index a53bdc33805..a5f12e2e539 100644 --- a/test/res.get.js +++ b/test/res.get.js @@ -1,3 +1,4 @@ +'use strict' var express = require('..'); var request = require('supertest'); diff --git a/test/res.json.js b/test/res.json.js index 69f6723af54..dcaceae5ca4 100644 --- a/test/res.json.js +++ b/test/res.json.js @@ -1,3 +1,4 @@ +'use strict' var express = require('../') , request = require('supertest') @@ -102,12 +103,49 @@ describe('res', function(){ }) }) + describe('"json escape" setting', function () { + it('should be undefined by default', function () { + var app = express() + assert.strictEqual(app.get('json escape'), undefined) + }) + + it('should unicode escape HTML-sniffing characters', function (done) { + var app = express() + + app.enable('json escape') + + app.use(function (req, res) { + res.json({ '&': '