diff --git a/package-lock.json b/package-lock.json index 27eb7a2a..4149a68a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ultimate-express", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ultimate-express", - "version": "1.3.0", + "version": "1.3.1", "license": "Apache-2.0", "dependencies": { "@types/express": "^4.0.0", @@ -31,6 +31,7 @@ "vary": "^1.1.2" }, "devDependencies": { + "after": "0.8.2", "body-parser": "^1.20.3", "compression": "^1.7.4", "cookie-parser": "^1.4.6", @@ -39,7 +40,7 @@ "ejs": "^3.1.10", "errorhandler": "^1.5.1", "exit-hook": "^2.2.1", - "express": "^4.19.2", + "express": "^4.21.1", "express-art-template": "^1.0.1", "express-async-errors": "^3.1.1", "express-dot-engine": "^1.0.8", @@ -48,22 +49,32 @@ "express-rate-limit": "^7.4.0", "express-session": "^1.18.0", "express-subdomain": "^1.0.6", + "marked": "^14.1.3", "method-override": "^3.0.0", + "mocha": "^10.7.3", + "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "mustache-express": "^1.3.2", "pako": "^2.1.0", + "pbkdf2-password": "^1.2.1", "pkg-pr-new": "^0.0.29", "pug": "^3.0.3", "response-time": "^2.3.2", "serve-index": "^1.9.1", "serve-static": "^1.16.2", + "supertest": "^6.3.0", "swig": "^1.4.2", + "u-express-local": "file://./src/index.js", + "uWSSupertest": "file://./tests/uWSSupertest.js", "vhost": "^3.0.2" }, "engines": { "node": ">=16" } }, + "../../../../../src/index.js": { + "extraneous": true + }, "node_modules/@babel/helper-string-parser": { "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", @@ -555,6 +566,12 @@ "node": ">=0.4.0" } }, + "node_modules/after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==", + "dev": true + }, "node_modules/amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -565,6 +582,15 @@ "node": ">=0.4.2" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -594,6 +620,31 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -673,6 +724,12 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/babel-walk": { "version": "3.0.0-canary-5", "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", @@ -693,6 +750,24 @@ "dev": true, "license": "MIT" }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -707,6 +782,18 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -743,6 +830,24 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -844,6 +949,30 @@ "is-regex": "^1.0.3" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/clean-css": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", @@ -869,6 +998,75 @@ "node": ">=0.10.0" } }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -889,6 +1087,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", @@ -897,6 +1107,15 @@ "license": "MIT", "peer": true }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -1018,13 +1237,12 @@ } }, "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", "dev": true, - "license": "MIT", "dependencies": { - "cookie": "0.4.1", + "cookie": "0.7.2", "cookie-signature": "1.0.6" }, "engines": { @@ -1032,11 +1250,10 @@ } }, "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1083,6 +1300,12 @@ "node": ">=6.6.0" } }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, "node_modules/cookies": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", @@ -1193,6 +1416,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1219,6 +1451,25 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", @@ -1317,12 +1568,33 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/escodegen": { "version": "1.14.3", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", @@ -1418,37 +1690,37 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -1541,13 +1813,12 @@ } }, "node_modules/express-session": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", - "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", "dev": true, - "license": "MIT", "dependencies": { - "cookie": "0.6.0", + "cookie": "0.7.2", "cookie-signature": "1.0.7", "debug": "2.6.9", "depd": "~2.0.0", @@ -1560,6 +1831,15 @@ "node": ">= 0.8.0" } }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/express-session/node_modules/cookie-signature": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", @@ -1574,29 +1854,13 @@ "dev": true, "license": "MIT" }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">= 0.6" } }, "node_modules/express/node_modules/cookie-signature": { @@ -1606,73 +1870,6 @@ "dev": true, "license": "MIT" }, - "node_modules/express/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/express/node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/express/node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1696,6 +1893,12 @@ "fast-decode-uri-component": "^1.0.1" } }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fast-zlib": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-zlib/-/fast-zlib-2.0.1.tgz", @@ -1706,8 +1909,20 @@ "url": "https://patreon.com/timotejroiko" } }, - "node_modules/fdir": { - "version": "6.4.0", + "node_modules/fastfall": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", + "dev": true, + "dependencies": { + "reusify": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fdir": { + "version": "6.4.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", "dev": true, @@ -1754,6 +1969,18 @@ "node": ">=10" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/filter-obj": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", @@ -1768,13 +1995,13 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -1785,14 +2012,29 @@ "node": ">= 0.8" } }, - "node_modules/finalhandler/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" } }, "node_modules/foreground-child": { @@ -1812,6 +2054,35 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "dev": true, + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1829,6 +2100,26 @@ "node": ">= 0.6" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1837,6 +2128,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -1879,6 +2179,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2031,11 +2343,19 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "he": "bin/he" } }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/html-minifier": { "version": "3.5.21", "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", @@ -2097,6 +2417,17 @@ "node": ">= 4" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2111,6 +2442,18 @@ "node": ">= 0.10" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -2138,6 +2481,15 @@ "object-assign": "^4.1.1" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2148,6 +2500,18 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-keyword-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-keyword-js/-/is-keyword-js-1.0.3.tgz", @@ -2159,6 +2523,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -2183,6 +2565,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -2316,6 +2710,21 @@ "node": ">= 0.8.0" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2323,6 +2732,22 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lower-case": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", @@ -2341,6 +2766,18 @@ "node": "20 || >=22" } }, + "node_modules/marked": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.3.tgz", + "integrity": "sha512-ZibJqTULGlt9g5k4VMARAktMAjXoVnnr+Y3aCqW1oDftcV4BA3UmrBifzXoZyenHRk75csiPu9iwsTj4VNBT0g==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2350,10 +2787,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-source-map": { "version": "1.1.0", @@ -2445,82 +2885,295 @@ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mlly": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz", + "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.12.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mlly/node_modules/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/mocha": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", + "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "ansi-regex": "^5.0.1" }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, - "license": "ISC", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", "dev": true, - "license": "MIT", "dependencies": { - "minimist": "^1.2.6" + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/mlly": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz", - "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "dev": true, - "license": "MIT", "dependencies": { - "acorn": "^8.12.1", - "pathe": "^1.1.2", - "pkg-types": "^1.2.0", - "ufo": "^1.5.4" - } - }, - "node_modules/mlly/node_modules/acorn": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", - "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "ee-first": "1.1.1" }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.8" } }, "node_modules/ms": { @@ -2609,6 +3262,15 @@ "lower-case": "^1.1.1" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2709,6 +3371,36 @@ "node": ">= 0.8.0" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", @@ -2750,6 +3442,15 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2785,9 +3486,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true }, "node_modules/pathe": { @@ -2797,6 +3498,15 @@ "dev": true, "license": "MIT" }, + "node_modules/pbkdf2-password": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pbkdf2-password/-/pbkdf2-password-1.2.1.tgz", + "integrity": "sha512-I6ZiUi82uhYN47lOzi8P7O69e70LRopgS4TIkq0ecDAPHrlxABWOHxLEsTzCuogkxTofFJDj0eEbOALgrrxhKg==", + "dev": true, + "dependencies": { + "fastfall": "^1.2.3" + } + }, "node_modules/picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", @@ -3092,6 +3802,15 @@ "node": ">= 0.8" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3139,6 +3858,30 @@ "dev": true, "license": "MIT" }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -3150,6 +3893,15 @@ "node": ">= 0.10" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -3192,6 +3944,16 @@ "node": ">= 0.6" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3218,6 +3980,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -3253,6 +4027,15 @@ "node": ">= 0.8" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -3593,6 +4376,82 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "deprecated": "Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", + "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.1.2" + }, + "engines": { + "node": ">=6.4.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3698,6 +4557,18 @@ "node": ">=4" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -3774,6 +4645,10 @@ "dev": true, "license": "MIT" }, + "node_modules/u-express-local": { + "resolved": "src/index.js", + "link": true + }, "node_modules/ufo": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", @@ -3906,6 +4781,10 @@ "resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#51ae1d1fd92dff77cbbdc7c431021f85578da1a6", "license": "Apache-2.0" }, + "node_modules/uWSSupertest": { + "resolved": "tests/uWSSupertest.js", + "link": true + }, "node_modules/validate-npm-package-name": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", @@ -4004,6 +4883,12 @@ "dev": true, "license": "MIT" }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -4116,6 +5001,15 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -4136,6 +5030,54 @@ "wordwrap": "0.0.2" } }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yargs/node_modules/wordwrap": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", @@ -4146,6 +5088,18 @@ "node": ">=0.4.0" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", @@ -4168,6 +5122,24 @@ "engines": { "node": ">=20" } + }, + "src/index.js": { + "dev": true + }, + "test/uWSSupertest.js": { + "extraneous": true + }, + "tests/after.js": { + "extraneous": true + }, + "tests/express-test/after.js": { + "extraneous": true + }, + "tests/express-tests/after.js": { + "extraneous": true + }, + "tests/uWSSupertest.js": { + "dev": true } } } diff --git a/package.json b/package.json index 6532171c..6d9793bd 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "vary": "^1.1.2" }, "devDependencies": { + "after": "0.8.2", "body-parser": "^1.20.3", "compression": "^1.7.4", "cookie-parser": "^1.4.6", @@ -69,7 +70,7 @@ "ejs": "^3.1.10", "errorhandler": "^1.5.1", "exit-hook": "^2.2.1", - "express": "^4.19.2", + "express": "^4.21.1", "express-art-template": "^1.0.1", "express-async-errors": "^3.1.1", "express-dot-engine": "^1.0.8", @@ -78,16 +79,23 @@ "express-rate-limit": "^7.4.0", "express-session": "^1.18.0", "express-subdomain": "^1.0.6", + "marked": "^14.1.3", "method-override": "^3.0.0", + "mocha": "^10.7.3", + "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "mustache-express": "^1.3.2", "pako": "^2.1.0", + "pbkdf2-password": "^1.2.1", "pkg-pr-new": "^0.0.29", "pug": "^3.0.3", "response-time": "^2.3.2", "serve-index": "^1.9.1", "serve-static": "^1.16.2", + "supertest": "^6.3.0", "swig": "^1.4.2", + "u-express-local": "file://./src/index.js", + "uWSSupertest": "file://./tests/uWSSupertest.js", "vhost": "^3.0.2" } } diff --git a/tests/express-tests/examples/README.md b/tests/express-tests/examples/README.md new file mode 100644 index 00000000..bd1f1f63 --- /dev/null +++ b/tests/express-tests/examples/README.md @@ -0,0 +1,29 @@ +# 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 +- [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/tests/express-tests/examples/auth/index.js b/tests/express-tests/examples/auth/index.js new file mode 100644 index 00000000..979a6479 --- /dev/null +++ b/tests/express-tests/examples/auth/index.js @@ -0,0 +1,133 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var hash = require('pbkdf2-password')() +var path = require('path'); +var session = require('express-session'); + +var app = module.exports = express(); + +// config + +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + +// middleware + +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 + secret: 'shhhh, very secret' +})); + +// Session-persisted message middleware + +app.use(function(req, res, next){ + var err = req.session.error; + var msg = req.session.success; + delete req.session.error; + delete req.session.success; + res.locals.message = ''; + if (err) res.locals.message = '

' + err + '

'; + if (msg) res.locals.message = '

' + msg + '

'; + next(); +}); + +// dummy database + +var users = { + tj: { name: 'tj' } +}; + +// when you create a user, generate a salt +// and hash the password ('foobar' is the pass here) + +hash({ password: 'foobar' }, function (err, pass, salt, hash) { + if (err) throw err; + // store the salt & hash in the "db" + users.tj.salt = salt; + users.tj.hash = hash; +}); + + +// Authenticate using our plain-object database of doom! + +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(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(null, null) + }); +} + +function restrict(req, res, next) { + if (req.session.user) { + next(); + } else { + req.session.error = 'Access denied!'; + res.redirect('/login'); + } +} + +app.get('/', function(req, res){ + res.redirect('/login'); +}); + +app.get('/restricted', restrict, function(req, res){ + res.send('Wahoo! restricted area, click to logout'); +}); + +app.get('/logout', function(req, res){ + // destroy the user's session to log them out + // will be re-created next request + req.session.destroy(function(){ + res.redirect('/'); + }); +}); + +app.get('/login', function(req, res){ + res.render('login'); +}); + +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 + req.session.regenerate(function(){ + // Store the user's primary key + // in the session store to be retrieved, + // or in this case the entire user object + req.session.user = user; + req.session.success = 'Authenticated as ' + user.name + + ' click to logout. ' + + ' You may now access /restricted.'; + res.redirect('back'); + }); + } else { + req.session.error = 'Authentication failed, please check your ' + + ' username and password.' + + ' (use "tj" and "foobar")'; + res.redirect('/login'); + } + }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/auth/views/foot.ejs b/tests/express-tests/examples/auth/views/foot.ejs new file mode 100644 index 00000000..b605728e --- /dev/null +++ b/tests/express-tests/examples/auth/views/foot.ejs @@ -0,0 +1,2 @@ + + diff --git a/tests/express-tests/examples/auth/views/head.ejs b/tests/express-tests/examples/auth/views/head.ejs new file mode 100644 index 00000000..65386267 --- /dev/null +++ b/tests/express-tests/examples/auth/views/head.ejs @@ -0,0 +1,20 @@ + + + + + + <%= title %> + + + diff --git a/tests/express-tests/examples/auth/views/login.ejs b/tests/express-tests/examples/auth/views/login.ejs new file mode 100644 index 00000000..181c36ca --- /dev/null +++ b/tests/express-tests/examples/auth/views/login.ejs @@ -0,0 +1,21 @@ + +<%- include('head', { title: 'Authentication Example' }) -%> + +

Login

+<%- message %> +Try accessing /restricted, then authenticate with "tj" and "foobar". +
+

+ + +

+

+ + +

+

+ +

+
+ +<%- include('foot') -%> diff --git a/tests/express-tests/examples/content-negotiation/db.js b/tests/express-tests/examples/content-negotiation/db.js new file mode 100644 index 00000000..f59b23bf --- /dev/null +++ b/tests/express-tests/examples/content-negotiation/db.js @@ -0,0 +1,9 @@ +'use strict' + +var users = []; + +users.push({ name: 'Tobi' }); +users.push({ name: 'Loki' }); +users.push({ name: 'Jane' }); + +module.exports = users; diff --git a/tests/express-tests/examples/content-negotiation/index.js b/tests/express-tests/examples/content-negotiation/index.js new file mode 100644 index 00000000..e8cc3702 --- /dev/null +++ b/tests/express-tests/examples/content-negotiation/index.js @@ -0,0 +1,46 @@ +'use strict' + +var express = require("express"); +var app = module.exports = express(); +var users = require('./db'); + +// so either you can deal with different types of formatting +// for expected response in index.js +app.get('/', function(req, res){ + res.format({ + html: function(){ + res.send(''); + }, + + text: function(){ + res.send(users.map(function(user){ + return ' - ' + user.name + '\n'; + }).join('')); + }, + + json: function(){ + res.json(users); + } + }); +}); + +// or you could write a tiny middleware like +// this to add a layer of abstraction +// and make things a bit more declarative: + +function format(path) { + var obj = require(path); + return function(req, res){ + res.format(obj); + }; +} + +app.get('/users', format('./users')); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/content-negotiation/users.js b/tests/express-tests/examples/content-negotiation/users.js new file mode 100644 index 00000000..fe703e73 --- /dev/null +++ b/tests/express-tests/examples/content-negotiation/users.js @@ -0,0 +1,19 @@ +'use strict' + +var users = require('./db'); + +exports.html = function(req, res){ + res.send(''); +}; + +exports.text = function(req, res){ + res.send(users.map(function(user){ + return ' - ' + user.name + '\n'; + }).join('')); +}; + +exports.json = function(req, res){ + res.json(users); +}; diff --git a/tests/express-tests/examples/cookie-sessions/index.js b/tests/express-tests/examples/cookie-sessions/index.js new file mode 100644 index 00000000..1dd553bf --- /dev/null +++ b/tests/express-tests/examples/cookie-sessions/index.js @@ -0,0 +1,25 @@ +'use strict' + +/** + * Module dependencies. + */ + +var cookieSession = require('cookie-session'); +var express = require("express"); + +var app = module.exports = express(); + +// add req.session cookie support +app.use(cookieSession({ secret: 'manny is cool' })); + +// do something with the session +app.get('/', function (req, res) { + req.session.count = (req.session.count || 0) + 1 + res.send('viewed ' + req.session.count + ' times\n') +}) + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/cookies/index.js b/tests/express-tests/examples/cookies/index.js new file mode 100644 index 00000000..88184841 --- /dev/null +++ b/tests/express-tests/examples/cookies/index.js @@ -0,0 +1,49 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var app = module.exports = express(); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); + +// custom log format +if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')) + +// parses request cookies, populating +// req.cookies and req.signedCookies +// when the secret is passed, used +// for signing the cookies. +app.use(cookieParser('my secret here')); + +// parses x-www-form-urlencoded +app.use(express.urlencoded({ extended: false })) + +app.get('/', function(req, res){ + if (req.cookies.remember) { + res.send('Remembered :). Click to forget!.'); + } else { + res.send('

Check to ' + + '.

'); + } +}); + +app.get('/forget', function(req, res){ + res.clearCookie('remember'); + res.redirect('back'); +}); + +app.post('/', function(req, res){ + var minute = 60000; + if (req.body.remember) res.cookie('remember', 1, { maxAge: minute }); + res.redirect('back'); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git "a/tests/express-tests/examples/downloads/files/CCTV\345\244\247\350\265\233\344\270\212\346\265\267\345\210\206\350\265\233\345\214\272.txt" "b/tests/express-tests/examples/downloads/files/CCTV\345\244\247\350\265\233\344\270\212\346\265\267\345\210\206\350\265\233\345\214\272.txt" new file mode 100644 index 00000000..3b049c31 --- /dev/null +++ "b/tests/express-tests/examples/downloads/files/CCTV\345\244\247\350\265\233\344\270\212\346\265\267\345\210\206\350\265\233\345\214\272.txt" @@ -0,0 +1,2 @@ +Only for test. +The file name is faked. \ No newline at end of file diff --git a/tests/express-tests/examples/downloads/files/amazing.txt b/tests/express-tests/examples/downloads/files/amazing.txt new file mode 100644 index 00000000..c478ec56 --- /dev/null +++ b/tests/express-tests/examples/downloads/files/amazing.txt @@ -0,0 +1 @@ +what an amazing download \ No newline at end of file diff --git a/tests/express-tests/examples/downloads/files/notes/groceries.txt b/tests/express-tests/examples/downloads/files/notes/groceries.txt new file mode 100644 index 00000000..fb438777 --- /dev/null +++ b/tests/express-tests/examples/downloads/files/notes/groceries.txt @@ -0,0 +1,3 @@ +* milk +* eggs +* bread diff --git a/tests/express-tests/examples/downloads/index.js b/tests/express-tests/examples/downloads/index.js new file mode 100644 index 00000000..f950d948 --- /dev/null +++ b/tests/express-tests/examples/downloads/index.js @@ -0,0 +1,40 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var path = require('path'); + +var app = module.exports = express(); + +// path to where the files are stored on disk +var FILES_DIR = path.join(__dirname, 'files') + +app.get('/', function(req, res){ + res.send('') +}); + +// /files/* is accessed via req.params[0] +// but here we name it :file +app.get('/files/:file(*)', function(req, res, next){ + res.download(req.params.file, { root: FILES_DIR }, function (err) { + if (!err) return; // file sent + if (err.status !== 404) return next(err); // non-404 error + // file for download not found + res.statusCode = 404; + res.send('Cant find that file, sorry!'); + }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/ejs/index.js b/tests/express-tests/examples/ejs/index.js new file mode 100644 index 00000000..1113a4ea --- /dev/null +++ b/tests/express-tests/examples/ejs/index.js @@ -0,0 +1,57 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var path = require('path'); + +var app = module.exports = express(); + +// Register ejs as .html. If we did +// not call this, we would need to +// name our views foo.ejs instead +// of foo.html. The __express method +// is simply a function that engines +// use to hook into the Express view +// system by default, so if we want +// to change "foo.ejs" to "foo.html" +// we simply pass _any_ function, in this +// case `ejs.__express`. + +app.engine('.html', require('ejs').__express); + +// Optional since express defaults to CWD/views + +app.set('views', path.join(__dirname, 'views')); + +// Path to our public directory + +app.use(express.static(path.join(__dirname, 'public'))); + +// Without this you would need to +// supply the extension to res.render() +// ex: res.render('users.html'). +app.set('view engine', 'html'); + +// Dummy users +var users = [ + { name: 'tobi', email: 'tobi@learnboost.com' }, + { name: 'loki', email: 'loki@learnboost.com' }, + { name: 'jane', email: 'jane@learnboost.com' } +]; + +app.get('/', function(req, res){ + res.render('users', { + users: users, + title: "EJS example", + header: "Some users" + }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/ejs/public/stylesheets/style.css b/tests/express-tests/examples/ejs/public/stylesheets/style.css new file mode 100644 index 00000000..db60b89f --- /dev/null +++ b/tests/express-tests/examples/ejs/public/stylesheets/style.css @@ -0,0 +1,4 @@ +body { + padding: 50px 80px; + font: 14px "Helvetica Neue", "Lucida Grande", Arial, sans-serif; +} diff --git a/tests/express-tests/examples/ejs/views/footer.html b/tests/express-tests/examples/ejs/views/footer.html new file mode 100644 index 00000000..308b1d01 --- /dev/null +++ b/tests/express-tests/examples/ejs/views/footer.html @@ -0,0 +1,2 @@ + + diff --git a/tests/express-tests/examples/ejs/views/header.html b/tests/express-tests/examples/ejs/views/header.html new file mode 100644 index 00000000..c642a15f --- /dev/null +++ b/tests/express-tests/examples/ejs/views/header.html @@ -0,0 +1,9 @@ + + + + + + <%= title %> + + + diff --git a/tests/express-tests/examples/ejs/views/users.html b/tests/express-tests/examples/ejs/views/users.html new file mode 100644 index 00000000..dad24625 --- /dev/null +++ b/tests/express-tests/examples/ejs/views/users.html @@ -0,0 +1,10 @@ +<%- include('header.html') -%> + +

Users

+ + +<%- include('footer.html') -%> diff --git a/tests/express-tests/examples/error-pages/index.js b/tests/express-tests/examples/error-pages/index.js new file mode 100644 index 00000000..00f3b604 --- /dev/null +++ b/tests/express-tests/examples/error-pages/index.js @@ -0,0 +1,103 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var path = require('path'); +var app = module.exports = express(); +var logger = require('morgan'); +var silent = process.env.NODE_ENV === 'test' + +// general config +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// our custom "verbose errors" setting +// which we can use in the templates +// via settings['verbose errors'] +app.enable('verbose errors'); + +// disable them in production +// use $ NODE_ENV=production node examples/error-pages +if (app.settings.env === 'production') app.disable('verbose errors') + +silent || app.use(logger('dev')); + +// Routes + +app.get('/', function(req, res){ + res.render('index.ejs'); +}); + +app.get('/404', function(req, res, next){ + // trigger a 404 since no other middleware + // will match /404 after this one, and we're not + // responding here + next(); +}); + +app.get('/403', function(req, res, next){ + // trigger a 403 error + var err = new Error('not allowed!'); + err.status = 403; + next(err); +}); + +app.get('/500', function(req, res, next){ + // trigger a generic (500) error + next(new Error('keyboard cat!')); +}); + +// Error handlers + +// Since this is the last non-error-handling +// middleware use()d, we assume 404, as nothing else +// responded. + +// $ curl http://localhost:3000/notfound +// $ curl http://localhost:3000/notfound -H "Accept: application/json" +// $ curl http://localhost:3000/notfound -H "Accept: text/plain" + +app.use(function(req, res, next){ + res.status(404); + + res.format({ + html: function () { + res.render('404', { url: req.url }) + }, + json: function () { + res.json({ error: 'Not found' }) + }, + default: function () { + res.type('txt').send('Not found') + } + }) +}); + +// error-handling middleware, take the same form +// as regular middleware, however they require an +// arity of 4, aka the signature (err, req, res, next). +// when connect has an error, it will invoke ONLY error-handling +// middleware. + +// If we were to next() here any remaining non-error-handling +// middleware would then be executed, or if we next(err) to +// continue passing the error, only error-handling middleware +// would remain being executed, however here +// we simply respond with an error page. + +app.use(function(err, req, res, next){ + // we may use properties of the error object + // here and next(err) appropriately, or if + // we possibly recovered from the error, simply next(). + res.status(err.status || 500); + res.render('500', { error: err }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/error-pages/views/404.ejs b/tests/express-tests/examples/error-pages/views/404.ejs new file mode 100644 index 00000000..d992ce02 --- /dev/null +++ b/tests/express-tests/examples/error-pages/views/404.ejs @@ -0,0 +1,3 @@ +<%- include('error_header') -%> +

Cannot find <%= url %>

+<%- include('footer') -%> diff --git a/tests/express-tests/examples/error-pages/views/500.ejs b/tests/express-tests/examples/error-pages/views/500.ejs new file mode 100644 index 00000000..c9d855f8 --- /dev/null +++ b/tests/express-tests/examples/error-pages/views/500.ejs @@ -0,0 +1,8 @@ +<%- include('error_header') -%> +

Error: <%= error.message %>

+<% if (settings['verbose errors']) { %> +
<%= error.stack %>
+<% } else { %> +

An error occurred!

+<% } %> +<%- include('footer') -%> diff --git a/tests/express-tests/examples/error-pages/views/error_header.ejs b/tests/express-tests/examples/error-pages/views/error_header.ejs new file mode 100644 index 00000000..b2451ab3 --- /dev/null +++ b/tests/express-tests/examples/error-pages/views/error_header.ejs @@ -0,0 +1,10 @@ + + + + + +Error + + + +

An error occurred!

diff --git a/tests/express-tests/examples/error-pages/views/footer.ejs b/tests/express-tests/examples/error-pages/views/footer.ejs new file mode 100644 index 00000000..308b1d01 --- /dev/null +++ b/tests/express-tests/examples/error-pages/views/footer.ejs @@ -0,0 +1,2 @@ + + diff --git a/tests/express-tests/examples/error-pages/views/index.ejs b/tests/express-tests/examples/error-pages/views/index.ejs new file mode 100644 index 00000000..ae8c9282 --- /dev/null +++ b/tests/express-tests/examples/error-pages/views/index.ejs @@ -0,0 +1,20 @@ + + + + + +Custom Pages Example + + + +

My Site

+

Pages Example

+ + + + + diff --git a/tests/express-tests/examples/error/index.js b/tests/express-tests/examples/error/index.js new file mode 100644 index 00000000..1e5dca41 --- /dev/null +++ b/tests/express-tests/examples/error/index.js @@ -0,0 +1,53 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var logger = require('morgan'); +var app = module.exports = express(); +var test = app.get('env') === 'test' + +if (!test) app.use(logger('dev')); + +// error handling middleware have an arity of 4 +// instead of the typical (req, res, next), +// otherwise they behave exactly like regular +// middleware, you may have several of them, +// in different orders etc. + +function error(err, req, res, next) { + // log it + if (!test) console.error(err.stack); + + // respond with 500 "Internal Server Error". + res.status(500); + res.send('Internal Server Error'); +} + +app.get('/', function () { + // Caught and passed down to the errorHandler middleware + throw new Error('something broke!'); +}); + +app.get('/next', function(req, res, next){ + // We can also pass exceptions to next() + // The reason for process.nextTick() is to show that + // next() can be called inside an async operation, + // in real life it can be a DB read or HTTP request. + process.nextTick(function(){ + next(new Error('oh no!')); + }); +}); + +// the error handler is placed after routes +// if it were above it would not receive errors +// from app.get() etc +app.use(error); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/hello-world/index.js b/tests/express-tests/examples/hello-world/index.js new file mode 100644 index 00000000..9cb9e230 --- /dev/null +++ b/tests/express-tests/examples/hello-world/index.js @@ -0,0 +1,15 @@ +'use strict' + +var express = require("express"); + +var app = module.exports = express() + +app.get('/', function(req, res){ + res.send('Hello World'); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/markdown/index.js b/tests/express-tests/examples/markdown/index.js new file mode 100644 index 00000000..28791207 --- /dev/null +++ b/tests/express-tests/examples/markdown/index.js @@ -0,0 +1,44 @@ +'use strict' + +/** + * Module dependencies. + */ + +var escapeHtml = require('escape-html'); +var express = require("express"); +var fs = require('fs'); +var marked = require('marked'); +var path = require('path'); + +var app = module.exports = express(); + +// register .md as an engine in express view system + +app.engine('md', function(path, options, fn){ + fs.readFile(path, 'utf8', function(err, str){ + if (err) return fn(err); + var html = marked.parse(str).replace(/\{([^}]+)\}/g, function(_, name){ + return escapeHtml(options[name] || ''); + }); + fn(null, html); + }); +}); + +app.set('views', path.join(__dirname, 'views')); + +// make it the default, so we don't need .md +app.set('view engine', 'md'); + +app.get('/', function(req, res){ + res.render('index', { title: 'Markdown Example' }); +}); + +app.get('/fail', function(req, res){ + res.render('missing', { title: 'Markdown Example' }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/markdown/views/index.md b/tests/express-tests/examples/markdown/views/index.md new file mode 100644 index 00000000..b0b2e717 --- /dev/null +++ b/tests/express-tests/examples/markdown/views/index.md @@ -0,0 +1,4 @@ + +# {title} + +Just an example view rendered with _markdown_. \ No newline at end of file diff --git a/tests/express-tests/examples/multi-router/controllers/api_v1.js b/tests/express-tests/examples/multi-router/controllers/api_v1.js new file mode 100644 index 00000000..f7aac960 --- /dev/null +++ b/tests/express-tests/examples/multi-router/controllers/api_v1.js @@ -0,0 +1,15 @@ +'use strict' + +var express = require("express"); + +var apiv1 = express.Router(); + +apiv1.get('/', function(req, res) { + res.send('Hello from APIv1 root route.'); +}); + +apiv1.get('/users', function(req, res) { + res.send('List of APIv1 users.'); +}); + +module.exports = apiv1; diff --git a/tests/express-tests/examples/multi-router/controllers/api_v2.js b/tests/express-tests/examples/multi-router/controllers/api_v2.js new file mode 100644 index 00000000..5d292fc2 --- /dev/null +++ b/tests/express-tests/examples/multi-router/controllers/api_v2.js @@ -0,0 +1,15 @@ +'use strict' + +var express = require("express"); + +var apiv2 = express.Router(); + +apiv2.get('/', function(req, res) { + res.send('Hello from APIv2 root route.'); +}); + +apiv2.get('/users', function(req, res) { + res.send('List of APIv2 users.'); +}); + +module.exports = apiv2; diff --git a/tests/express-tests/examples/multi-router/index.js b/tests/express-tests/examples/multi-router/index.js new file mode 100644 index 00000000..bb26c5bf --- /dev/null +++ b/tests/express-tests/examples/multi-router/index.js @@ -0,0 +1,18 @@ +'use strict' + +var express = require("express"); + +var app = module.exports = express(); + +app.use('/api/v1', require('./controllers/api_v1')); +app.use('/api/v2', require('./controllers/api_v2')); + +app.get('/', function(req, res) { + res.send('Hello from root route.') +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/mvc/controllers/main/index.js b/tests/express-tests/examples/mvc/controllers/main/index.js new file mode 100644 index 00000000..74cde191 --- /dev/null +++ b/tests/express-tests/examples/mvc/controllers/main/index.js @@ -0,0 +1,5 @@ +'use strict' + +exports.index = function(req, res){ + res.redirect('/users'); +}; diff --git a/tests/express-tests/examples/mvc/controllers/pet/index.js b/tests/express-tests/examples/mvc/controllers/pet/index.js new file mode 100644 index 00000000..214160f9 --- /dev/null +++ b/tests/express-tests/examples/mvc/controllers/pet/index.js @@ -0,0 +1,31 @@ +'use strict' + +/** + * Module dependencies. + */ + +var db = require('../../db'); + +exports.engine = 'ejs'; + +exports.before = function(req, res, next){ + var pet = db.pets[req.params.pet_id]; + if (!pet) return next('route'); + req.pet = pet; + next(); +}; + +exports.show = function(req, res, next){ + res.render('show', { pet: req.pet }); +}; + +exports.edit = function(req, res, next){ + res.render('edit', { pet: req.pet }); +}; + +exports.update = function(req, res, next){ + var body = req.body; + req.pet.name = body.pet.name; + res.message('Information updated!'); + res.redirect('/pet/' + req.pet.id); +}; diff --git a/tests/express-tests/examples/mvc/controllers/pet/views/edit.ejs b/tests/express-tests/examples/mvc/controllers/pet/views/edit.ejs new file mode 100644 index 00000000..655666e0 --- /dev/null +++ b/tests/express-tests/examples/mvc/controllers/pet/views/edit.ejs @@ -0,0 +1,17 @@ + + + + + + +Edit <%= pet.name %> + + + +

<%= pet.name %>

+
+ + +
+ + diff --git a/tests/express-tests/examples/mvc/controllers/pet/views/show.ejs b/tests/express-tests/examples/mvc/controllers/pet/views/show.ejs new file mode 100644 index 00000000..7e1e338e --- /dev/null +++ b/tests/express-tests/examples/mvc/controllers/pet/views/show.ejs @@ -0,0 +1,15 @@ + + + + + + +<%= pet.name %> + + + +

<%= pet.name %> edit

+ +

You are viewing <%= pet.name %>

+ + diff --git a/tests/express-tests/examples/mvc/controllers/user-pet/index.js b/tests/express-tests/examples/mvc/controllers/user-pet/index.js new file mode 100644 index 00000000..42a29ade --- /dev/null +++ b/tests/express-tests/examples/mvc/controllers/user-pet/index.js @@ -0,0 +1,22 @@ +'use strict' + +/** + * Module dependencies. + */ + +var db = require('../../db'); + +exports.name = 'pet'; +exports.prefix = '/user/:user_id'; + +exports.create = function(req, res, next){ + var id = req.params.user_id; + var user = db.users[id]; + var body = req.body; + if (!user) return next('route'); + var pet = { name: body.pet.name }; + pet.id = db.pets.push(pet) - 1; + user.pets.push(pet); + res.message('Added pet ' + body.pet.name); + res.redirect('/user/' + id); +}; diff --git a/tests/express-tests/examples/mvc/controllers/user/index.js b/tests/express-tests/examples/mvc/controllers/user/index.js new file mode 100644 index 00000000..ec3ae4c8 --- /dev/null +++ b/tests/express-tests/examples/mvc/controllers/user/index.js @@ -0,0 +1,41 @@ +'use strict' + +/** + * Module dependencies. + */ + +var db = require('../../db'); + +exports.engine = 'hbs'; + +exports.before = function(req, res, next){ + var id = req.params.user_id; + if (!id) return next(); + // pretend to query a database... + process.nextTick(function(){ + req.user = db.users[id]; + // cant find that user + if (!req.user) return next('route'); + // found it, move on to the routes + next(); + }); +}; + +exports.list = function(req, res, next){ + res.render('list', { users: db.users }); +}; + +exports.edit = function(req, res, next){ + res.render('edit', { user: req.user }); +}; + +exports.show = function(req, res, next){ + res.render('show', { user: req.user }); +}; + +exports.update = function(req, res, next){ + var body = req.body; + req.user.name = body.user.name; + res.message('Information updated!'); + res.redirect('/user/' + req.user.id); +}; diff --git a/tests/express-tests/examples/mvc/controllers/user/views/edit.hbs b/tests/express-tests/examples/mvc/controllers/user/views/edit.hbs new file mode 100644 index 00000000..2be7ddc4 --- /dev/null +++ b/tests/express-tests/examples/mvc/controllers/user/views/edit.hbs @@ -0,0 +1,27 @@ + + + + + + + Edit {{user.name}} + + +

{{user.name}}

+
+ + + +
+ +
+ + + +
+ + diff --git a/tests/express-tests/examples/mvc/controllers/user/views/list.hbs b/tests/express-tests/examples/mvc/controllers/user/views/list.hbs new file mode 100644 index 00000000..448c66f8 --- /dev/null +++ b/tests/express-tests/examples/mvc/controllers/user/views/list.hbs @@ -0,0 +1,18 @@ + + + + + + + Users + + +

Users

+

Click a user below to view their pets.

+ + + diff --git a/tests/express-tests/examples/mvc/controllers/user/views/show.hbs b/tests/express-tests/examples/mvc/controllers/user/views/show.hbs new file mode 100644 index 00000000..f3fccfe0 --- /dev/null +++ b/tests/express-tests/examples/mvc/controllers/user/views/show.hbs @@ -0,0 +1,31 @@ + + + + + + + {{user.name}} + + +

{{user.name}} edit

+ +{{#if hasMessages}} + +{{/if}} + +{{#if user.pets.length}} +

View {{user.name}}'s pets:

+ +{{else}} +

No pets!

+{{/if}} + + diff --git a/tests/express-tests/examples/mvc/db.js b/tests/express-tests/examples/mvc/db.js new file mode 100644 index 00000000..94d1480f --- /dev/null +++ b/tests/express-tests/examples/mvc/db.js @@ -0,0 +1,16 @@ +'use strict' + +// faux database + +var pets = exports.pets = []; + +pets.push({ name: 'Tobi', id: 0 }); +pets.push({ name: 'Loki', id: 1 }); +pets.push({ name: 'Jane', id: 2 }); +pets.push({ name: 'Raul', id: 3 }); + +var users = exports.users = []; + +users.push({ name: 'TJ', pets: [pets[0], pets[1], pets[2]], id: 0 }); +users.push({ name: 'Guillermo', pets: [pets[3]], id: 1 }); +users.push({ name: 'Nathan', pets: [], id: 2 }); diff --git a/tests/express-tests/examples/mvc/index.js b/tests/express-tests/examples/mvc/index.js new file mode 100644 index 00000000..5aef8a18 --- /dev/null +++ b/tests/express-tests/examples/mvc/index.js @@ -0,0 +1,95 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var logger = require('morgan'); +var path = require('path'); +var session = require('express-session'); +var methodOverride = require('method-override'); + +var app = module.exports = express(); + +// set our default template engine to "ejs" +// which prevents the need for using file extensions +app.set('view engine', 'ejs'); + +// set views for error and 404 pages +app.set('views', path.join(__dirname, 'views')); + +// define a custom res.message() method +// which stores messages in the session +app.response.message = function(msg){ + // reference `req.session` via the `this.req` reference + var sess = this.req.session; + // simply add the msg to an array for later + sess.messages = sess.messages || []; + sess.messages.push(msg); + return this; +}; + +// log +if (!module.parent) app.use(logger('dev')); + +// serve static files +app.use(express.static(path.join(__dirname, 'public'))); + +// session support +app.use(session({ + resave: false, // don't save session if unmodified + saveUninitialized: false, // don't create session until something stored + secret: 'some secret here' +})); + +// parse request bodies (req.body) +app.use(express.urlencoded({ extended: true })) + +// allow overriding methods in query (?_method=put) +app.use(methodOverride('_method')); + +// expose the "messages" local variable when views are rendered +app.use(function(req, res, next){ + var msgs = req.session.messages || []; + + // expose "messages" local variable + res.locals.messages = msgs; + + // expose "hasMessages" + res.locals.hasMessages = !! msgs.length; + + /* This is equivalent: + res.locals({ + messages: msgs, + hasMessages: !! msgs.length + }); + */ + + next(); + // empty or "flush" the messages so they + // don't build up + req.session.messages = []; +}); + +// load controllers +require('./lib/boot')(app, { verbose: !module.parent }); + +app.use(function(err, req, res, next){ + // log it + if (!module.parent) console.error(err.stack); + + // error page + res.status(500).render('5xx'); +}); + +// assume 404 since no middleware responded +app.use(function(req, res, next){ + res.status(404).render('404', { url: req.originalUrl }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/mvc/lib/boot.js b/tests/express-tests/examples/mvc/lib/boot.js new file mode 100644 index 00000000..1568f296 --- /dev/null +++ b/tests/express-tests/examples/mvc/lib/boot.js @@ -0,0 +1,83 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var fs = require('fs'); +var path = require('path'); + +module.exports = function(parent, options){ + var dir = path.join(__dirname, '..', 'controllers'); + var verbose = options.verbose; + fs.readdirSync(dir).forEach(function(name){ + var file = path.join(dir, name) + if (!fs.statSync(file).isDirectory()) return; + verbose && console.log('\n %s:', name); + var obj = require(file); + var name = obj.name || name; + var prefix = obj.prefix || ''; + var app = express(); + var handler; + var method; + var url; + + // allow specifying the view engine + if (obj.engine) app.set('view engine', obj.engine); + app.set('views', path.join(__dirname, '..', 'controllers', name, 'views')); + + // generate routes based + // on the exported methods + for (var key in obj) { + // "reserved" exports + if (~['name', 'prefix', 'engine', 'before'].indexOf(key)) continue; + // route exports + switch (key) { + case 'show': + method = 'get'; + url = '/' + name + '/:' + name + '_id'; + break; + case 'list': + method = 'get'; + url = '/' + name + 's'; + break; + case 'edit': + method = 'get'; + url = '/' + name + '/:' + name + '_id/edit'; + break; + case 'update': + method = 'put'; + url = '/' + name + '/:' + name + '_id'; + break; + case 'create': + method = 'post'; + url = '/' + name; + break; + case 'index': + method = 'get'; + url = '/'; + break; + default: + /* istanbul ignore next */ + throw new Error('unrecognized route: ' + name + '.' + key); + } + + // setup + handler = obj[key]; + url = prefix + url; + + // before middleware support + if (obj.before) { + app[method](url, obj.before, handler); + verbose && console.log(' %s %s -> before -> %s', method.toUpperCase(), url, key); + } else { + app[method](url, handler); + verbose && console.log(' %s %s -> %s', method.toUpperCase(), url, key); + } + } + + // mount the app + parent.use(app); + }); +}; diff --git a/tests/express-tests/examples/mvc/public/style.css b/tests/express-tests/examples/mvc/public/style.css new file mode 100644 index 00000000..8a23f9d4 --- /dev/null +++ b/tests/express-tests/examples/mvc/public/style.css @@ -0,0 +1,14 @@ +body { + padding: 50px; + font: 16px "Helvetica Neue", Helvetica, Arial, sans-serif; +} +a { + color: #107aff; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +h1 a { + font-size: 16px; +} diff --git a/tests/express-tests/examples/mvc/views/404.ejs b/tests/express-tests/examples/mvc/views/404.ejs new file mode 100644 index 00000000..21a86f8a --- /dev/null +++ b/tests/express-tests/examples/mvc/views/404.ejs @@ -0,0 +1,13 @@ + + + + + + Not Found + + + +

404: Not Found

+

Sorry we can't find <%= url %>

+ + diff --git a/tests/express-tests/examples/mvc/views/5xx.ejs b/tests/express-tests/examples/mvc/views/5xx.ejs new file mode 100644 index 00000000..190f5805 --- /dev/null +++ b/tests/express-tests/examples/mvc/views/5xx.ejs @@ -0,0 +1,13 @@ + + + + + + Internal Server Error + + + +

500: Internal Server Error

+

Looks like something blew up!

+ + diff --git a/tests/express-tests/examples/online/index.js b/tests/express-tests/examples/online/index.js new file mode 100644 index 00000000..8c304fb8 --- /dev/null +++ b/tests/express-tests/examples/online/index.js @@ -0,0 +1,61 @@ +'use strict' + +// install redis first: +// https://redis.io/ + +// then: +// $ npm install redis online +// $ redis-server + +/** + * Module dependencies. + */ + +var express = require("express"); +var online = require('online'); +var redis = require('redis'); +var db = redis.createClient(); + +// online + +online = online(db); + +// app + +var app = express(); + +// activity tracking, in this case using +// the UA string, you would use req.user.id etc + +app.use(function(req, res, next){ + // fire-and-forget + online.add(req.headers['user-agent']); + next(); +}); + +/** + * List helper. + */ + +function list(ids) { + return ''; +} + +/** + * GET users online. + */ + +app.get('/', function(req, res, next){ + online.last(5, function(err, ids){ + if (err) return next(err); + res.send('

Users online: ' + ids.length + '

' + list(ids)); + }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/params/index.js b/tests/express-tests/examples/params/index.js new file mode 100644 index 00000000..9b47157b --- /dev/null +++ b/tests/express-tests/examples/params/index.js @@ -0,0 +1,73 @@ +'use strict' + +/** + * Module dependencies. + */ + +var createError = require('http-errors') +var express = require("express"); +var app = module.exports = express(); + +// Faux database + +var users = [ + { name: 'tj' } + , { name: 'tobi' } + , { name: 'loki' } + , { name: 'jane' } + , { name: 'bandit' } +]; + +// Convert :to and :from to integers + +app.param(['to', 'from'], function(req, res, next, num, name){ + req.params[name] = parseInt(num, 10); + if( isNaN(req.params[name]) ){ + next(createError(400, 'failed to parseInt '+num)); + } else { + next(); + } +}); + +// Load user by id + +app.param('user', function(req, res, next, id){ + if (req.user = users[id]) { + next(); + } else { + next(createError(404, 'failed to find user')); + } +}); + +/** + * GET index. + */ + +app.get('/', function(req, res){ + res.send('Visit /user/0 or /users/0-2'); +}); + +/** + * GET :user. + */ + +app.get('/user/:user', function (req, res) { + res.send('user ' + req.user.name); +}); + +/** + * GET users :from - :to. + */ + +app.get('/users/:from-:to', function (req, res) { + var from = req.params.from; + var to = req.params.to; + var names = users.map(function(user){ return user.name; }); + res.send('users ' + names.slice(from, to + 1).join(', ')); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/resource/index.js b/tests/express-tests/examples/resource/index.js new file mode 100644 index 00000000..8f73c19a --- /dev/null +++ b/tests/express-tests/examples/resource/index.js @@ -0,0 +1,95 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); + +var app = module.exports = express(); + +// Ad-hoc example resource method + +app.resource = function(path, obj) { + this.get(path, obj.index); + this.get(path + '/:a..:b.:format?', function(req, res){ + var a = parseInt(req.params.a, 10); + var b = parseInt(req.params.b, 10); + var format = req.params.format; + obj.range(req, res, a, b, format); + }); + this.get(path + '/:id', obj.show); + this.delete(path + '/:id', function(req, res){ + var id = parseInt(req.params.id, 10); + obj.destroy(req, res, id); + }); +}; + +// Fake records + +var users = [ + { name: 'tj' } + , { name: 'ciaran' } + , { name: 'aaron' } + , { name: 'guillermo' } + , { name: 'simon' } + , { name: 'tobi' } +]; + +// Fake controller. + +var User = { + index: function(req, res){ + res.send(users); + }, + show: function(req, res){ + res.send(users[req.params.id] || { error: 'Cannot find user' }); + }, + destroy: function(req, res, id){ + var destroyed = id in users; + delete users[id]; + res.send(destroyed ? 'destroyed' : 'Cannot find user'); + }, + range: function(req, res, a, b, format){ + var range = users.slice(a, b + 1); + switch (format) { + case 'json': + res.send(range); + break; + case 'html': + default: + var html = ''; + res.send(html); + break; + } + } +}; + +// curl http://localhost:3000/users -- responds with all users +// curl http://localhost:3000/users/1 -- responds with user 1 +// curl http://localhost:3000/users/4 -- responds with error +// curl http://localhost:3000/users/1..3 -- responds with several users +// curl -X DELETE http://localhost:3000/users/1 -- deletes the user + +app.resource('/users', User); + +app.get('/', function(req, res){ + res.send([ + '

Examples:

' + ].join('\n')); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/route-map/index.js b/tests/express-tests/examples/route-map/index.js new file mode 100644 index 00000000..657b92e0 --- /dev/null +++ b/tests/express-tests/examples/route-map/index.js @@ -0,0 +1,75 @@ +'use strict' + +/** + * Module dependencies. + */ + +var escapeHtml = require('escape-html') +var express = require("express"); + +var verbose = process.env.NODE_ENV !== 'test' + +var app = module.exports = express(); + +app.map = function(a, route){ + route = route || ''; + for (var key in a) { + switch (typeof a[key]) { + // { '/path': { ... }} + case 'object': + app.map(a[key], route + key); + break; + // get: function(){ ... } + case 'function': + if (verbose) console.log('%s %s', key, route); + app[key](route, a[key]); + break; + } + } +}; + +var users = { + list: function(req, res){ + res.send('user list'); + }, + + get: function(req, res){ + res.send('user ' + escapeHtml(req.params.uid)) + }, + + delete: function(req, res){ + res.send('delete users'); + } +}; + +var pets = { + list: function(req, res){ + res.send('user ' + escapeHtml(req.params.uid) + '\'s pets') + }, + + delete: function(req, res){ + res.send('delete ' + escapeHtml(req.params.uid) + '\'s pet ' + escapeHtml(req.params.pid)) + } +}; + +app.map({ + '/users': { + get: users.list, + delete: users.delete, + '/:uid': { + get: users.get, + '/pets': { + get: pets.list, + '/:pid': { + delete: pets.delete + } + } + } + } +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/route-middleware/index.js b/tests/express-tests/examples/route-middleware/index.js new file mode 100644 index 00000000..c2f69349 --- /dev/null +++ b/tests/express-tests/examples/route-middleware/index.js @@ -0,0 +1,90 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); + +var app = express(); + +// Example requests: +// curl http://localhost:3000/user/0 +// curl http://localhost:3000/user/0/edit +// curl http://localhost:3000/user/1 +// curl http://localhost:3000/user/1/edit (unauthorized since this is not you) +// curl -X DELETE http://localhost:3000/user/0 (unauthorized since you are not an admin) + +// Dummy users +var users = [ + { id: 0, name: 'tj', email: 'tj@vision-media.ca', role: 'member' } + , { id: 1, name: 'ciaran', email: 'ciaranj@gmail.com', role: 'member' } + , { id: 2, name: 'aaron', email: 'aaron.heckmann+github@gmail.com', role: 'admin' } +]; + +function loadUser(req, res, next) { + // You would fetch your user from the db + var user = users[req.params.id]; + if (user) { + req.user = user; + next(); + } else { + next(new Error('Failed to load user ' + req.params.id)); + } +} + +function andRestrictToSelf(req, res, next) { + // If our authenticated user is the user we are viewing + // then everything is fine :) + if (req.authenticatedUser.id === req.user.id) { + next(); + } else { + // You may want to implement specific exceptions + // such as UnauthorizedError or similar so that you + // can handle these can be special-cased in an error handler + // (view ./examples/pages for this) + next(new Error('Unauthorized')); + } +} + +function andRestrictTo(role) { + return function(req, res, next) { + if (req.authenticatedUser.role === role) { + next(); + } else { + next(new Error('Unauthorized')); + } + } +} + +// Middleware for faux authentication +// you would of course implement something real, +// but this illustrates how an authenticated user +// may interact with middleware + +app.use(function(req, res, next){ + req.authenticatedUser = users[0]; + next(); +}); + +app.get('/', function(req, res){ + res.redirect('/user/0'); +}); + +app.get('/user/:id', loadUser, function(req, res){ + res.send('Viewing user ' + req.user.name); +}); + +app.get('/user/:id/edit', loadUser, andRestrictToSelf, function(req, res){ + res.send('Editing user ' + req.user.name); +}); + +app.delete('/user/:id', loadUser, andRestrictTo('admin'), function(req, res){ + res.send('Deleted user ' + req.user.name); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/route-separation/index.js b/tests/express-tests/examples/route-separation/index.js new file mode 100644 index 00000000..661a6218 --- /dev/null +++ b/tests/express-tests/examples/route-separation/index.js @@ -0,0 +1,55 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var path = require('path'); +var app = express(); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); +var methodOverride = require('method-override'); +var site = require('./site'); +var post = require('./post'); +var user = require('./user'); + +module.exports = app; + +// Config + +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + +/* istanbul ignore next */ +if (!module.parent) { + app.use(logger('dev')); +} + +app.use(methodOverride('_method')); +app.use(cookieParser()); +app.use(express.urlencoded({ extended: true })) +app.use(express.static(path.join(__dirname, 'public'))); + +// General + +app.get('/', site.index); + +// User + +app.get('/users', user.list); +app.all('/user/:id/:op?', user.load); +app.get('/user/:id', user.view); +app.get('/user/:id/view', user.view); +app.get('/user/:id/edit', user.edit); +app.put('/user/:id/edit', user.update); + +// Posts + +app.get('/posts', post.list); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/route-separation/post.js b/tests/express-tests/examples/route-separation/post.js new file mode 100644 index 00000000..3a8e3a2d --- /dev/null +++ b/tests/express-tests/examples/route-separation/post.js @@ -0,0 +1,13 @@ +'use strict' + +// Fake posts database + +var posts = [ + { title: 'Foo', body: 'some foo bar' }, + { title: 'Foo bar', body: 'more foo bar' }, + { title: 'Foo bar baz', body: 'more foo bar baz' } +]; + +exports.list = function(req, res){ + res.render('posts', { title: 'Posts', posts: posts }); +}; diff --git a/tests/express-tests/examples/route-separation/public/style.css b/tests/express-tests/examples/route-separation/public/style.css new file mode 100644 index 00000000..d699784c --- /dev/null +++ b/tests/express-tests/examples/route-separation/public/style.css @@ -0,0 +1,24 @@ +body { + padding: 50px; + font: 14px "Helvetica Neue", Arial, sans-serif; +} +a { + color: #00AEFF; + text-decoration: none; +} +a.edit { + color: #000; + opacity: .3; +} +a.edit::before { + content: ' ['; +} +a.edit::after { + content: ']'; +} +dt { + font-weight: bold; +} +dd { + margin: 15px; +} \ No newline at end of file diff --git a/tests/express-tests/examples/route-separation/site.js b/tests/express-tests/examples/route-separation/site.js new file mode 100644 index 00000000..aee36d1b --- /dev/null +++ b/tests/express-tests/examples/route-separation/site.js @@ -0,0 +1,5 @@ +'use strict' + +exports.index = function(req, res){ + res.render('index', { title: 'Route Separation Example' }); +}; diff --git a/tests/express-tests/examples/route-separation/user.js b/tests/express-tests/examples/route-separation/user.js new file mode 100644 index 00000000..1c2aec7c --- /dev/null +++ b/tests/express-tests/examples/route-separation/user.js @@ -0,0 +1,47 @@ +'use strict' + +// Fake user database + +var users = [ + { name: 'TJ', email: 'tj@vision-media.ca' }, + { name: 'Tobi', email: 'tobi@vision-media.ca' } +]; + +exports.list = function(req, res){ + res.render('users', { title: 'Users', users: users }); +}; + +exports.load = function(req, res, next){ + var id = req.params.id; + req.user = users[id]; + if (req.user) { + next(); + } else { + var err = new Error('cannot find user ' + id); + err.status = 404; + next(err); + } +}; + +exports.view = function(req, res){ + res.render('users/view', { + title: 'Viewing user ' + req.user.name, + user: req.user + }); +}; + +exports.edit = function(req, res){ + res.render('users/edit', { + title: 'Editing user ' + req.user.name, + user: req.user + }); +}; + +exports.update = function(req, res){ + // Normally you would handle all kinds of + // validation and save back to the db + var user = req.body.user; + req.user.name = user.name; + req.user.email = user.email; + res.redirect('back'); +}; diff --git a/tests/express-tests/examples/route-separation/views/footer.ejs b/tests/express-tests/examples/route-separation/views/footer.ejs new file mode 100644 index 00000000..308b1d01 --- /dev/null +++ b/tests/express-tests/examples/route-separation/views/footer.ejs @@ -0,0 +1,2 @@ + + diff --git a/tests/express-tests/examples/route-separation/views/header.ejs b/tests/express-tests/examples/route-separation/views/header.ejs new file mode 100644 index 00000000..4300325e --- /dev/null +++ b/tests/express-tests/examples/route-separation/views/header.ejs @@ -0,0 +1,9 @@ + + + + + + <%= title %> + + + diff --git a/tests/express-tests/examples/route-separation/views/index.ejs b/tests/express-tests/examples/route-separation/views/index.ejs new file mode 100644 index 00000000..7fd75879 --- /dev/null +++ b/tests/express-tests/examples/route-separation/views/index.ejs @@ -0,0 +1,10 @@ +<%- include('header') -%> + +

<%= title %>

+ + + +<%- include('footer') -%> diff --git a/tests/express-tests/examples/route-separation/views/posts/index.ejs b/tests/express-tests/examples/route-separation/views/posts/index.ejs new file mode 100644 index 00000000..cbcebffe --- /dev/null +++ b/tests/express-tests/examples/route-separation/views/posts/index.ejs @@ -0,0 +1,12 @@ +<%- include('../header') -%> + +

Posts

+ +
+ <% posts.forEach(function(post) { %> +
<%= post.title %>
+
<%= post.body %>
+ <% }) %> +
+ +<%- include('../footer') -%> diff --git a/tests/express-tests/examples/route-separation/views/users/edit.ejs b/tests/express-tests/examples/route-separation/views/users/edit.ejs new file mode 100644 index 00000000..6df78a95 --- /dev/null +++ b/tests/express-tests/examples/route-separation/views/users/edit.ejs @@ -0,0 +1,23 @@ +<%- include('../header') -%> + +

Editing <%= user.name %>

+ +
+
+

+ Name: + +

+ +

+ Email: + +

+ +

+ +

+
+
+ +<%- include('../footer') -%> diff --git a/tests/express-tests/examples/route-separation/views/users/index.ejs b/tests/express-tests/examples/route-separation/views/users/index.ejs new file mode 100644 index 00000000..949412a2 --- /dev/null +++ b/tests/express-tests/examples/route-separation/views/users/index.ejs @@ -0,0 +1,14 @@ +<%- include('../header') -%> + +

<%= title %>

+ +
+ <% users.forEach(function(user, index) { %> +
  • + <%= user.name %> + edit +
  • + <% }) %> +
    + +<%- include('../footer') -%> diff --git a/tests/express-tests/examples/route-separation/views/users/view.ejs b/tests/express-tests/examples/route-separation/views/users/view.ejs new file mode 100644 index 00000000..457bd539 --- /dev/null +++ b/tests/express-tests/examples/route-separation/views/users/view.ejs @@ -0,0 +1,9 @@ +<%- include('../header') -%> + +

    <%= user.name %>

    + +
    +

    Email: <%= user.email %>

    +
    + +<%- include('../footer') -%> diff --git a/tests/express-tests/examples/search/index.js b/tests/express-tests/examples/search/index.js new file mode 100644 index 00000000..7836e60c --- /dev/null +++ b/tests/express-tests/examples/search/index.js @@ -0,0 +1,61 @@ +'use strict' + +// install redis first: +// https://redis.io/ + +// then: +// $ npm install redis +// $ redis-server + +/** + * Module dependencies. + */ + +var express = require("express"); +var path = require('path'); +var redis = require('redis'); + +var db = redis.createClient(); + +// npm install redis + +var app = express(); + +app.use(express.static(path.join(__dirname, 'public'))); + +// populate search + +db.sadd('ferret', 'tobi'); +db.sadd('ferret', 'loki'); +db.sadd('ferret', 'jane'); +db.sadd('cat', 'manny'); +db.sadd('cat', 'luna'); + +/** + * GET search for :query. + */ + +app.get('/search/:query?', function(req, res){ + var query = req.params.query; + db.smembers(query, function(err, vals){ + if (err) return res.send(500); + res.send(vals); + }); +}); + +/** + * GET client javascript. Here we use sendFile() + * because serving __dirname with the static() middleware + * would also mean serving our server "index.js" and the "search.jade" + * template. + */ + +app.get('/client.js', function(req, res){ + res.sendFile(path.join(__dirname, 'client.js')); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/search/public/client.js b/tests/express-tests/examples/search/public/client.js new file mode 100644 index 00000000..cd43faf7 --- /dev/null +++ b/tests/express-tests/examples/search/public/client.js @@ -0,0 +1,15 @@ +'use strict' + +var search = document.querySelector('[type=search]'); +var code = document.querySelector('pre'); + +search.addEventListener('keyup', function(){ + var xhr = new XMLHttpRequest; + xhr.open('GET', '/search/' + search.value, true); + xhr.onreadystatechange = function(){ + if (xhr.readyState === 4) { + code.textContent = xhr.responseText; + } + }; + xhr.send(); +}, false); diff --git a/tests/express-tests/examples/search/public/index.html b/tests/express-tests/examples/search/public/index.html new file mode 100644 index 00000000..7353644b --- /dev/null +++ b/tests/express-tests/examples/search/public/index.html @@ -0,0 +1,21 @@ + + + + + + Search example + + + +

    Search

    +

    Try searching for "ferret" or "cat".

    + +
    
    +  
    +
    +
    diff --git a/tests/express-tests/examples/session/index.js b/tests/express-tests/examples/session/index.js
    new file mode 100644
    index 00000000..ca3bfcd9
    --- /dev/null
    +++ b/tests/express-tests/examples/session/index.js
    @@ -0,0 +1,37 @@
    +'use strict'
    +
    +// install redis first:
    +// https://redis.io/
    +
    +// then:
    +// $ npm install redis
    +// $ redis-server
    +
    +var express = require("express");
    +var session = require('express-session');
    +
    +var app = express();
    +
    +// Populates req.session
    +app.use(session({
    +  resave: false, // don't save session if unmodified
    +  saveUninitialized: false, // don't create session until something stored
    +  secret: 'keyboard cat'
    +}));
    +
    +app.get('/', function(req, res){
    +  var body = '';
    +  if (req.session.views) {
    +    ++req.session.views;
    +  } else {
    +    req.session.views = 1;
    +    body += '

    First time visiting? view this page in several browsers :)

    '; + } + res.send(body + '

    viewed ' + req.session.views + ' times.

    '); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/session/redis.js b/tests/express-tests/examples/session/redis.js new file mode 100644 index 00000000..f24f4ed9 --- /dev/null +++ b/tests/express-tests/examples/session/redis.js @@ -0,0 +1,39 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var logger = require('morgan'); +var session = require('express-session'); + +// pass the express to the connect redis module +// allowing it to inherit from session.Store +var RedisStore = require('connect-redis')(session); + +var app = express(); + +app.use(logger('dev')); + +// Populates req.session +app.use(session({ + resave: false, // don't save session if unmodified + saveUninitialized: false, // don't create session until something stored + secret: 'keyboard cat', + store: new RedisStore +})); + +app.get('/', function(req, res){ + var body = ''; + if (req.session.views) { + ++req.session.views; + } else { + req.session.views = 1; + body += '

    First time visiting? view this page in several browsers :)

    '; + } + res.send(body + '

    viewed ' + req.session.views + ' times.

    '); +}); + +app.listen(3000); +console.log('Express app started on port 3000'); diff --git a/tests/express-tests/examples/static-files/index.js b/tests/express-tests/examples/static-files/index.js new file mode 100644 index 00000000..7250d118 --- /dev/null +++ b/tests/express-tests/examples/static-files/index.js @@ -0,0 +1,43 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var logger = require('morgan'); +var path = require('path'); +var app = express(); + +// log requests +app.use(logger('dev')); + +// express on its own has no notion +// of a "file". The express.static() +// middleware checks for a file matching +// the `req.path` within the directory +// that you pass it. In this case "GET /js/app.js" +// will look for "./public/js/app.js". + +app.use(express.static(path.join(__dirname, 'public'))); + +// if you wanted to "prefix" you may use +// the mounting feature of Connect, for example +// "GET /static/js/app.js" instead of "GET /js/app.js". +// The mount-path "/static" is simply removed before +// passing control to the express.static() middleware, +// thus it serves the file correctly by ignoring "/static" +app.use('/static', express.static(path.join(__dirname, 'public'))); + +// if for some reason you want to serve files from +// several directories, you can use express.static() +// multiple times! Here we're passing "./public/css", +// this will allow "GET /style.css" instead of "GET /css/style.css": +app.use(express.static(path.join(__dirname, 'public', 'css'))); + +app.listen(3000); +console.log('listening on port 3000'); +console.log('try:'); +console.log(' GET /hello.txt'); +console.log(' GET /js/app.js'); +console.log(' GET /css/style.css'); diff --git a/tests/express-tests/examples/static-files/public/css/style.css b/tests/express-tests/examples/static-files/public/css/style.css new file mode 100644 index 00000000..7fc28f96 --- /dev/null +++ b/tests/express-tests/examples/static-files/public/css/style.css @@ -0,0 +1,3 @@ +body { + +} \ No newline at end of file diff --git a/tests/express-tests/examples/static-files/public/hello.txt b/tests/express-tests/examples/static-files/public/hello.txt new file mode 100644 index 00000000..2b31011c --- /dev/null +++ b/tests/express-tests/examples/static-files/public/hello.txt @@ -0,0 +1 @@ +hey \ No newline at end of file diff --git a/tests/express-tests/examples/static-files/public/js/app.js b/tests/express-tests/examples/static-files/public/js/app.js new file mode 100644 index 00000000..775eb734 --- /dev/null +++ b/tests/express-tests/examples/static-files/public/js/app.js @@ -0,0 +1 @@ +// foo diff --git a/tests/express-tests/examples/vhost/index.js b/tests/express-tests/examples/vhost/index.js new file mode 100644 index 00000000..4311efae --- /dev/null +++ b/tests/express-tests/examples/vhost/index.js @@ -0,0 +1,53 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var logger = require('morgan'); +var vhost = require('vhost'); + +/* +edit /etc/hosts: + +127.0.0.1 foo.example.com +127.0.0.1 bar.example.com +127.0.0.1 example.com +*/ + +// Main server app + +var main = express(); + +if (!module.parent) main.use(logger('dev')); + +main.get('/', function(req, res){ + res.send('Hello from main app!'); +}); + +main.get('/:sub', function(req, res){ + res.send('requested ' + req.params.sub); +}); + +// Redirect app + +var redirect = express(); + +redirect.use(function(req, res){ + if (!module.parent) console.log(req.vhost); + res.redirect('http://example.com:3000/' + req.vhost[0]); +}); + +// Vhost app + +var app = module.exports = express(); + +app.use(vhost('*.example.com', redirect)); // Serves all subdomains via Redirect app +app.use(vhost('example.com', main)); // Serves top level domain via Main server app + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/view-constructor/github-view.js b/tests/express-tests/examples/view-constructor/github-view.js new file mode 100644 index 00000000..43d29336 --- /dev/null +++ b/tests/express-tests/examples/view-constructor/github-view.js @@ -0,0 +1,53 @@ +'use strict' + +/** + * Module dependencies. + */ + +var https = require('https'); +var path = require('path'); +var extname = path.extname; + +/** + * Expose `GithubView`. + */ + +module.exports = GithubView; + +/** + * Custom view that fetches and renders + * remove github templates. You could + * render templates from a database etc. + */ + +function GithubView(name, options){ + this.name = name; + options = options || {}; + this.engine = options.engines[extname(name)]; + // "root" is the app.set('views') setting, however + // in your own implementation you could ignore this + this.path = '/' + options.root + '/master/' + name; +} + +/** + * Render the view. + */ + +GithubView.prototype.render = function(options, fn){ + var self = this; + var opts = { + host: 'raw.githubusercontent.com', + port: 443, + path: this.path, + method: 'GET' + }; + + https.request(opts, function(res) { + var buf = ''; + res.setEncoding('utf8'); + res.on('data', function(str){ buf += str }); + res.on('end', function(){ + self.engine(buf, options, fn); + }); + }).end(); +}; diff --git a/tests/express-tests/examples/view-constructor/index.js b/tests/express-tests/examples/view-constructor/index.js new file mode 100644 index 00000000..c87067d4 --- /dev/null +++ b/tests/express-tests/examples/view-constructor/index.js @@ -0,0 +1,48 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var GithubView = require('./github-view'); +var md = require('marked').parse; + +var app = module.exports = express(); + +// register .md as an engine in express view system +app.engine('md', function(str, options, fn){ + try { + var html = md(str); + html = html.replace(/\{([^}]+)\}/g, function(_, name){ + return options[name] || ''; + }); + fn(null, html); + } catch(err) { + fn(err); + } +}); + +// pointing to a particular github repo to load files from it +app.set('views', 'expressjs/express'); + +// register a new view constructor +app.set('view', GithubView); + +app.get('/', function(req, res){ + // rendering a view relative to the repo. + // app.locals, res.locals, and locals passed + // work like they normally would + res.render('examples/markdown/views/index.md', { title: 'Example' }); +}); + +app.get('/Readme.md', function(req, res){ + // rendering a view from https://github.com/expressjs/express/blob/master/Readme.md + res.render('Readme.md'); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/view-locals/index.js b/tests/express-tests/examples/view-locals/index.js new file mode 100644 index 00000000..10c8a75e --- /dev/null +++ b/tests/express-tests/examples/view-locals/index.js @@ -0,0 +1,155 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); +var path = require('path'); +var User = require('./user'); +var app = express(); + +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +// filter ferrets only + +function ferrets(user) { + return user.species === 'ferret' +} + +// naive nesting approach, +// delegating errors to next(err) +// in order to expose the "count" +// and "users" locals + +app.get('/', function(req, res, next){ + User.count(function(err, count){ + if (err) return next(err); + User.all(function(err, users){ + if (err) return next(err); + res.render('index', { + title: 'Users', + count: count, + users: users.filter(ferrets) + }); + }) + }) +}); + + + + +// this approach is cleaner, +// less nesting and we have +// the variables available +// on the request object + +function count(req, res, next) { + User.count(function(err, count){ + if (err) return next(err); + req.count = count; + next(); + }) +} + +function users(req, res, next) { + User.all(function(err, users){ + if (err) return next(err); + req.users = users; + next(); + }) +} + +app.get('/middleware', count, users, function (req, res) { + res.render('index', { + title: 'Users', + count: req.count, + users: req.users.filter(ferrets) + }); +}); + + + + +// this approach is much like the last +// however we're explicitly exposing +// the locals within each middleware +// +// note that this may not always work +// well, for example here we filter +// the users in the middleware, which +// may not be ideal for our application. +// so in that sense the previous example +// is more flexible with `req.users`. + +function count2(req, res, next) { + User.count(function(err, count){ + if (err) return next(err); + res.locals.count = count; + next(); + }) +} + +function users2(req, res, next) { + User.all(function(err, users){ + if (err) return next(err); + res.locals.users = users.filter(ferrets); + next(); + }) +} + +app.get('/middleware-locals', count2, users2, function (req, res) { + // you can see now how we have much less + // to pass to res.render(). If we have + // several routes related to users this + // can be a great productivity booster + res.render('index', { title: 'Users' }); +}); + +// keep in mind that middleware may be placed anywhere +// and in various combinations, so if you have locals +// that you wish to make available to all subsequent +// middleware/routes you can do something like this: + +/* + +app.use(function(req, res, next){ + res.locals.user = req.user; + res.locals.sess = req.session; + next(); +}); + +*/ + +// or suppose you have some /admin +// "global" local variables: + +/* + +app.use('/api', function(req, res, next){ + res.locals.user = req.user; + res.locals.sess = req.session; + next(); +}); + +*/ + +// the following is effectively the same, +// but uses a route instead: + +/* + +app.all('/api/*', function(req, res, next){ + res.locals.user = req.user; + res.locals.sess = req.session; + next(); +}); + +*/ + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/examples/view-locals/user.js b/tests/express-tests/examples/view-locals/user.js new file mode 100644 index 00000000..aaa6f85f --- /dev/null +++ b/tests/express-tests/examples/view-locals/user.js @@ -0,0 +1,36 @@ +'use strict' + +module.exports = User; + +// faux model + +function User(name, age, species) { + this.name = name; + this.age = age; + this.species = species; +} + +User.all = function(fn){ + // process.nextTick makes sure this function API + // behaves in an asynchronous manner, like if it + // was a real DB query to read all users. + process.nextTick(function(){ + fn(null, users); + }); +}; + +User.count = function(fn){ + process.nextTick(function(){ + fn(null, users.length); + }); +}; + +// faux database + +var users = []; + +users.push(new User('Tobi', 2, 'ferret')); +users.push(new User('Loki', 1, 'ferret')); +users.push(new User('Jane', 6, 'ferret')); +users.push(new User('Luna', 1, 'cat')); +users.push(new User('Manny', 1, 'cat')); diff --git a/tests/express-tests/examples/view-locals/views/index.ejs b/tests/express-tests/examples/view-locals/views/index.ejs new file mode 100644 index 00000000..287f34bc --- /dev/null +++ b/tests/express-tests/examples/view-locals/views/index.ejs @@ -0,0 +1,20 @@ + + + + + + <%= title %> + + + +

    <%= title %>

    + <% users.forEach(function(user) { %> +
  • <%= user.name %> is a <% user.age %> year old <%= user.species %>
  • + <% }); %> + + diff --git a/tests/express-tests/examples/web-service/index.js b/tests/express-tests/examples/web-service/index.js new file mode 100644 index 00000000..ed0ccf89 --- /dev/null +++ b/tests/express-tests/examples/web-service/index.js @@ -0,0 +1,117 @@ +'use strict' + +/** + * Module dependencies. + */ + +var express = require("express"); + +var app = module.exports = express(); + +// create an error with .status. we +// can then use the property in our +// custom error handler (Connect respects this prop as well) + +function error(status, msg) { + var err = new Error(msg); + err.status = status; + return err; +} + +// if we wanted to supply more than JSON, we could +// use something similar to the content-negotiation +// example. + +// here we validate the API key, +// by mounting this middleware to /api +// meaning only paths prefixed with "/api" +// will cause this middleware to be invoked + +app.use('/api', function(req, res, next){ + var key = req.query['api-key']; + + // key isn't present + if (!key) return next(error(400, 'api key required')); + + // key is invalid + if (apiKeys.indexOf(key) === -1) return next(error(401, 'invalid api key')) + + // all good, store req.key for route access + req.key = key; + next(); +}); + +// map of valid api keys, typically mapped to +// account info with some sort of database like redis. +// api keys do _not_ serve as authentication, merely to +// track API usage or help prevent malicious behavior etc. + +var apiKeys = ['foo', 'bar', 'baz']; + +// these two objects will serve as our faux database + +var repos = [ + { 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: 'loki' } + , { name: 'jane' } +]; + +var userRepos = { + tobi: [repos[0], repos[1]] + , loki: [repos[1]] + , jane: [repos[2]] +}; + +// 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) { + res.send(users); +}); + +// example: http://localhost:3000/api/repos/?api-key=foo +app.get('/api/repos', function (req, res) { + 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]; + + if (user) res.send(user); + else next(); +}); + +// middleware with an arity of 4 are considered +// error handling middleware. When you next(err) +// it will be passed through the defined middleware +// in order, but ONLY those with an arity of 4, ignoring +// regular middleware. +app.use(function(err, req, res, next){ + // whatever you want here, feel free to populate + // properties on `err` to treat it differently in here. + res.status(err.status || 500); + res.send({ error: err.message }); +}); + +// our custom JSON 404 middleware. Since it's placed last +// it will be the last middleware called, if all others +// invoke next() and do not respond. +app.use(function(req, res){ + res.status(404); + res.send({ error: "Sorry, can't find that" }) +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/tests/express-tests/test/Route.js b/tests/express-tests/test/Route.js new file mode 100644 index 00000000..5e5ab6df --- /dev/null +++ b/tests/express-tests/test/Route.js @@ -0,0 +1,274 @@ +// 'use strict' + +// var after = require('after'); +// var assert = require('assert') +// var express = require("express") +// , Route = express.Route +// , methods = require('methods') + +// describe('Route', function(){ +// it('should work without handlers', function(done) { +// var req = { method: 'GET', url: '/' } +// var route = new Route('/foo') +// route.dispatch(req, {}, done) +// }) + +// it('should not stack overflow with a large sync stack', function (done) { +// this.timeout(5000) // long-running test + +// var req = { method: 'GET', url: '/' } +// var route = new Route('/foo') + +// route.get(function (req, res, next) { +// req.counter = 0 +// next() +// }) + +// for (var i = 0; i < 6000; i++) { +// route.all(function (req, res, next) { +// req.counter++ +// next() +// }) +// } + +// route.get(function (req, res, next) { +// req.called = true +// next() +// }) + +// route.dispatch(req, {}, function (err) { +// if (err) return done(err) +// assert.ok(req.called) +// assert.strictEqual(req.counter, 6000) +// done() +// }) +// }) + +// describe('.all', function(){ +// it('should add handler', function(done){ +// var req = { method: 'GET', url: '/' }; +// var route = new Route('/foo'); + +// route.all(function(req, res, next) { +// req.called = true; +// next(); +// }); + +// route.dispatch(req, {}, function (err) { +// if (err) return done(err); +// assert.ok(req.called) +// done(); +// }); +// }) + +// it('should handle VERBS', function(done) { +// var count = 0; +// var route = new Route('/foo'); +// var cb = after(methods.length, function (err) { +// if (err) return done(err); +// assert.strictEqual(count, methods.length) +// done(); +// }); + +// route.all(function(req, res, next) { +// count++; +// next(); +// }); + +// methods.forEach(function testMethod(method) { +// var req = { method: method, url: '/' }; +// route.dispatch(req, {}, cb); +// }); +// }) + +// it('should stack', function(done) { +// var req = { count: 0, method: 'GET', url: '/' }; +// var route = new Route('/foo'); + +// route.all(function(req, res, next) { +// req.count++; +// next(); +// }); + +// route.all(function(req, res, next) { +// req.count++; +// next(); +// }); + +// route.dispatch(req, {}, function (err) { +// if (err) return done(err); +// assert.strictEqual(req.count, 2) +// done(); +// }); +// }) +// }) + +// describe('.VERB', function(){ +// it('should support .get', function(done){ +// var req = { method: 'GET', url: '/' }; +// var route = new Route(''); + +// route.get(function(req, res, next) { +// req.called = true; +// next(); +// }) + +// route.dispatch(req, {}, function (err) { +// if (err) return done(err); +// assert.ok(req.called) +// done(); +// }); +// }) + +// it('should limit to just .VERB', function(done){ +// var req = { method: 'POST', url: '/' }; +// var route = new Route(''); + +// route.get(function () { +// throw new Error('not me!'); +// }) + +// route.post(function(req, res, next) { +// req.called = true; +// next(); +// }) + +// route.dispatch(req, {}, function (err) { +// if (err) return done(err); +// assert.ok(req.called) +// done(); +// }); +// }) + +// it('should allow fallthrough', function(done){ +// var req = { order: '', method: 'GET', url: '/' }; +// var route = new Route(''); + +// route.get(function(req, res, next) { +// req.order += 'a'; +// next(); +// }) + +// route.all(function(req, res, next) { +// req.order += 'b'; +// next(); +// }); + +// route.get(function(req, res, next) { +// req.order += 'c'; +// next(); +// }) + +// route.dispatch(req, {}, function (err) { +// if (err) return done(err); +// assert.strictEqual(req.order, 'abc') +// done(); +// }); +// }) +// }) + +// describe('errors', function(){ +// it('should handle errors via arity 4 functions', function(done){ +// var req = { order: '', method: 'GET', url: '/' }; +// var route = new Route(''); + +// route.all(function(req, res, next){ +// next(new Error('foobar')); +// }); + +// route.all(function(req, res, next){ +// req.order += '0'; +// next(); +// }); + +// route.all(function(err, req, res, next){ +// req.order += 'a'; +// next(err); +// }); + +// route.dispatch(req, {}, function (err) { +// assert.ok(err) +// assert.strictEqual(err.message, 'foobar') +// assert.strictEqual(req.order, 'a') +// done(); +// }); +// }) + +// it('should handle throw', function(done) { +// var req = { order: '', method: 'GET', url: '/' }; +// var route = new Route(''); + +// route.all(function () { +// throw new Error('foobar'); +// }); + +// route.all(function(req, res, next){ +// req.order += '0'; +// next(); +// }); + +// route.all(function(err, req, res, next){ +// req.order += 'a'; +// next(err); +// }); + +// route.dispatch(req, {}, function (err) { +// assert.ok(err) +// assert.strictEqual(err.message, 'foobar') +// assert.strictEqual(req.order, 'a') +// done(); +// }); +// }); + +// it('should handle throwing inside error handlers', function(done) { +// var req = { method: 'GET', url: '/' }; +// var route = new Route(''); + +// route.get(function () { +// throw new Error('boom!'); +// }); + +// route.get(function(err, req, res, next){ +// throw new Error('oops'); +// }); + +// route.get(function(err, req, res, next){ +// req.message = err.message; +// next(); +// }); + +// route.dispatch(req, {}, function (err) { +// if (err) return done(err); +// assert.strictEqual(req.message, 'oops') +// done(); +// }); +// }); + +// it('should handle throw in .all', function(done) { +// var req = { method: 'GET', url: '/' }; +// var route = new Route(''); + +// route.all(function(req, res, next){ +// throw new Error('boom!'); +// }); + +// route.dispatch(req, {}, function(err){ +// assert.ok(err) +// assert.strictEqual(err.message, 'boom!') +// done(); +// }); +// }); + +// it('should handle single error handler', function(done) { +// var req = { method: 'GET', url: '/' }; +// var route = new Route(''); + +// route.all(function(err, req, res, next){ +// // this should not execute +// throw new Error('should not be called') +// }); + +// route.dispatch(req, {}, done); +// }); +// }) +// }) diff --git a/tests/express-tests/test/Router.js b/tests/express-tests/test/Router.js new file mode 100644 index 00000000..ffb0febb --- /dev/null +++ b/tests/express-tests/test/Router.js @@ -0,0 +1,640 @@ +'use strict' + +var after = require('after'); +var express = require("express") + , Router = express.Router + , methods = require('methods') + , assert = require('assert'); + +describe('Router', function(){ + it('should return a function with router methods', function() { + var router = new Router(); + assert(typeof router === '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){ + var router = new Router(); + var another = new Router(); + + another.get('/bar', function(req, res){ + res.end(); + }); + router.use('/foo', another); + + router.handle({ url: '/foo/bar', method: 'GET' }, { end: done }); + }); + + it('should support dynamic routes', function(done){ + var router = new Router(); + var another = new Router(); + + another.get('/:bar', function(req, res){ + assert.strictEqual(req.params.bar, 'route') + res.end(); + }); + router.use('/:foo', another); + + router.handle({ url: '/test/route', method: 'GET' }, { end: done }); + }); + + it('should handle blank URL', function(done){ + var router = new Router(); + + router.use(function (req, res) { + throw new Error('should not be called') + }); + + router.handle({ url: '', method: 'GET' }, {}, done); + }); + + it('should handle missing URL', function (done) { + var router = new Router() + + router.use(function (req, res) { + throw new Error('should not be called') + }) + + router.handle({ method: 'GET' }, {}, done) + }) + + it('handle missing method', function (done) { + var all = false + var router = new Router() + var route = router.route('/foo') + var use = false + + route.post(function (req, res, next) { next(new Error('should not run')) }) + route.all(function (req, res, next) { + all = true + next() + }) + route.get(function (req, res, next) { next(new Error('should not run')) }) + + router.get('/foo', function (req, res, next) { next(new Error('should not run')) }) + router.use(function (req, res, next) { + use = true + next() + }) + + router.handle({ url: '/foo' }, {}, function (err) { + if (err) return done(err) + assert.ok(all) + assert.ok(use) + done() + }) + }) + + it('should not stack overflow with many registered routes', function(done){ + this.timeout(5000) // long-running test + + var handler = function(req, res){ res.end(new Error('wrong handler')) }; + var router = new Router(); + + for (var i = 0; i < 6000; i++) { + router.get('/thing' + i, handler) + } + + router.get('/', function (req, res) { + res.end(); + }); + + router.handle({ url: '/', method: 'GET' }, { end: done }); + }); + + it('should not stack overflow with a large sync route stack', function (done) { + this.timeout(5000) // long-running test + + var router = new Router() + + router.get('/foo', function (req, res, next) { + req.counter = 0 + next() + }) + + for (var i = 0; i < 6000; i++) { + router.get('/foo', function (req, res, next) { + req.counter++ + next() + }) + } + + router.get('/foo', function (req, res) { + assert.strictEqual(req.counter, 6000) + res.end() + }) + + router.handle({ url: '/foo', method: 'GET' }, { end: done }) + }) + + it('should not stack overflow with a large sync middleware stack', function (done) { + this.timeout(5000) // long-running test + + var router = new Router() + + router.use(function (req, res, next) { + req.counter = 0 + next() + }) + + for (var i = 0; i < 6000; i++) { + router.use(function (req, res, next) { + req.counter++ + next() + }) + } + + router.use(function (req, res) { + assert.strictEqual(req.counter, 6000) + res.end() + }) + + router.handle({ url: '/', method: 'GET' }, { end: done }) + }) + + describe('.handle', function(){ + it('should dispatch', function(done){ + var router = new Router(); + + router.route('/foo').get(function(req, res){ + res.send('foo'); + }); + + var res = { + send: function(val) { + assert.strictEqual(val, 'foo') + done(); + } + } + router.handle({ url: '/foo', method: 'GET' }, res); + }) + }) + + describe('.multiple callbacks', function(){ + it('should throw if a callback is null', function(){ + assert.throws(function () { + var router = new Router(); + router.route('/foo').all(null); + }) + }) + + it('should throw if a callback is undefined', function(){ + assert.throws(function () { + var router = new Router(); + router.route('/foo').all(undefined); + }) + }) + + it('should throw if a callback is not a function', function(){ + assert.throws(function () { + var router = new Router(); + router.route('/foo').all('not a function'); + }) + }) + + it('should not throw if all callbacks are functions', function(){ + var router = new Router(); + router.route('/foo').all(function(){}).all(function(){}); + }) + }) + + describe('error', function(){ + it('should skip non error middleware', function(done){ + var router = new Router(); + + router.get('/foo', function(req, res, next){ + next(new Error('foo')); + }); + + router.get('/bar', function(req, res, next){ + next(new Error('bar')); + }); + + router.use(function(req, res, next){ + assert(false); + }); + + router.use(function(err, req, res, next){ + assert.equal(err.message, 'foo'); + done(); + }); + + router.handle({ url: '/foo', method: 'GET' }, {}, done); + }); + + it('should handle throwing inside routes with params', function(done) { + var router = new Router(); + + router.get('/foo/:id', function () { + throw new Error('foo'); + }); + + router.use(function(req, res, next){ + assert(false); + }); + + router.use(function(err, req, res, next){ + assert.equal(err.message, 'foo'); + done(); + }); + + router.handle({ url: '/foo/2', method: 'GET' }, {}, function() {}); + }); + + it('should handle throwing in handler after async param', function(done) { + var router = new Router(); + + router.param('user', function(req, res, next, val){ + process.nextTick(function(){ + req.user = val; + next(); + }); + }); + + router.use('/:user', function(req, res, next){ + throw new Error('oh no!'); + }); + + router.use(function(err, req, res, next){ + assert.equal(err.message, 'oh no!'); + done(); + }); + + router.handle({ url: '/bob', method: 'GET' }, {}, function() {}); + }); + + it('should handle throwing inside error handlers', function(done) { + var router = new Router(); + + router.use(function(req, res, next){ + throw new Error('boom!'); + }); + + router.use(function(err, req, res, next){ + throw new Error('oops'); + }); + + router.use(function(err, req, res, next){ + assert.equal(err.message, 'oops'); + done(); + }); + + router.handle({ url: '/', method: 'GET' }, {}, done); + }); + }) + + describe('FQDN', function () { + it('should not obscure FQDNs', function (done) { + var request = { hit: 0, url: 'http://example.com/foo', method: 'GET' }; + var router = new Router(); + + router.use(function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, 'http://example.com/foo'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 1); + done(); + }); + }); + + it('should ignore FQDN in search', function (done) { + var request = { hit: 0, url: '/proxy?url=http://example.com/blog/post/1', method: 'GET' }; + var router = new Router(); + + router.use('/proxy', function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, '/?url=http://example.com/blog/post/1'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 1); + done(); + }); + }); + + it('should ignore FQDN in path', function (done) { + var request = { hit: 0, url: '/proxy/http://example.com/blog/post/1', method: 'GET' }; + var router = new Router(); + + router.use('/proxy', function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, '/http://example.com/blog/post/1'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 1); + done(); + }); + }); + + it('should adjust FQDN req.url', function (done) { + var request = { hit: 0, url: 'http://example.com/blog/post/1', method: 'GET' }; + var router = new Router(); + + router.use('/blog', function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, 'http://example.com/post/1'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 1); + done(); + }); + }); + + it('should adjust FQDN req.url with multiple handlers', function (done) { + var request = { hit: 0, url: 'http://example.com/blog/post/1', method: 'GET' }; + var router = new Router(); + + router.use(function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, 'http://example.com/blog/post/1'); + next(); + }); + + router.use('/blog', function (req, res, next) { + assert.equal(req.hit++, 1); + assert.equal(req.url, 'http://example.com/post/1'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 2); + done(); + }); + }); + + it('should adjust FQDN req.url with multiple routed handlers', function (done) { + var request = { hit: 0, url: 'http://example.com/blog/post/1', method: 'GET' }; + var router = new Router(); + + router.use('/blog', function (req, res, next) { + assert.equal(req.hit++, 0); + assert.equal(req.url, 'http://example.com/post/1'); + next(); + }); + + router.use('/blog', function (req, res, next) { + assert.equal(req.hit++, 1); + assert.equal(req.url, 'http://example.com/post/1'); + next(); + }); + + router.use(function (req, res, next) { + assert.equal(req.hit++, 2); + assert.equal(req.url, 'http://example.com/blog/post/1'); + next(); + }); + + router.handle(request, {}, function (err) { + if (err) return done(err); + assert.equal(request.hit, 3); + done(); + }); + }); + }) + + describe('.all', function() { + it('should support using .all to capture all http verbs', function(done){ + var router = new Router(); + + var count = 0; + router.all('/foo', function(){ count++; }); + + var url = '/foo?bar=baz'; + + methods.forEach(function testMethod(method) { + router.handle({ url: url, method: method }, {}, function() {}); + }); + + assert.equal(count, methods.length); + done(); + }) + + it('should be called for any URL when "*"', function (done) { + var cb = after(4, done) + var router = new Router() + + function no () { + throw new Error('should not be called') + } + + router.all('*', function (req, res) { + res.end() + }) + + router.handle({ url: '/', method: 'GET' }, { end: cb }, no) + router.handle({ url: '/foo', method: 'GET' }, { end: cb }, no) + router.handle({ url: 'foo', method: 'GET' }, { end: cb }, no) + router.handle({ url: '*', method: 'GET' }, { end: cb }, no) + }) + }) + + describe('.use', function() { + it('should require middleware', function () { + var router = new Router() + assert.throws(function () { router.use('/') }, /requires a middleware function/) + }) + + 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) { + var cb = after(4, done) + var router = new Router() + + function no () { + throw new Error('should not be called') + } + + router.use(function (req, res) { + res.end() + }) + + router.handle({ url: '/', method: 'GET' }, { end: cb }, no) + router.handle({ url: '/foo', method: 'GET' }, { end: cb }, no) + router.handle({ url: 'foo', method: 'GET' }, { end: cb }, no) + router.handle({ url: '*', method: 'GET' }, { end: cb }, no) + }) + + it('should accept array of middleware', function(done){ + var count = 0; + var router = new Router(); + + function fn1(req, res, next){ + assert.equal(++count, 1); + next(); + } + + function fn2(req, res, next){ + assert.equal(++count, 2); + next(); + } + + router.use([fn1, fn2], function(req, res){ + assert.equal(++count, 3); + done(); + }); + + router.handle({ url: '/foo', method: 'GET' }, {}, function(){}); + }) + }) + + describe('.param', function() { + it('should call param function when routing VERBS', function(done) { + var router = new Router(); + + router.param('id', function(req, res, next, id) { + assert.equal(id, '123'); + next(); + }); + + router.get('/foo/:id/bar', function(req, res, next) { + assert.equal(req.params.id, '123'); + next(); + }); + + router.handle({ url: '/foo/123/bar', method: 'get' }, {}, done); + }); + + it('should call param function when routing middleware', function(done) { + var router = new Router(); + + router.param('id', function(req, res, next, id) { + assert.equal(id, '123'); + next(); + }); + + router.use('/foo/:id/bar', function(req, res, next) { + assert.equal(req.params.id, '123'); + assert.equal(req.url, '/baz'); + next(); + }); + + router.handle({ url: '/foo/123/bar/baz', method: 'get' }, {}, done); + }); + + it('should only call once per request', function(done) { + var count = 0; + var req = { url: '/foo/bob/bar', method: 'get' }; + var router = new Router(); + var sub = new Router(); + + sub.get('/bar', function(req, res, next) { + next(); + }); + + router.param('user', function(req, res, next, user) { + count++; + req.user = user; + next(); + }); + + router.use('/foo/:user/', new Router()); + router.use('/foo/:user/', sub); + + router.handle(req, {}, function(err) { + if (err) return done(err); + assert.equal(count, 1); + assert.equal(req.user, 'bob'); + done(); + }); + }); + + it('should call when values differ', function(done) { + var count = 0; + var req = { url: '/foo/bob/bar', method: 'get' }; + var router = new Router(); + var sub = new Router(); + + sub.get('/bar', function(req, res, next) { + next(); + }); + + router.param('user', function(req, res, next, user) { + count++; + req.user = user; + next(); + }); + + router.use('/foo/:user/', new Router()); + router.use('/:user/bob/', sub); + + router.handle(req, {}, function(err) { + if (err) return done(err); + assert.equal(count, 2); + assert.equal(req.user, 'foo'); + done(); + }); + }); + }); + + describe('parallel requests', function() { + it('should not mix requests', function(done) { + var req1 = { url: '/foo/50/bar', method: 'get' }; + var req2 = { url: '/foo/10/bar', method: 'get' }; + var router = new Router(); + var sub = new Router(); + var cb = after(2, done) + + + sub.get('/bar', function(req, res, next) { + next(); + }); + + router.param('ms', function(req, res, next, ms) { + ms = parseInt(ms, 10); + req.ms = ms; + setTimeout(next, ms); + }); + + router.use('/foo/:ms/', new Router()); + router.use('/foo/:ms/', sub); + + router.handle(req1, {}, function(err) { + assert.ifError(err); + assert.equal(req1.ms, 50); + assert.equal(req1.originalUrl, '/foo/50/bar'); + cb() + }); + + router.handle(req2, {}, function(err) { + assert.ifError(err); + assert.equal(req2.ms, 10); + assert.equal(req2.originalUrl, '/foo/10/bar'); + cb() + }); + }); + }); +}) diff --git a/tests/express-tests/test/acceptance/auth.js b/tests/express-tests/test/acceptance/auth.js new file mode 100644 index 00000000..d7838755 --- /dev/null +++ b/tests/express-tests/test/acceptance/auth.js @@ -0,0 +1,117 @@ +var app = require('../../examples/auth') +var request = require('supertest') + +function getCookie(res) { + return res.headers['set-cookie'][0].split(';')[0]; +} + +describe('auth', function(){ + describe('GET /',function(){ + it('should redirect to /login', function(done){ + request(app) + .get('/') + .expect('Location', '/login') + .expect(302, done) + }) + }) + + describe('GET /login',function(){ + it('should render login form', function(done){ + request(app) + .get('/login') + .expect(200, /
  • Tobi
  • Loki
  • Jane
  • ', done) + }) + + it('should accept to text/plain', function(done){ + request(app) + .get('/') + .set('Accept', 'text/plain') + .expect(200, ' - Tobi\n - Loki\n - Jane\n', done) + }) + + it('should accept to application/json', function(done){ + request(app) + .get('/') + .set('Accept', 'application/json') + .expect(200, '[{"name":"Tobi"},{"name":"Loki"},{"name":"Jane"}]', done) + }) + }) + + describe('GET /users', function(){ + it('should default to text/html', function(done){ + request(app) + .get('/users') + .expect(200, '', done) + }) + + it('should accept to text/plain', function(done){ + request(app) + .get('/users') + .set('Accept', 'text/plain') + .expect(200, ' - Tobi\n - Loki\n - Jane\n', done) + }) + + it('should accept to application/json', function(done){ + request(app) + .get('/users') + .set('Accept', 'application/json') + .expect(200, '[{"name":"Tobi"},{"name":"Loki"},{"name":"Jane"}]', done) + }) + }) +}) diff --git a/tests/express-tests/test/acceptance/cookie-sessions.js b/tests/express-tests/test/acceptance/cookie-sessions.js new file mode 100644 index 00000000..e83c8e41 --- /dev/null +++ b/tests/express-tests/test/acceptance/cookie-sessions.js @@ -0,0 +1,38 @@ + +var app = require('../../examples/cookie-sessions') +var request = require('supertest') + +describe('cookie-sessions', function () { + describe('GET /', function () { + it('should display no views', function (done) { + request(app) + .get('/') + .expect(200, 'viewed 1 times\n', done) + }) + + it('should set a session cookie', function (done) { + request(app) + .get('/') + .expect('Set-Cookie', /session=/) + .expect(200, done) + }) + + it('should display 1 view on revisit', function (done) { + request(app) + .get('/') + .expect(200, 'viewed 1 times\n', function (err, res) { + if (err) return done(err) + request(app) + .get('/') + .set('Cookie', getCookies(res)) + .expect(200, 'viewed 2 times\n', done) + }) + }) + }) +}) + +function getCookies(res) { + return res.headers['set-cookie'].map(function (val) { + return val.split(';')[0] + }).join('; '); +} diff --git a/tests/express-tests/test/acceptance/cookies.js b/tests/express-tests/test/acceptance/cookies.js new file mode 100644 index 00000000..aa9e1fae --- /dev/null +++ b/tests/express-tests/test/acceptance/cookies.js @@ -0,0 +1,71 @@ + +var app = require('../../examples/cookies') + , request = require('supertest'); +var utils = require('../support/utils'); + +describe('cookies', function(){ + describe('GET /', function(){ + it('should have a form', function(done){ + request(app) + .get('/') + .expect(/tobi <tobi@learnboost\.com><\/li>/) + .expect(/
  • loki <loki@learnboost\.com><\/li>/) + .expect(/
  • jane <jane@learnboost\.com><\/li>/) + .expect(200, done) + }) + }) +}) diff --git a/tests/express-tests/test/acceptance/error-pages.js b/tests/express-tests/test/acceptance/error-pages.js new file mode 100644 index 00000000..48feb3fb --- /dev/null +++ b/tests/express-tests/test/acceptance/error-pages.js @@ -0,0 +1,99 @@ + +var app = require('../../examples/error-pages') + , request = require('supertest'); + +describe('error-pages', function(){ + describe('GET /', function(){ + it('should respond with page list', function(done){ + request(app) + .get('/') + .expect(/Pages Example/, done) + }) + }) + + describe('Accept: text/html',function(){ + describe('GET /403', function(){ + it('should respond with 403', function(done){ + request(app) + .get('/403') + .expect(403, done) + }) + }) + + describe('GET /404', function(){ + it('should respond with 404', function(done){ + request(app) + .get('/404') + .expect(404, done) + }) + }) + + describe('GET /500', function(){ + it('should respond with 500', function(done){ + request(app) + .get('/500') + .expect(500, done) + }) + }) + }) + + describe('Accept: application/json',function(){ + describe('GET /403', function(){ + it('should respond with 403', function(done){ + request(app) + .get('/403') + .set('Accept','application/json') + .expect(403, done) + }) + }) + + describe('GET /404', function(){ + it('should respond with 404', function(done){ + request(app) + .get('/404') + .set('Accept','application/json') + .expect(404, { error: 'Not found' }, done) + }) + }) + + describe('GET /500', function(){ + it('should respond with 500', function(done){ + request(app) + .get('/500') + .set('Accept', 'application/json') + .expect(500, done) + }) + }) + }) + + + describe('Accept: text/plain',function(){ + describe('GET /403', function(){ + it('should respond with 403', function(done){ + request(app) + .get('/403') + .set('Accept','text/plain') + .expect(403, done) + }) + }) + + describe('GET /404', function(){ + it('should respond with 404', function(done){ + request(app) + .get('/404') + .set('Accept', 'text/plain') + .expect(404) + .expect('Not found', done); + }) + }) + + describe('GET /500', function(){ + it('should respond with 500', function(done){ + request(app) + .get('/500') + .set('Accept','text/plain') + .expect(500, done) + }) + }) + }) +}) diff --git a/tests/express-tests/test/acceptance/error.js b/tests/express-tests/test/acceptance/error.js new file mode 100644 index 00000000..6bdf099f --- /dev/null +++ b/tests/express-tests/test/acceptance/error.js @@ -0,0 +1,29 @@ + +var app = require('../../examples/error') + , request = require('supertest'); + +describe('error', function(){ + describe('GET /', function(){ + it('should respond with 500', function(done){ + request(app) + .get('/') + .expect(500,done) + }) + }) + + describe('GET /next', function(){ + it('should respond with 500', function(done){ + request(app) + .get('/next') + .expect(500,done) + }) + }) + + describe('GET /missing', function(){ + it('should respond with 404', function(done){ + request(app) + .get('/missing') + .expect(404,done) + }) + }) +}) diff --git a/tests/express-tests/test/acceptance/hello-world.js b/tests/express-tests/test/acceptance/hello-world.js new file mode 100644 index 00000000..db90349c --- /dev/null +++ b/tests/express-tests/test/acceptance/hello-world.js @@ -0,0 +1,21 @@ + +var app = require('../../examples/hello-world') +var request = require('supertest') + +describe('hello-world', function () { + describe('GET /', function () { + it('should respond with hello world', function (done) { + request(app) + .get('/') + .expect(200, 'Hello World', done) + }) + }) + + describe('GET /missing', function () { + it('should respond with 404', function (done) { + request(app) + .get('/missing') + .expect(404, done) + }) + }) +}) diff --git a/tests/express-tests/test/acceptance/markdown.js b/tests/express-tests/test/acceptance/markdown.js new file mode 100644 index 00000000..1a7d9e3c --- /dev/null +++ b/tests/express-tests/test/acceptance/markdown.js @@ -0,0 +1,21 @@ + +var app = require('../../examples/markdown') +var request = require('supertest') + +describe('markdown', function(){ + describe('GET /', function(){ + it('should respond with html', function(done){ + request(app) + .get('/') + .expect(/]*>Markdown Example<\/h1>/,done) + }) + }) + + describe('GET /fail',function(){ + it('should respond with an error', function(done){ + request(app) + .get('/fail') + .expect(500,done) + }) + }) +}) diff --git a/tests/express-tests/test/acceptance/multi-router.js b/tests/express-tests/test/acceptance/multi-router.js new file mode 100644 index 00000000..4362a830 --- /dev/null +++ b/tests/express-tests/test/acceptance/multi-router.js @@ -0,0 +1,44 @@ +var app = require('../../examples/multi-router') +var request = require('supertest') + +describe('multi-router', function(){ + describe('GET /',function(){ + it('should respond with root handler', function(done){ + request(app) + .get('/') + .expect(200, 'Hello from root route.', done) + }) + }) + + describe('GET /api/v1/',function(){ + it('should respond with APIv1 root handler', function(done){ + request(app) + .get('/api/v1/') + .expect(200, 'Hello from APIv1 root route.', done) + }) + }) + + describe('GET /api/v1/users',function(){ + it('should respond with users from APIv1', function(done){ + request(app) + .get('/api/v1/users') + .expect(200, 'List of APIv1 users.', done) + }) + }) + + describe('GET /api/v2/',function(){ + it('should respond with APIv2 root handler', function(done){ + request(app) + .get('/api/v2/') + .expect(200, 'Hello from APIv2 root route.', done) + }) + }) + + describe('GET /api/v2/users',function(){ + it('should respond with users from APIv2', function(done){ + request(app) + .get('/api/v2/users') + .expect(200, 'List of APIv2 users.', done) + }) + }) +}) diff --git a/tests/express-tests/test/acceptance/mvc.js b/tests/express-tests/test/acceptance/mvc.js new file mode 100644 index 00000000..35709f6f --- /dev/null +++ b/tests/express-tests/test/acceptance/mvc.js @@ -0,0 +1,132 @@ + +var request = require('supertest') + , app = require('../../examples/mvc'); + +describe('mvc', function(){ + describe('GET /', function(){ + it('should redirect to /users', function(done){ + request(app) + .get('/') + .expect('Location', '/users') + .expect(302, done) + }) + }) + + describe('GET /pet/0', function(){ + it('should get pet', function(done){ + request(app) + .get('/pet/0') + .expect(200, /Tobi/, done) + }) + }) + + describe('GET /pet/0/edit', function(){ + it('should get pet edit page', function(done){ + request(app) + .get('/pet/0/edit') + .expect(/Users<\/h1>/) + .expect(/>TJGuillermoNathanTJ edit/, done) + }) + + it('should display the users pets', function(done){ + request(app) + .get('/user/0') + .expect(/\/pet\/0">Tobi/) + .expect(/\/pet\/1">Loki/) + .expect(/\/pet\/2">Jane/) + .expect(200, done) + }) + }) + + describe('when not present', function(){ + it('should 404', function(done){ + request(app) + .get('/user/123') + .expect(404, done); + }) + }) + }) + + describe('GET /user/:id/edit', function(){ + it('should display the edit form', function(done){ + request(app) + .get('/user/1/edit') + .expect(/Guillermo/) + .expect(200, /Examples:<\/h1>/,done) + }) + }) + + describe('GET /users', function(){ + it('should respond with all users', function(done){ + request(app) + .get('/users') + .expect(/^\[{"name":"tj"},{"name":"ciaran"},{"name":"aaron"},{"name":"guillermo"},{"name":"simon"},{"name":"tobi"}\]/,done) + }) + }) + + describe('GET /users/1', function(){ + it('should respond with user 1', function(done){ + request(app) + .get('/users/1') + .expect(/^{"name":"ciaran"}/,done) + }) + }) + + describe('GET /users/9', function(){ + it('should respond with error', function(done){ + request(app) + .get('/users/9') + .expect('{"error":"Cannot find user"}', done) + }) + }) + + describe('GET /users/1..3', function(){ + it('should respond with users 1 through 3', function(done){ + request(app) + .get('/users/1..3') + .expect(/^
    • ciaran<\/li>\n
    • aaron<\/li>\n
    • guillermo<\/li><\/ul>/,done) + }) + }) + + describe('DELETE /users/1', function(){ + it('should delete user 1', function(done){ + request(app) + .del('/users/1') + .expect(/^destroyed/,done) + }) + }) + + describe('DELETE /users/9', function(){ + it('should fail', function(done){ + request(app) + .del('/users/9') + .expect('Cannot find user', done) + }) + }) + + describe('GET /users/1..3.json', function(){ + it('should respond with users 2 and 3 as json', function(done){ + request(app) + .get('/users/1..3.json') + .expect(/^\[null,{"name":"aaron"},{"name":"guillermo"}\]/,done) + }) + }) +}) diff --git a/tests/express-tests/test/acceptance/route-map.js b/tests/express-tests/test/acceptance/route-map.js new file mode 100644 index 00000000..0bd2a6d3 --- /dev/null +++ b/tests/express-tests/test/acceptance/route-map.js @@ -0,0 +1,45 @@ + +var request = require('supertest') + , app = require('../../examples/route-map'); + +describe('route-map', function(){ + describe('GET /users', function(){ + it('should respond with users', function(done){ + request(app) + .get('/users') + .expect('user list', done); + }) + }) + + describe('DELETE /users', function(){ + it('should delete users', function(done){ + request(app) + .del('/users') + .expect('delete users', done); + }) + }) + + describe('GET /users/:id', function(){ + it('should get a user', function(done){ + request(app) + .get('/users/12') + .expect('user 12', done); + }) + }) + + describe('GET /users/:id/pets', function(){ + it('should get a users pets', function(done){ + request(app) + .get('/users/12/pets') + .expect('user 12\'s pets', done); + }) + }) + + describe('GET /users/:id/pets/:pid', function(){ + it('should get a users pet', function(done){ + request(app) + .del('/users/12/pets/2') + .expect('delete 12\'s pet 2', done); + }) + }) +}) diff --git a/tests/express-tests/test/acceptance/route-separation.js b/tests/express-tests/test/acceptance/route-separation.js new file mode 100644 index 00000000..867fd295 --- /dev/null +++ b/tests/express-tests/test/acceptance/route-separation.js @@ -0,0 +1,97 @@ + +var app = require('../../examples/route-separation') +var request = require('supertest') + +describe('route-separation', function () { + describe('GET /', function () { + it('should respond with index', function (done) { + request(app) + .get('/') + .expect(200, /Route Separation Example/, done) + }) + }) + + describe('GET /users', function () { + it('should list users', function (done) { + request(app) + .get('/users') + .expect(/TJ/) + .expect(/Tobi/) + .expect(200, done) + }) + }) + + describe('GET /user/:id', function () { + it('should get a user', function (done) { + request(app) + .get('/user/0') + .expect(200, /Viewing user TJ/, done) + }) + + it('should 404 on missing user', function (done) { + request(app) + .get('/user/10') + .expect(404, done) + }) + }) + + describe('GET /user/:id/view', function () { + it('should get a user', function (done) { + request(app) + .get('/user/0/view') + .expect(200, /Viewing user TJ/, done) + }) + + it('should 404 on missing user', function (done) { + request(app) + .get('/user/10/view') + .expect(404, done) + }) + }) + + describe('GET /user/:id/edit', function () { + it('should get a user to edit', function (done) { + request(app) + .get('/user/0/edit') + .expect(200, /Editing user TJ/, done) + }) + }) + + describe('PUT /user/:id/edit', function () { + it('should edit a user', function (done) { + request(app) + .put('/user/0/edit') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send({ user: { name: 'TJ', email: 'tj-invalid@vision-media.ca' } }) + .expect(302, function (err) { + if (err) return done(err) + request(app) + .get('/user/0') + .expect(200, /tj-invalid@vision-media\.ca/, done) + }) + }) + }) + + describe('POST /user/:id/edit?_method=PUT', function () { + it('should edit a user', function (done) { + request(app) + .post('/user/1/edit?_method=PUT') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send({ user: { name: 'Tobi', email: 'tobi-invalid@vision-media.ca' } }) + .expect(302, function (err) { + if (err) return done(err) + request(app) + .get('/user/1') + .expect(200, /tobi-invalid@vision-media\.ca/, done) + }) + }) + }) + + describe('GET /posts', function () { + it('should get a list of posts', function (done) { + request(app) + .get('/posts') + .expect(200, /Posts/, done) + }) + }) +}) diff --git a/tests/express-tests/test/acceptance/vhost.js b/tests/express-tests/test/acceptance/vhost.js new file mode 100644 index 00000000..1b633d4b --- /dev/null +++ b/tests/express-tests/test/acceptance/vhost.js @@ -0,0 +1,46 @@ +var app = require('../../examples/vhost') +var request = require('supertest') + +describe('vhost', function(){ + describe('example.com', function(){ + describe('GET /', function(){ + it('should say hello', function(done){ + request(app) + .get('/') + .set('Host', 'example.com') + .expect(200, /hello/i, done) + }) + }) + + describe('GET /foo', function(){ + it('should say foo', function(done){ + request(app) + .get('/foo') + .set('Host', 'example.com') + .expect(200, 'requested foo', done) + }) + }) + }) + + describe('foo.example.com', function(){ + describe('GET /', function(){ + it('should redirect to /foo', function(done){ + request(app) + .get('/') + .set('Host', 'foo.example.com') + .expect(302, /Redirecting to http:\/\/example.com:3000\/foo/, done) + }) + }) + }) + + describe('bar.example.com', function(){ + describe('GET /', function(){ + it('should redirect to /bar', function(done){ + request(app) + .get('/') + .set('Host', 'bar.example.com') + .expect(302, /Redirecting to http:\/\/example.com:3000\/bar/, done) + }) + }) + }) +}) diff --git a/tests/express-tests/test/acceptance/web-service.js b/tests/express-tests/test/acceptance/web-service.js new file mode 100644 index 00000000..2e37b48c --- /dev/null +++ b/tests/express-tests/test/acceptance/web-service.js @@ -0,0 +1,105 @@ + +var request = require('supertest') + , app = require('../../examples/web-service'); + +describe('web-service', function(){ + describe('GET /api/users', function(){ + describe('without an api key', function(){ + it('should respond with 400 bad request', function(done){ + request(app) + .get('/api/users') + .expect(400, done); + }) + }) + + describe('with an invalid api key', function(){ + it('should respond with 401 unauthorized', function(done){ + request(app) + .get('/api/users?api-key=rawr') + .expect(401, done); + }) + }) + + describe('with a valid api key', function(){ + it('should respond users json', function(done){ + request(app) + .get('/api/users?api-key=foo') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '[{"name":"tobi"},{"name":"loki"},{"name":"jane"}]', done) + }) + }) + }) + + describe('GET /api/repos', function(){ + describe('without an api key', function(){ + it('should respond with 400 bad request', function(done){ + request(app) + .get('/api/repos') + .expect(400, done); + }) + }) + + describe('with an invalid api key', function(){ + it('should respond with 401 unauthorized', function(done){ + request(app) + .get('/api/repos?api-key=rawr') + .expect(401, done); + }) + }) + + describe('with a valid api key', function(){ + it('should respond repos json', function(done){ + request(app) + .get('/api/repos?api-key=foo') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(/"name":"express"/) + .expect(/"url":"https:\/\/github.com\/expressjs\/express"/) + .expect(200, done) + }) + }) + }) + + describe('GET /api/user/:name/repos', function(){ + describe('without an api key', function(){ + it('should respond with 400 bad request', function(done){ + request(app) + .get('/api/user/loki/repos') + .expect(400, done); + }) + }) + + describe('with an invalid api key', function(){ + it('should respond with 401 unauthorized', function(done){ + request(app) + .get('/api/user/loki/repos?api-key=rawr') + .expect(401, done); + }) + }) + + describe('with a valid api key', function(){ + it('should respond user repos json', function(done){ + request(app) + .get('/api/user/loki/repos?api-key=foo') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(/"name":"stylus"/) + .expect(/"url":"https:\/\/github.com\/learnboost\/stylus"/) + .expect(200, done) + }) + + it('should 404 with unknown user', function(done){ + request(app) + .get('/api/user/bob/repos?api-key=foo') + .expect(404, done) + }) + }) + }) + + describe('when requesting an invalid route', function(){ + it('should respond with 404 json', function(done){ + request(app) + .get('/api/something?api-key=bar') + .expect('Content-Type', /json/) + .expect(404, '{"error":"Sorry, can\'t find that"}', done) + }) + }) +}) diff --git a/tests/express-tests/test/app.all.js b/tests/express-tests/test/app.all.js new file mode 100644 index 00000000..a2b24976 --- /dev/null +++ b/tests/express-tests/test/app.all.js @@ -0,0 +1,38 @@ +'use strict' + +var after = require('after') +var express = require("express") + , request = require('supertest'); + +describe('app.all()', function(){ + it('should add a router per method', function(done){ + var app = express(); + var cb = after(2, done) + + app.all('/tobi', function(req, res){ + res.end(req.method); + }); + + request(app) + .put('/tobi') + .expect(200, 'PUT', cb) + + request(app) + .get('/tobi') + .expect(200, 'GET', cb) + }) + + it('should run the callback for a method just once', function(done){ + var app = express() + , n = 0; + + app.all('/*', function(req, res, next){ + if (n++) return done(new Error('DELETE called several times')); + next(); + }); + + request(app) + .del('/tobi') + .expect(404, done); + }) +}) diff --git a/tests/express-tests/test/app.del.js b/tests/express-tests/test/app.del.js new file mode 100644 index 00000000..f3418349 --- /dev/null +++ b/tests/express-tests/test/app.del.js @@ -0,0 +1,18 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('app.del()', function(){ + it('should alias app.delete()', function(done){ + var app = express(); + + app.del('/tobi', function(req, res){ + res.end('deleted tobi!'); + }); + + request(app) + .del('/tobi') + .expect('deleted tobi!', done); + }) +}) diff --git a/tests/express-tests/test/app.engine.js b/tests/express-tests/test/app.engine.js new file mode 100644 index 00000000..0f574b34 --- /dev/null +++ b/tests/express-tests/test/app.engine.js @@ -0,0 +1,83 @@ +'use strict' + +var assert = require('assert') +var express = require("express") + , fs = require('fs'); +var path = require('path') + +function render(path, options, fn) { + fs.readFile(path, 'utf8', function(err, str){ + if (err) return fn(err); + str = str.replace('{{user.name}}', options.user.name); + fn(null, str); + }); +} + +describe('app', function(){ + describe('.engine(ext, fn)', function(){ + it('should map a template engine', function(done){ + var app = express(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.engine('.html', render); + app.locals.user = { name: 'tobi' }; + + app.render('user.html', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should throw when the callback is missing', function(){ + var app = express(); + assert.throws(function () { + app.engine('.html', null); + }, /callback function required/) + }) + + it('should work without leading "."', function(done){ + var app = express(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.engine('html', render); + app.locals.user = { name: 'tobi' }; + + app.render('user.html', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should work "view engine" setting', function(done){ + var app = express(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.engine('html', render); + app.set('view engine', 'html'); + app.locals.user = { name: 'tobi' }; + + app.render('user', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should work "view engine" with leading "."', function(done){ + var app = express(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.engine('.html', render); + app.set('view engine', '.html'); + app.locals.user = { name: 'tobi' }; + + app.render('user', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + }) +}) diff --git a/tests/express-tests/test/app.head.js b/tests/express-tests/test/app.head.js new file mode 100644 index 00000000..8ab8eaad --- /dev/null +++ b/tests/express-tests/test/app.head.js @@ -0,0 +1,66 @@ +'use strict' + +var express = require("express"); +var request = require('supertest'); +var assert = require('assert'); + +describe('HEAD', function(){ + it('should default to GET', function(done){ + var app = express(); + + app.get('/tobi', function(req, res){ + // send() detects HEAD + res.send('tobi'); + }); + + request(app) + .head('/tobi') + .expect(200, done); + }) + + it('should output the same headers as GET requests', function(done){ + var app = express(); + + app.get('/tobi', function(req, res){ + // send() detects HEAD + res.send('tobi'); + }); + + request(app) + .head('/tobi') + .expect(200, function(err, res){ + if (err) return done(err); + var headers = res.headers; + request(app) + .get('/tobi') + .expect(200, function(err, res){ + if (err) return done(err); + delete headers.date; + delete res.headers.date; + assert.deepEqual(res.headers, headers); + done(); + }); + }); + }) +}) + +describe('app.head()', function(){ + it('should override', function(done){ + var app = express() + + app.head('/tobi', function(req, res){ + res.header('x-method', 'head') + res.end() + }); + + app.get('/tobi', function(req, res){ + res.header('x-method', 'get') + res.send('tobi'); + }); + + request(app) + .head('/tobi') + .expect('x-method', 'head') + .expect(200, done) + }) +}) diff --git a/tests/express-tests/test/app.js b/tests/express-tests/test/app.js new file mode 100644 index 00000000..3fda4a05 --- /dev/null +++ b/tests/express-tests/test/app.js @@ -0,0 +1,132 @@ +'use strict' + +var assert = require('assert') +var express = require("express") +var request = require('supertest') + +describe('app', function(){ + it('should inherit from event emitter', function(done){ + var app = express(); + app.on('foo', done); + app.emit('foo'); + }) + + it('should be callable', function(){ + var app = express(); + assert.equal(typeof app, 'function'); + }) + + it('should 404 without routes', function(done){ + request(express()) + .get('/') + .expect(404, done); + }) +}) + +describe('app.parent', function(){ + it('should return the parent when mounted', function(){ + var app = express() + , blog = express() + , blogAdmin = express(); + + app.use('/blog', blog); + blog.use('/admin', blogAdmin); + + assert(!app.parent, 'app.parent'); + assert.strictEqual(blog.parent, app) + assert.strictEqual(blogAdmin.parent, blog) + }) +}) + +describe('app.mountpath', function(){ + it('should return the mounted path', function(){ + var admin = express(); + var app = express(); + var blog = express(); + var fallback = express(); + + app.use('/blog', blog); + app.use(fallback); + blog.use('/admin', admin); + + assert.strictEqual(admin.mountpath, '/admin') + assert.strictEqual(app.mountpath, '/') + assert.strictEqual(blog.mountpath, '/blog') + assert.strictEqual(fallback.mountpath, '/') + }) +}) + +describe('app.router', function(){ + it('should throw with notice', function(done){ + var app = express() + + try { + app.router; + } catch(err) { + done(); + } + }) +}) + +describe('app.path()', function(){ + it('should return the canonical', function(){ + var app = express() + , blog = express() + , blogAdmin = express(); + + app.use('/blog', blog); + blog.use('/admin', blogAdmin); + + 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(){ + var app = express(); + 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(){ + var app = express(); + 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(){ + var app = express(); + assert.strictEqual(app.get('env'), 'development') + }) +}) diff --git a/tests/express-tests/test/app.listen.js b/tests/express-tests/test/app.listen.js new file mode 100644 index 00000000..9a41c7b3 --- /dev/null +++ b/tests/express-tests/test/app.listen.js @@ -0,0 +1,13 @@ +'use strict' + +var express = require("express") + +describe('app.listen()', function(){ + it('should wrap with an HTTP server', function(done){ + var app = express(); + + var server = app.listen(0, function () { + server.close(done) + }); + }) +}) diff --git a/tests/express-tests/test/app.locals.js b/tests/express-tests/test/app.locals.js new file mode 100644 index 00000000..a704b352 --- /dev/null +++ b/tests/express-tests/test/app.locals.js @@ -0,0 +1,25 @@ +'use strict' + +var assert = require('assert') +var express = require("express") + +describe('app', function(){ + describe('.locals', function () { + it('should default object', function () { + var app = express() + assert.ok(app.locals) + assert.strictEqual(typeof app.locals, 'object') + }) + + describe('.settings', function () { + it('should contain app settings ', function () { + var app = express() + app.set('title', 'Express') + assert.ok(app.locals.settings) + assert.strictEqual(typeof app.locals.settings, 'object') + assert.strictEqual(app.locals.settings, app.settings) + assert.strictEqual(app.locals.settings.title, 'Express') + }) + }) + }) +}) diff --git a/tests/express-tests/test/app.options.js b/tests/express-tests/test/app.options.js new file mode 100644 index 00000000..58172b0a --- /dev/null +++ b/tests/express-tests/test/app.options.js @@ -0,0 +1,116 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('OPTIONS', function(){ + it('should default to the routes defined', function(done){ + var app = express(); + + app.del('/', function(){}); + app.get('/users', function(req, res){}); + app.put('/users', function(req, res){}); + + request(app) + .options('/users') + .expect('Allow', 'GET,HEAD,PUT') + .expect(200, 'GET,HEAD,PUT', done); + }) + + it('should only include each method once', function(done){ + var app = express(); + + app.del('/', function(){}); + app.get('/users', function(req, res){}); + app.put('/users', function(req, res){}); + app.get('/users', function(req, res){}); + + request(app) + .options('/users') + .expect('Allow', 'GET,HEAD,PUT') + .expect(200, 'GET,HEAD,PUT', done); + }) + + it('should not be affected by app.all', function(done){ + var app = express(); + + app.get('/', function(){}); + app.get('/users', function(req, res){}); + app.put('/users', function(req, res){}); + app.all('/users', function(req, res, next){ + res.setHeader('x-hit', '1'); + next(); + }); + + request(app) + .options('/users') + .expect('x-hit', '1') + .expect('Allow', 'GET,HEAD,PUT') + .expect(200, 'GET,HEAD,PUT', done); + }) + + it('should not respond if the path is not defined', function(done){ + var app = express(); + + app.get('/users', function(req, res){}); + + request(app) + .options('/other') + .expect(404, done); + }) + + it('should forward requests down the middleware chain', function(done){ + var app = express(); + var router = new express.Router(); + + router.get('/users', function(req, res){}); + app.use(router); + app.get('/other', function(req, res){}); + + request(app) + .options('/other') + .expect('Allow', 'GET,HEAD') + .expect(200, 'GET,HEAD', done); + }) + + describe('when error occurs in response handler', function () { + it('should pass error to callback', function (done) { + var app = express(); + var router = express.Router(); + + router.get('/users', function(req, res){}); + + app.use(function (req, res, next) { + res.writeHead(200); + next(); + }); + app.use(router); + app.use(function (err, req, res, next) { + res.end('true'); + }); + + request(app) + .options('/users') + .expect(200, 'true', done) + }) + }) +}) + +describe('app.options()', function(){ + it('should override the default behavior', function(done){ + var app = express(); + + app.options('/users', function(req, res){ + res.set('Allow', 'GET'); + res.send('GET'); + }); + + app.get('/users', function(req, res){}); + app.put('/users', function(req, res){}); + + request(app) + .options('/users') + .expect('GET') + .expect('Allow', 'GET', done); + }) +}) diff --git a/tests/express-tests/test/app.param.js b/tests/express-tests/test/app.param.js new file mode 100644 index 00000000..c5a52350 --- /dev/null +++ b/tests/express-tests/test/app.param.js @@ -0,0 +1,365 @@ +'use strict' + +var assert = require('assert') +var express = require("express") + , request = require('supertest'); + +describe('app', function(){ + describe('.param(fn)', function(){ + it('should map app.param(name, ...) logic', function(done){ + var app = express(); + + app.param(function(name, regexp){ + if (Object.prototype.toString.call(regexp) === '[object RegExp]') { // See #1557 + return function(req, res, next, val){ + var captures; + if (captures = regexp.exec(String(val))) { + req.params[name] = captures[1]; + next(); + } else { + next('route'); + } + } + } + }) + + app.param(':name', /^([a-zA-Z]+)$/); + + app.get('/user/:name', function(req, res){ + res.send(req.params.name); + }); + + request(app) + .get('/user/tj') + .expect(200, 'tj', function (err) { + if (err) return done(err) + request(app) + .get('/user/123') + .expect(404, done); + }); + + }) + + it('should fail if not given fn', function(){ + var app = express(); + assert.throws(app.param.bind(app, ':name', 'bob')) + }) + }) + + describe('.param(names, fn)', function(){ + it('should map the array', function(done){ + var app = express(); + + app.param(['id', 'uid'], function(req, res, next, id){ + id = Number(id); + if (isNaN(id)) return next('route'); + req.params.id = id; + next(); + }); + + app.get('/post/:id', function(req, res){ + var id = req.params.id; + res.send((typeof id) + ':' + id) + }); + + app.get('/user/:uid', function(req, res){ + var id = req.params.id; + res.send((typeof id) + ':' + id) + }); + + request(app) + .get('/user/123') + .expect(200, 'number:123', function (err) { + if (err) return done(err) + request(app) + .get('/post/123') + .expect('number:123', done) + }) + }) + }) + + describe('.param(name, fn)', function(){ + it('should map logic for a single param', function(done){ + var app = express(); + + app.param('id', function(req, res, next, id){ + id = Number(id); + if (isNaN(id)) return next('route'); + req.params.id = id; + next(); + }); + + app.get('/user/:id', function(req, res){ + var id = req.params.id; + res.send((typeof id) + ':' + id) + }); + + request(app) + .get('/user/123') + .expect(200, 'number:123', done) + }) + + it('should only call once per request', function(done) { + var app = express(); + var called = 0; + var count = 0; + + app.param('user', function(req, res, next, user) { + called++; + req.user = user; + next(); + }); + + app.get('/foo/:user', function(req, res, next) { + count++; + next(); + }); + app.get('/foo/:user', function(req, res, next) { + count++; + next(); + }); + app.use(function(req, res) { + res.end([count, called, req.user].join(' ')); + }); + + request(app) + .get('/foo/bob') + .expect('2 1 bob', done); + }) + + it('should call when values differ', function(done) { + var app = express(); + var called = 0; + var count = 0; + + app.param('user', function(req, res, next, user) { + called++; + req.users = (req.users || []).concat(user); + next(); + }); + + app.get('/:user/bob', function(req, res, next) { + count++; + next(); + }); + app.get('/foo/:user', function(req, res, next) { + count++; + next(); + }); + app.use(function(req, res) { + res.end([count, called, req.users.join(',')].join(' ')); + }); + + request(app) + .get('/foo/bob') + .expect('2 2 foo,bob', done); + }) + + it('should support altering req.params across routes', function(done) { + var app = express(); + + app.param('user', function(req, res, next, user) { + req.params.user = 'loki'; + next(); + }); + + app.get('/:user', function(req, res, next) { + next('route'); + }); + app.get('/:user', function (req, res) { + res.send(req.params.user); + }); + + request(app) + .get('/bob') + .expect('loki', done); + }) + + it('should not invoke without route handler', function(done) { + var app = express(); + + app.param('thing', function(req, res, next, thing) { + req.thing = thing; + next(); + }); + + app.param('user', function(req, res, next, user) { + next(new Error('invalid invocation')) + }); + + app.post('/:user', function (req, res) { + res.send(req.params.user); + }); + + app.get('/:thing', function (req, res) { + res.send(req.thing); + }); + + request(app) + .get('/bob') + .expect(200, 'bob', done); + }) + + it('should work with encoded values', function(done){ + var app = express(); + + app.param('name', function(req, res, next, name){ + req.params.name = name; + next(); + }); + + app.get('/user/:name', function(req, res){ + var name = req.params.name; + res.send('' + name); + }); + + request(app) + .get('/user/foo%25bar') + .expect('foo%bar', done); + }) + + it('should catch thrown error', function(done){ + var app = express(); + + app.param('id', function(req, res, next, id){ + throw new Error('err!'); + }); + + app.get('/user/:id', function(req, res){ + var id = req.params.id; + res.send('' + id); + }); + + request(app) + .get('/user/123') + .expect(500, done); + }) + + it('should catch thrown secondary error', function(done){ + var app = express(); + + app.param('id', function(req, res, next, val){ + process.nextTick(next); + }); + + app.param('id', function(req, res, next, id){ + throw new Error('err!'); + }); + + app.get('/user/:id', function(req, res){ + var id = req.params.id; + res.send('' + id); + }); + + request(app) + .get('/user/123') + .expect(500, done); + }) + + it('should defer to next route', function(done){ + var app = express(); + + app.param('id', function(req, res, next, id){ + next('route'); + }); + + app.get('/user/:id', function(req, res){ + var id = req.params.id; + res.send('' + id); + }); + + app.get('/:name/123', function(req, res){ + res.send('name'); + }); + + request(app) + .get('/user/123') + .expect('name', done); + }) + + it('should defer all the param routes', function(done){ + var app = express(); + + app.param('id', function(req, res, next, val){ + if (val === 'new') return next('route'); + return next(); + }); + + app.all('/user/:id', function(req, res){ + res.send('all.id'); + }); + + app.get('/user/:id', function(req, res){ + res.send('get.id'); + }); + + app.get('/user/new', function(req, res){ + res.send('get.new'); + }); + + request(app) + .get('/user/new') + .expect('get.new', done); + }) + + it('should not call when values differ on error', function(done) { + var app = express(); + var called = 0; + var count = 0; + + app.param('user', function(req, res, next, user) { + called++; + if (user === 'foo') throw new Error('err!'); + req.user = user; + next(); + }); + + app.get('/:user/bob', function(req, res, next) { + count++; + next(); + }); + app.get('/foo/:user', function(req, res, next) { + count++; + next(); + }); + + app.use(function(err, req, res, next) { + res.status(500); + res.send([count, called, err.message].join(' ')); + }); + + request(app) + .get('/foo/bob') + .expect(500, '0 1 err!', done) + }); + + it('should call when values differ when using "next"', function(done) { + var app = express(); + var called = 0; + var count = 0; + + app.param('user', function(req, res, next, user) { + called++; + if (user === 'foo') return next('route'); + req.user = user; + next(); + }); + + app.get('/:user/bob', function(req, res, next) { + count++; + next(); + }); + app.get('/foo/:user', function(req, res, next) { + count++; + next(); + }); + app.use(function(req, res) { + res.end([count, called, req.user].join(' ')); + }); + + request(app) + .get('/foo/bob') + .expect('1 2 bob', done); + }) + }) +}) diff --git a/tests/express-tests/test/app.render.js b/tests/express-tests/test/app.render.js new file mode 100644 index 00000000..709ce59c --- /dev/null +++ b/tests/express-tests/test/app.render.js @@ -0,0 +1,374 @@ +'use strict' + +var assert = require('assert') +var express = require("express"); +var path = require('path') +var tmpl = require('./support/tmpl'); + +describe('app', function(){ + describe('.render(name, fn)', function(){ + it('should support absolute paths', function(done){ + var app = createApp(); + + app.locals.user = { name: 'tobi' }; + + app.render(path.join(__dirname, 'fixtures', 'user.tmpl'), function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should support absolute paths with "view engine"', function(done){ + var app = createApp(); + + app.set('view engine', 'tmpl'); + app.locals.user = { name: 'tobi' }; + + app.render(path.join(__dirname, 'fixtures', 'user'), function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should expose app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + app.render('user.tmpl', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should support index.', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.set('view engine', 'tmpl'); + + app.render('blog/post', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      blog post

      ') + done(); + }) + }) + + it('should handle render error throws', function(done){ + var app = express(); + + function View(name, options){ + this.name = name; + this.path = 'fale'; + } + + View.prototype.render = function(options, fn){ + throw new Error('err!'); + }; + + app.set('view', View); + + app.render('something', function(err, str){ + assert.ok(err) + assert.strictEqual(err.message, 'err!') + done(); + }) + }) + + describe('when the file does not exist', function(){ + it('should provide a helpful error', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.render('rawr.tmpl', function (err) { + assert.ok(err) + assert.equal(err.message, 'Failed to lookup view "rawr.tmpl" in views directory "' + path.join(__dirname, 'fixtures') + '"') + done(); + }); + }) + }) + + describe('when an error occurs', function(){ + it('should invoke the callback', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.render('user.tmpl', function (err) { + assert.ok(err) + assert.equal(err.name, 'RenderError') + done() + }) + }) + }) + + describe('when an extension is given', function(){ + it('should render the template', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.render('email.tmpl', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      This is an email

      ') + done(); + }) + }) + }) + + describe('when "view engine" is given', function(){ + it('should render the template', function(done){ + var app = createApp(); + + app.set('view engine', 'tmpl'); + app.set('views', path.join(__dirname, 'fixtures')) + + app.render('email', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, '

      This is an email

      ') + done(); + }) + }) + }) + + describe('when "views" is given', function(){ + it('should lookup the file in the path', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures', 'default_layout')) + app.locals.user = { name: 'tobi' }; + + app.render('user.tmpl', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + describe('when array of paths', function(){ + it('should lookup the file in the path', function(done){ + var app = createApp(); + var views = [ + path.join(__dirname, 'fixtures', 'local_layout'), + path.join(__dirname, 'fixtures', 'default_layout') + ] + + app.set('views', views); + app.locals.user = { name: 'tobi' }; + + app.render('user.tmpl', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, 'tobi') + done(); + }) + }) + + it('should lookup in later paths until found', function(done){ + var app = createApp(); + var views = [ + path.join(__dirname, 'fixtures', 'local_layout'), + path.join(__dirname, 'fixtures', 'default_layout') + ] + + app.set('views', views); + app.locals.name = 'tobi'; + + app.render('name.tmpl', function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should error if file does not exist', function(done){ + var app = createApp(); + var views = [ + path.join(__dirname, 'fixtures', 'local_layout'), + path.join(__dirname, 'fixtures', 'default_layout') + ] + + app.set('views', views); + app.locals.name = 'tobi'; + + app.render('pet.tmpl', function (err, str) { + assert.ok(err) + assert.equal(err.message, 'Failed to lookup view "pet.tmpl" in views directories "' + views[0] + '" or "' + views[1] + '"') + done(); + }) + }) + }) + }) + + describe('when a "view" constructor is given', function(){ + it('should create an instance of it', function(done){ + var app = express(); + + function View(name, options){ + this.name = name; + this.path = 'path is required by application.js as a signal of success even though it is not used there.'; + } + + View.prototype.render = function(options, fn){ + fn(null, 'abstract engine'); + }; + + app.set('view', View); + + app.render('something', function(err, str){ + if (err) return done(err); + assert.strictEqual(str, 'abstract engine') + done(); + }) + }) + }) + + describe('caching', function(){ + it('should always lookup view without cache', function(done){ + var app = express(); + var count = 0; + + function View(name, options){ + this.name = name; + this.path = 'fake'; + count++; + } + + View.prototype.render = function(options, fn){ + fn(null, 'abstract engine'); + }; + + app.set('view cache', false); + app.set('view', View); + + app.render('something', function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') + app.render('something', function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 2) + assert.strictEqual(str, 'abstract engine') + done(); + }) + }) + }) + + it('should cache with "view cache" setting', function(done){ + var app = express(); + var count = 0; + + function View(name, options){ + this.name = name; + this.path = 'fake'; + count++; + } + + View.prototype.render = function(options, fn){ + fn(null, 'abstract engine'); + }; + + app.set('view cache', true); + app.set('view', View); + + app.render('something', function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') + app.render('something', function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') + done(); + }) + }) + }) + }) + }) + + describe('.render(name, options, fn)', function(){ + it('should render the template', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + var user = { name: 'tobi' }; + + app.render('user.tmpl', { user: user }, function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should expose app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + app.render('user.tmpl', {}, function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      tobi

      ') + done(); + }) + }) + + it('should give precedence to app.render() locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + var jane = { name: 'jane' }; + + app.render('user.tmpl', { user: jane }, function (err, str) { + if (err) return done(err); + assert.strictEqual(str, '

      jane

      ') + done(); + }) + }) + + describe('caching', function(){ + it('should cache with cache option', function(done){ + var app = express(); + var count = 0; + + function View(name, options){ + this.name = name; + this.path = 'fake'; + count++; + } + + View.prototype.render = function(options, fn){ + fn(null, 'abstract engine'); + }; + + app.set('view cache', false); + app.set('view', View); + + app.render('something', {cache: true}, function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') + app.render('something', {cache: true}, function(err, str){ + if (err) return done(err); + assert.strictEqual(count, 1) + assert.strictEqual(str, 'abstract engine') + done(); + }) + }) + }) + }) + }) +}) + +function createApp() { + var app = express(); + + app.engine('.tmpl', tmpl); + + return app; +} diff --git a/tests/express-tests/test/app.request.js b/tests/express-tests/test/app.request.js new file mode 100644 index 00000000..09338c02 --- /dev/null +++ b/tests/express-tests/test/app.request.js @@ -0,0 +1,143 @@ +'use strict' + +var after = require('after') +var express = require("express") + , request = require('supertest'); + +describe('app', function(){ + describe('.request', function(){ + it('should extend the request prototype', function(done){ + var app = express(); + + app.request.querystring = function(){ + return require('url').parse(this.url).query; + }; + + app.use(function(req, res){ + res.end(req.querystring()); + }); + + request(app) + .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/tests/express-tests/test/app.response.js b/tests/express-tests/test/app.response.js new file mode 100644 index 00000000..13e05b33 --- /dev/null +++ b/tests/express-tests/test/app.response.js @@ -0,0 +1,143 @@ +'use strict' + +var after = require('after') +var express = require("express") + , request = require('supertest'); + +describe('app', function(){ + describe('.response', function(){ + it('should extend the response prototype', function(done){ + var app = express(); + + app.response.shout = function(str){ + this.send(str.toUpperCase()); + }; + + app.use(function(req, res){ + res.shout('hey'); + }); + + request(app) + .get('/') + .expect('HEY', done); + }) + + it('should only extend for the referenced app', function (done) { + var app1 = express() + var app2 = express() + var cb = after(2, done) + + app1.response.shout = function (str) { + this.send(str.toUpperCase()) + } + + app1.get('/', function (req, res) { + res.shout('foo') + }) + + app2.get('/', function (req, res) { + res.shout('foo') + }) + + 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/tests/express-tests/test/app.route.js b/tests/express-tests/test/app.route.js new file mode 100644 index 00000000..cf6b1f1b --- /dev/null +++ b/tests/express-tests/test/app.route.js @@ -0,0 +1,64 @@ +'use strict' + +var express = require("express"); +var request = require('supertest'); + +describe('app.route', function(){ + it('should return a new route', function(done){ + var app = express(); + + app.route('/foo') + .get(function(req, res) { + res.send('get'); + }) + .post(function(req, res) { + res.send('post'); + }); + + request(app) + .post('/foo') + .expect('post', done); + }); + + it('should all .VERB after .all', function(done){ + var app = express(); + + app.route('/foo') + .all(function(req, res, next) { + next(); + }) + .get(function(req, res) { + res.send('get'); + }) + .post(function(req, res) { + res.send('post'); + }); + + request(app) + .post('/foo') + .expect('post', done); + }); + + it('should support dynamic routes', function(done){ + var app = express(); + + app.route('/:foo') + .get(function(req, res) { + res.send(req.params.foo); + }); + + request(app) + .get('/test') + .expect('test', done); + }); + + it('should not error on empty routes', function(done){ + var app = express(); + + app.route('/:foo'); + + request(app) + .get('/test') + .expect(404, done); + }); +}); diff --git a/tests/express-tests/test/app.router.js b/tests/express-tests/test/app.router.js new file mode 100644 index 00000000..eca3505a --- /dev/null +++ b/tests/express-tests/test/app.router.js @@ -0,0 +1,1142 @@ +'use strict' + +var after = require('after'); +var express = require("express") + , request = require('supertest') + , assert = require('assert') + , methods = require('methods'); + +var shouldSkipQuery = require('./support/utils').shouldSkipQuery + +describe('app.router', function(){ + it('should restore req.params after leaving router', function(done){ + var app = express(); + var router = new express.Router(); + + function handler1(req, res, next){ + res.setHeader('x-user-id', String(req.params.id)); + next() + } + + function handler2(req, res){ + res.send(req.params.id); + } + + router.use(function(req, res, next){ + res.setHeader('x-router', String(req.params.id)); + next(); + }); + + app.get('/user/:id', handler1, router, handler2); + + request(app) + .get('/user/1') + .expect('x-router', 'undefined') + .expect('x-user-id', '1') + .expect(200, '1', done); + }) + + describe('methods', function(){ + methods.concat('del').forEach(function(method){ + if (method === 'connect') return; + + it('should include ' + method.toUpperCase(), function(done){ + if (method === 'query' && shouldSkipQuery(process.versions.node)) { + this.skip() + } + var app = express(); + + app[method]('/foo', function(req, res){ + res.send(method) + }); + + request(app) + [method]('/foo') + .expect(200, done) + }) + + it('should reject numbers for app.' + method, function(){ + var app = express(); + assert.throws(app[method].bind(app, '/', 3), /Number/) + }) + }); + + it('should re-route when method is altered', function (done) { + var app = express(); + var cb = after(3, done); + + app.use(function (req, res, next) { + if (req.method !== 'POST') return next(); + req.method = 'DELETE'; + res.setHeader('X-Method-Altered', '1'); + next(); + }); + + app.delete('/', function (req, res) { + res.end('deleted everything'); + }); + + request(app) + .get('/') + .expect(404, cb) + + request(app) + .delete('/') + .expect(200, 'deleted everything', cb); + + request(app) + .post('/') + .expect('X-Method-Altered', '1') + .expect(200, 'deleted everything', cb); + }); + }) + + describe('decode params', function () { + it('should decode correct params', function(done){ + var app = express(); + + app.get('/:name', function (req, res) { + res.send(req.params.name); + }); + + request(app) + .get('/foo%2Fbar') + .expect('foo/bar', done); + }) + + it('should not accept params in malformed paths', function(done) { + var app = express(); + + app.get('/:name', function (req, res) { + res.send(req.params.name); + }); + + request(app) + .get('/%foobar') + .expect(400, done); + }) + + it('should not decode spaces', function(done) { + var app = express(); + + app.get('/:name', function (req, res) { + res.send(req.params.name); + }); + + request(app) + .get('/foo+bar') + .expect('foo+bar', done); + }) + + it('should work with unicode', function(done) { + var app = express(); + + app.get('/:name', function (req, res) { + res.send(req.params.name); + }); + + request(app) + .get('/%ce%b1') + .expect('\u03b1', done); + }) + }) + + it('should be .use()able', function(done){ + var app = express(); + + var calls = []; + + app.use(function(req, res, next){ + calls.push('before'); + next(); + }); + + app.get('/', function(req, res, next){ + calls.push('GET /') + next(); + }); + + app.use(function(req, res, next){ + calls.push('after'); + res.json(calls) + }); + + request(app) + .get('/') + .expect(200, ['before', 'GET /', 'after'], done) + }) + + describe('when given a regexp', function(){ + it('should match the pathname only', function(done){ + var app = express(); + + app.get(/^\/user\/[0-9]+$/, function(req, res){ + res.end('user'); + }); + + request(app) + .get('/user/12?foo=bar') + .expect('user', done); + }) + + it('should populate req.params with the captures', function(done){ + var app = express(); + + app.get(/^\/user\/([0-9]+)\/(view|edit)?$/, function(req, res){ + var id = req.params[0] + , op = req.params[1]; + res.end(op + 'ing user ' + id); + }); + + request(app) + .get('/user/10/edit') + .expect('editing user 10', done); + }) + + if (supportsRegexp('(?.*)')) { + it('should populate req.params with named captures', function(done){ + var app = express(); + var re = new RegExp('^/user/(?[0-9]+)/(view|edit)?$'); + + app.get(re, function(req, res){ + var id = req.params.userId + , op = req.params[0]; + res.end(op + 'ing user ' + id); + }); + + request(app) + .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(){ + it('should be disabled by default', function(done){ + var app = express(); + + app.get('/user', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/USER') + .expect('tj', done); + }) + + describe('when "case sensitive routing" is enabled', function(){ + it('should match identical casing', function(done){ + var app = express(); + + app.enable('case sensitive routing'); + + app.get('/uSer', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/uSer') + .expect('tj', done); + }) + + it('should not match otherwise', function(done){ + var app = express(); + + app.enable('case sensitive routing'); + + app.get('/uSer', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/user') + .expect(404, done); + }) + }) + }) + + describe('params', function(){ + it('should overwrite existing req.params by default', function(done){ + var app = express(); + var router = new express.Router(); + + router.get('/:action', function(req, res){ + res.send(req.params); + }); + + app.use('/user/:user', router); + + request(app) + .get('/user/1/get') + .expect(200, '{"action":"get"}', done); + }) + + it('should allow merging existing req.params', function(done){ + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get('/:action', function(req, res){ + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function(k){ return [k, req.params[k]] })); + }); + + app.use('/user/:user', router); + + request(app) + .get('/user/tj/get') + .expect(200, '[["action","get"],["user","tj"]]', done); + }) + + it('should use params from router', function(done){ + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get('/:thing', function(req, res){ + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function(k){ return [k, req.params[k]] })); + }); + + app.use('/user/:thing', router); + + request(app) + .get('/user/tj/get') + .expect(200, '[["thing","get"]]', done); + }) + + it('should merge numeric indices req.params', function(done){ + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get('/*.*', function(req, res){ + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function(k){ return [k, req.params[k]] })); + }); + + app.use('/user/id:(\\d+)', router); + + request(app) + .get('/user/id:10/profile.json') + .expect(200, '[["0","10"],["1","profile"],["2","json"]]', done); + }) + + it('should merge numeric indices req.params when more in parent', function(done){ + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get('/*', function(req, res){ + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function(k){ return [k, req.params[k]] })); + }); + + app.use('/user/id:(\\d+)/name:(\\w+)', router); + + request(app) + .get('/user/id:10/name:tj/profile') + .expect(200, '[["0","10"],["1","tj"],["2","profile"]]', done); + }) + + it('should merge numeric indices req.params when parent has same number', function(done){ + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get('/name:(\\w+)', function(req, res){ + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function(k){ return [k, req.params[k]] })); + }); + + app.use('/user/id:(\\d+)', router); + + request(app) + .get('/user/id:10/name:tj') + .expect(200, '[["0","10"],["1","tj"]]', done); + }) + + it('should ignore invalid incoming req.params', function(done){ + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get('/:name', function(req, res){ + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function(k){ return [k, req.params[k]] })); + }); + + app.use('/user/', function (req, res, next) { + req.params = 3; // wat? + router(req, res, next); + }); + + request(app) + .get('/user/tj') + .expect(200, '[["name","tj"]]', done); + }) + + it('should restore req.params', function(done){ + var app = express(); + var router = new express.Router({ mergeParams: true }); + + router.get('/user:(\\w+)/*', function (req, res, next) { + next(); + }); + + app.use('/user/id:(\\d+)', function (req, res, next) { + router(req, res, function (err) { + var keys = Object.keys(req.params).sort(); + res.send(keys.map(function(k){ return [k, req.params[k]] })); + }); + }); + + request(app) + .get('/user/id:42/user:tj/profile') + .expect(200, '[["0","42"]]', done); + }) + }) + + describe('trailing slashes', function(){ + it('should be optional by default', function(done){ + var app = express(); + + app.get('/user', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/user/') + .expect('tj', done); + }) + + describe('when "strict routing" is enabled', function(){ + it('should match trailing slashes', function(done){ + var app = express(); + + app.enable('strict routing'); + + app.get('/user/', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/user/') + .expect('tj', done); + }) + + it('should pass-though middleware', function(done){ + var app = express(); + + app.enable('strict routing'); + + app.use(function (req, res, next) { + res.setHeader('x-middleware', 'true'); + next(); + }); + + app.get('/user/', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/user/') + .expect('x-middleware', 'true') + .expect(200, 'tj', done); + }) + + it('should pass-though mounted middleware', function(done){ + var app = express(); + + app.enable('strict routing'); + + app.use('/user/', function (req, res, next) { + res.setHeader('x-middleware', 'true'); + next(); + }); + + app.get('/user/test/', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/user/test/') + .expect('x-middleware', 'true') + .expect(200, 'tj', done); + }) + + it('should match no slashes', function(done){ + var app = express(); + + app.enable('strict routing'); + + app.get('/user', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/user') + .expect('tj', done); + }) + + it('should match middleware when omitting the trailing slash', function(done){ + var app = express(); + + app.enable('strict routing'); + + app.use('/user/', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/user') + .expect(200, 'tj', done); + }) + + it('should match middleware', function(done){ + var app = express(); + + app.enable('strict routing'); + + app.use('/user', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/user') + .expect(200, 'tj', done); + }) + + it('should match middleware when adding the trailing slash', function(done){ + var app = express(); + + app.enable('strict routing'); + + app.use('/user', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/user/') + .expect(200, 'tj', done); + }) + + it('should fail when omitting the trailing slash', function(done){ + var app = express(); + + app.enable('strict routing'); + + app.get('/user/', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/user') + .expect(404, done); + }) + + it('should fail when adding the trailing slash', function(done){ + var app = express(); + + app.enable('strict routing'); + + app.get('/user', function(req, res){ + res.end('tj'); + }); + + request(app) + .get('/user/') + .expect(404, done); + }) + }) + }) + + it('should allow escaped regexp', function(done){ + var app = express(); + + app.get('/user/\\d+', function(req, res){ + res.end('woot'); + }); + + request(app) + .get('/user/10') + .expect(200, function (err) { + if (err) return done(err) + request(app) + .get('/user/tj') + .expect(404, done); + }); + }) + + it('should allow literal "."', function(done){ + var app = express(); + + app.get('/api/users/:from..:to', function(req, res){ + var from = req.params.from + , to = req.params.to; + + res.end('users from ' + from + ' to ' + to); + }); + + request(app) + .get('/api/users/1..50') + .expect('users from 1 to 50', done); + }) + + describe('*', function(){ + it('should capture everything', function (done) { + var app = express() + + app.get('*', function (req, res) { + res.end(req.params[0]) + }) + + request(app) + .get('/user/tobi.json') + .expect('/user/tobi.json', done) + }) + + it('should decode the capture', function (done) { + var app = express() + + app.get('*', function (req, res) { + res.end(req.params[0]) + }) + + request(app) + .get('/user/tobi%20and%20loki.json') + .expect('/user/tobi and loki.json', done) + }) + + it('should denote a greedy capture group', function(done){ + var app = express(); + + app.get('/user/*.json', function(req, res){ + res.end(req.params[0]); + }); + + request(app) + .get('/user/tj.json') + .expect('tj', done); + }) + + it('should work with several', function(done){ + var app = express(); + + app.get('/api/*.*', function(req, res){ + var resource = req.params[0] + , format = req.params[1]; + res.end(resource + ' as ' + format); + }); + + request(app) + .get('/api/users/foo.bar.json') + .expect('users/foo.bar as json', done); + }) + + 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(200, '', cb) + + request(app) + .get('/api/hey') + .expect(200, '/hey', cb) + }) + + it('should allow naming', function(done){ + var app = express(); + + app.get('/api/:resource(*)', function(req, res){ + var resource = req.params.resource; + res.end(resource); + }); + + request(app) + .get('/api/users/0.json') + .expect('users/0.json', done); + }) + + it('should not be greedy immediately after param', function(done){ + var app = express(); + + app.get('/user/:user*', function(req, res){ + res.end(req.params.user); + }); + + request(app) + .get('/user/122') + .expect('122', done); + }) + + it('should eat everything after /', function(done){ + var app = express(); + + app.get('/user/:user*', function(req, res){ + res.end(req.params.user); + }); + + request(app) + .get('/user/122/aaa') + .expect('122', done); + }) + + it('should span multiple segments', function(done){ + var app = express(); + + app.get('/file/*', function(req, res){ + res.end(req.params[0]); + }); + + request(app) + .get('/file/javascripts/jquery.js') + .expect('javascripts/jquery.js', done); + }) + + it('should be optional', function(done){ + var app = express(); + + app.get('/file/*', function(req, res){ + res.end(req.params[0]); + }); + + request(app) + .get('/file/') + .expect('', done); + }) + + it('should require a preceding /', function(done){ + var app = express(); + + app.get('/file/*', function(req, res){ + res.end(req.params[0]); + }); + + request(app) + .get('/file') + .expect(404, done); + }) + + it('should keep correct parameter indexes', function(done){ + var app = express(); + + app.get('/*/user/:id', function (req, res) { + res.send(req.params); + }); + + request(app) + .get('/1/user/2') + .expect(200, '{"0":"1","id":"2"}', done); + }) + + it('should work within arrays', function(done){ + var app = express(); + + app.get(['/user/:id', '/foo/*', '/:bar'], function (req, res) { + res.send(req.params.bar); + }); + + request(app) + .get('/test') + .expect(200, 'test', done); + }) + }) + + describe(':name', function(){ + it('should denote a capture group', function(done){ + var app = express(); + + app.get('/user/:user', function(req, res){ + res.end(req.params.user); + }); + + request(app) + .get('/user/tj') + .expect('tj', done); + }) + + it('should match a single segment only', function(done){ + var app = express(); + + app.get('/user/:user', function(req, res){ + res.end(req.params.user); + }); + + request(app) + .get('/user/tj/edit') + .expect(404, done); + }) + + it('should allow several capture groups', function(done){ + var app = express(); + + app.get('/user/:user/:op', function(req, res){ + res.end(req.params.op + 'ing ' + req.params.user); + }); + + request(app) + .get('/user/tj/edit') + .expect('editing tj', done); + }) + + it('should work following a partial capture group', function(done){ + var app = express(); + var cb = after(2, done); + + app.get('/user(s)?/:user/:op', function(req, res){ + res.end(req.params.op + 'ing ' + req.params.user + (req.params[0] ? ' (old)' : '')); + }); + + request(app) + .get('/user/tj/edit') + .expect('editing tj', cb); + + request(app) + .get('/users/tj/edit') + .expect('editing tj (old)', cb); + }) + + it('should work inside literal parenthesis', function(done){ + var app = express(); + + app.get('/:user\\(:op\\)', function(req, res){ + res.end(req.params.op + 'ing ' + req.params.user); + }); + + request(app) + .get('/tj(edit)') + .expect('editing tj', done); + }) + + it('should work in array of paths', function(done){ + var app = express(); + var cb = after(2, done); + + app.get(['/user/:user/poke', '/user/:user/pokes'], function(req, res){ + res.end('poking ' + req.params.user); + }); + + request(app) + .get('/user/tj/poke') + .expect('poking tj', cb); + + request(app) + .get('/user/tj/pokes') + .expect('poking tj', cb); + }) + }) + + describe(':name?', function(){ + it('should denote an optional capture group', function(done){ + var app = express(); + + app.get('/user/:user/:op?', function(req, res){ + var op = req.params.op || 'view'; + res.end(op + 'ing ' + req.params.user); + }); + + request(app) + .get('/user/tj') + .expect('viewing tj', done); + }) + + it('should populate the capture group', function(done){ + var app = express(); + + app.get('/user/:user/:op?', function(req, res){ + var op = req.params.op || 'view'; + res.end(op + 'ing ' + req.params.user); + }); + + request(app) + .get('/user/tj/edit') + .expect('editing tj', done); + }) + }) + + 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(200, 'foo as json', cb) + + request(app) + .get('/foo') + .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(200, 'foo as html', cb) + + request(app) + .get('/foo.json') + .expect(200, 'foo as json', cb) + }) + }) + + describe('when next() is called', function(){ + it('should continue lookup', function(done){ + var app = express() + , calls = []; + + app.get('/foo/:bar?', function(req, res, next){ + calls.push('/foo/:bar?'); + next(); + }); + + app.get('/bar', function () { + assert(0); + }); + + app.get('/foo', function(req, res, next){ + calls.push('/foo'); + next(); + }); + + app.get('/foo', function (req, res) { + calls.push('/foo 2'); + res.json(calls) + }); + + request(app) + .get('/foo') + .expect(200, ['/foo/:bar?', '/foo', '/foo 2'], done) + }) + }) + + describe('when next("route") is called', function(){ + it('should jump to next route', function(done){ + var app = express() + + function fn(req, res, next){ + res.set('X-Hit', '1') + next('route') + } + + app.get('/foo', fn, function (req, res) { + res.end('failure') + }); + + app.get('/foo', function(req, res){ + res.end('success') + }) + + request(app) + .get('/foo') + .expect('X-Hit', '1') + .expect(200, 'success', done) + }) + }) + + describe('when next("router") is called', function () { + it('should jump out of router', function (done) { + var app = express() + var router = express.Router() + + function fn (req, res, next) { + res.set('X-Hit', '1') + next('router') + } + + router.get('/foo', fn, function (req, res) { + res.end('failure') + }) + + router.get('/foo', function (req, res) { + res.end('failure') + }) + + app.use(router) + + app.get('/foo', function (req, res) { + res.end('success') + }) + + request(app) + .get('/foo') + .expect('X-Hit', '1') + .expect(200, 'success', done) + }) + }) + + describe('when next(err) is called', function(){ + it('should break out of app.router', function(done){ + var app = express() + , calls = []; + + app.get('/foo/:bar?', function(req, res, next){ + calls.push('/foo/:bar?'); + next(); + }); + + app.get('/bar', function () { + assert(0); + }); + + app.get('/foo', function(req, res, next){ + calls.push('/foo'); + next(new Error('fail')); + }); + + app.get('/foo', function () { + assert(0); + }); + + app.use(function(err, req, res, next){ + res.json({ + calls: calls, + error: err.message + }) + }) + + request(app) + .get('/foo') + .expect(200, { calls: ['/foo/:bar?', '/foo'], error: 'fail' }, done) + }) + + it('should call handler in same route, if exists', function(done){ + var app = express(); + + function fn1(req, res, next) { + next(new Error('boom!')); + } + + function fn2(req, res, next) { + res.send('foo here'); + } + + function fn3(err, req, res, next) { + res.send('route go ' + err.message); + } + + app.get('/foo', fn1, fn2, fn3); + + app.use(function (err, req, res, next) { + res.end('error!'); + }) + + request(app) + .get('/foo') + .expect('route go boom!', done) + }) + }) + + it('should allow rewriting of the url', function(done){ + var app = express(); + + app.get('/account/edit', function(req, res, next){ + req.user = { id: 12 }; // faux authenticated user + req.url = '/user/' + req.user.id + '/edit'; + next(); + }); + + app.get('/user/:id/edit', function(req, res){ + res.send('editing user ' + req.params.id); + }); + + request(app) + .get('/account/edit') + .expect('editing user 12', done); + }) + + it('should run in order added', function(done){ + var app = express(); + var path = []; + + app.get('*', function(req, res, next){ + path.push(0); + next(); + }); + + app.get('/user/:id', function(req, res, next){ + path.push(1); + next(); + }); + + app.use(function(req, res, next){ + path.push(2); + next(); + }); + + app.all('/user/:id', function(req, res, next){ + path.push(3); + next(); + }); + + app.get('*', function(req, res, next){ + path.push(4); + next(); + }); + + app.use(function(req, res, next){ + path.push(5); + res.end(path.join(',')) + }); + + request(app) + .get('/user/1') + .expect(200, '0,1,2,3,4,5', done); + }) + + it('should be chainable', function(){ + var app = express(); + assert.strictEqual(app.get('/', function () {}), app) + }) +}) + +function supportsRegexp(source) { + try { + new RegExp(source) + return true + } catch (e) { + return false + } +} diff --git a/tests/express-tests/test/app.routes.error.js b/tests/express-tests/test/app.routes.error.js new file mode 100644 index 00000000..f0853fa2 --- /dev/null +++ b/tests/express-tests/test/app.routes.error.js @@ -0,0 +1,62 @@ +'use strict' + +var assert = require('assert') +var express = require("express") + , request = require('supertest'); + +describe('app', function(){ + describe('.VERB()', function(){ + it('should not get invoked without error handler on error', function(done) { + var app = express(); + + app.use(function(req, res, next){ + next(new Error('boom!')) + }); + + app.get('/bar', function(req, res){ + res.send('hello, world!'); + }); + + request(app) + .post('/bar') + .expect(500, /Error: boom!/, done); + }); + + it('should only call an error handling routing callback when an error is propagated', function(done){ + var app = express(); + + var a = false; + var b = false; + var c = false; + var d = false; + + app.get('/', function(req, res, next){ + next(new Error('fabricated error')); + }, function(req, res, next) { + a = true; + next(); + }, function(err, req, res, next){ + b = true; + assert.strictEqual(err.message, 'fabricated error') + next(err); + }, function(err, req, res, next){ + c = true; + assert.strictEqual(err.message, 'fabricated error') + next(); + }, function(err, req, res, next){ + d = true; + next(); + }, function(req, res){ + assert.ok(!a) + assert.ok(b) + assert.ok(c) + assert.ok(!d) + res.send(204); + }); + + request(app) + .get('/') + .expect(204, done); + }) + }) +}) diff --git a/tests/express-tests/test/app.use.js b/tests/express-tests/test/app.use.js new file mode 100644 index 00000000..6942fe2e --- /dev/null +++ b/tests/express-tests/test/app.use.js @@ -0,0 +1,542 @@ +'use strict' + +var after = require('after'); +var assert = require('assert') +var express = require("express"); +var request = require('supertest'); + +describe('app', function(){ + it('should emit "mount" when mounted', function(done){ + var blog = express() + , app = express(); + + blog.on('mount', function(arg){ + assert.strictEqual(arg, app) + done(); + }); + + app.use(blog); + }) + + describe('.use(app)', function(){ + it('should mount the app', function(done){ + var blog = express() + , app = express(); + + blog.get('/blog', function(req, res){ + res.end('blog'); + }); + + app.use(blog); + + request(app) + .get('/blog') + .expect('blog', done); + }) + + it('should support mount-points', function(done){ + var blog = express() + , forum = express() + , app = express(); + var cb = after(2, done) + + blog.get('/', function(req, res){ + res.end('blog'); + }); + + forum.get('/', function(req, res){ + res.end('forum'); + }); + + app.use('/blog', blog); + app.use('/forum', forum); + + request(app) + .get('/blog') + .expect(200, 'blog', cb) + + request(app) + .get('/forum') + .expect(200, 'forum', cb) + }) + + it('should set the child\'s .parent', function(){ + var blog = express() + , app = express(); + + app.use('/blog', blog); + assert.strictEqual(blog.parent, app) + }) + + it('should support dynamic routes', function(done){ + var blog = express() + , app = express(); + + blog.get('/', function(req, res){ + res.end('success'); + }); + + app.use('/post/:article', blog); + + request(app) + .get('/post/once-upon-a-time') + .expect('success', done); + }) + + it('should support mounted app anywhere', function(done){ + var cb = after(3, done); + var blog = express() + , other = express() + , app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + blog.get('/', function(req, res){ + res.end('success'); + }); + + blog.once('mount', function (parent) { + assert.strictEqual(parent, app) + cb(); + }); + other.once('mount', function (parent) { + assert.strictEqual(parent, app) + cb(); + }); + + app.use('/post/:article', fn1, other, fn2, blog); + + request(app) + .get('/post/once-upon-a-time') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('success', cb); + }) + }) + + describe('.use(middleware)', function(){ + it('should accept multiple arguments', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + app.use(fn1, fn2, function fn3(req, res) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + }); + + request(app) + .get('/') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should invoke middleware for all requests', function (done) { + var app = express(); + var cb = after(3, done); + + app.use(function (req, res) { + res.send('saw ' + req.method + ' ' + req.url); + }); + + request(app) + .get('/') + .expect(200, 'saw GET /', cb); + + request(app) + .options('/') + .expect(200, 'saw OPTIONS /', cb); + + request(app) + .post('/foo') + .expect(200, 'saw POST /foo', cb); + }) + + it('should accept array of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use([fn1, fn2, fn3]); + + request(app) + .get('/') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should accept multiple arrays of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use([fn1, fn2], [fn3]); + + request(app) + .get('/') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should accept nested arrays of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use([[fn1], fn2], [fn3]); + + request(app) + .get('/') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + }) + + describe('.use(path, middleware)', function(){ + it('should require middleware', function () { + var app = express() + assert.throws(function () { app.use('/') }, /requires a middleware function/) + }) + + 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) { + var app = express(); + + app.use('/foo', function (req, res) { + res.send('saw ' + req.method + ' ' + req.url); + }); + + request(app) + .get('/foo/bar') + .expect(200, 'saw GET /bar', done); + }) + + it('should accept multiple arguments', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + app.use('/foo', fn1, fn2, function fn3(req, res) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + }); + + request(app) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should invoke middleware for all requests starting with path', function (done) { + var app = express(); + var cb = after(3, done); + + app.use('/foo', function (req, res) { + res.send('saw ' + req.method + ' ' + req.url); + }); + + request(app) + .get('/') + .expect(404, cb); + + request(app) + .post('/foo') + .expect(200, 'saw POST /', cb); + + request(app) + .post('/foo/bar') + .expect(200, 'saw POST /bar', cb); + }) + + it('should work if path has trailing slash', function (done) { + var app = express(); + var cb = after(3, done); + + app.use('/foo/', function (req, res) { + res.send('saw ' + req.method + ' ' + req.url); + }); + + request(app) + .get('/') + .expect(404, cb); + + request(app) + .post('/foo') + .expect(200, 'saw POST /', cb); + + request(app) + .post('/foo/bar') + .expect(200, 'saw POST /bar', cb); + }) + + it('should accept array of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use('/foo', [fn1, fn2, fn3]); + + request(app) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should accept multiple arrays of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use('/foo', [fn1, fn2], [fn3]); + + request(app) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should accept nested arrays of middleware', function (done) { + var app = express(); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.end(); + } + + app.use('/foo', [fn1, [fn2]], [fn3]); + + request(app) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, done); + }) + + it('should support array of paths', function (done) { + var app = express(); + var cb = after(3, done); + + app.use(['/foo/', '/bar'], function (req, res) { + res.send('saw ' + req.method + ' ' + req.url + ' through ' + req.originalUrl); + }); + + request(app) + .get('/') + .expect(404, cb); + + request(app) + .get('/foo') + .expect(200, 'saw GET / through /foo', cb); + + request(app) + .get('/bar') + .expect(200, 'saw GET / through /bar', cb); + }) + + it('should support array of paths with middleware array', function (done) { + var app = express(); + var cb = after(2, done); + + function fn1(req, res, next) { + res.setHeader('x-fn-1', 'hit'); + next(); + } + + function fn2(req, res, next) { + res.setHeader('x-fn-2', 'hit'); + next(); + } + + function fn3(req, res, next) { + res.setHeader('x-fn-3', 'hit'); + res.send('saw ' + req.method + ' ' + req.url + ' through ' + req.originalUrl); + } + + app.use(['/foo/', '/bar'], [[fn1], fn2], [fn3]); + + request(app) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, 'saw GET / through /foo', cb); + + request(app) + .get('/bar') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, 'saw GET / through /bar', cb); + }) + + it('should support regexp path', function (done) { + var app = express(); + var cb = after(4, done); + + app.use(/^\/[a-z]oo/, function (req, res) { + res.send('saw ' + req.method + ' ' + req.url + ' through ' + req.originalUrl); + }); + + request(app) + .get('/') + .expect(404, cb); + + request(app) + .get('/foo') + .expect(200, 'saw GET / through /foo', cb); + + request(app) + .get('/zoo/bear') + .expect(200, 'saw GET /bear through /zoo/bear', cb); + + request(app) + .get('/get/zoo') + .expect(404, cb); + }) + + it('should support empty string path', function (done) { + var app = express(); + + app.use('', function (req, res) { + res.send('saw ' + req.method + ' ' + req.url + ' through ' + req.originalUrl); + }); + + request(app) + .get('/') + .expect(200, 'saw GET / through /', done); + }) + }) +}) diff --git a/tests/express-tests/test/config.js b/tests/express-tests/test/config.js new file mode 100644 index 00000000..77d6a6b3 --- /dev/null +++ b/tests/express-tests/test/config.js @@ -0,0 +1,207 @@ +'use strict' + +var assert = require('assert'); +var express = require("express"); + +describe('config', function () { + describe('.set()', function () { + it('should set a value', function () { + var app = express(); + app.set('foo', 'bar'); + assert.equal(app.get('foo'), 'bar'); + }) + + it('should set prototype values', function () { + var app = express() + app.set('hasOwnProperty', 42) + assert.strictEqual(app.get('hasOwnProperty'), 42) + }) + + it('should return the app', function () { + var app = express(); + assert.equal(app.set('foo', 'bar'), app); + }) + + it('should return the app when undefined', function () { + var app = express(); + assert.equal(app.set('foo', undefined), app); + }) + + it('should return set value', function () { + var app = express() + app.set('foo', 'bar') + assert.strictEqual(app.set('foo'), 'bar') + }) + + it('should return undefined for prototype values', function () { + var app = express() + assert.strictEqual(app.set('hasOwnProperty'), undefined) + }) + + describe('"etag"', function(){ + it('should throw on bad value', function(){ + var app = express(); + assert.throws(app.set.bind(app, 'etag', 42), /unknown value/); + }) + + it('should set "etag fn"', function(){ + var app = express() + var fn = function(){} + app.set('etag', fn) + assert.equal(app.get('etag fn'), fn) + }) + }) + + describe('"trust proxy"', function(){ + it('should set "trust proxy fn"', function(){ + var app = express() + var fn = function(){} + app.set('trust proxy', fn) + assert.equal(app.get('trust proxy fn'), fn) + }) + }) + }) + + describe('.get()', function(){ + it('should return undefined when unset', function(){ + var app = express(); + assert.strictEqual(app.get('foo'), undefined); + }) + + it('should return undefined for prototype values', function () { + var app = express() + assert.strictEqual(app.get('hasOwnProperty'), undefined) + }) + + it('should otherwise return the value', function(){ + var app = express(); + app.set('foo', 'bar'); + assert.equal(app.get('foo'), 'bar'); + }) + + describe('when mounted', function(){ + it('should default to the parent app', function(){ + var app = express(); + var blog = express(); + + app.set('title', 'Express'); + app.use(blog); + assert.equal(blog.get('title'), 'Express'); + }) + + it('should given precedence to the child', function(){ + var app = express(); + var blog = express(); + + app.use(blog); + app.set('title', 'Express'); + blog.set('title', 'Some Blog'); + + assert.equal(blog.get('title'), 'Some Blog'); + }) + + it('should inherit "trust proxy" setting', function () { + var app = express(); + var blog = express(); + + function fn() { return false } + + app.set('trust proxy', fn); + assert.equal(app.get('trust proxy'), fn); + assert.equal(app.get('trust proxy fn'), fn); + + app.use(blog); + + assert.equal(blog.get('trust proxy'), fn); + assert.equal(blog.get('trust proxy fn'), fn); + }) + + it('should prefer child "trust proxy" setting', function () { + var app = express(); + var blog = express(); + + function fn1() { return false } + function fn2() { return true } + + app.set('trust proxy', fn1); + assert.equal(app.get('trust proxy'), fn1); + assert.equal(app.get('trust proxy fn'), fn1); + + blog.set('trust proxy', fn2); + assert.equal(blog.get('trust proxy'), fn2); + assert.equal(blog.get('trust proxy fn'), fn2); + + app.use(blog); + + assert.equal(app.get('trust proxy'), fn1); + assert.equal(app.get('trust proxy fn'), fn1); + assert.equal(blog.get('trust proxy'), fn2); + assert.equal(blog.get('trust proxy fn'), fn2); + }) + }) + }) + + describe('.enable()', function(){ + it('should set the value to true', function(){ + var app = express(); + assert.equal(app.enable('tobi'), app); + assert.strictEqual(app.get('tobi'), true); + }) + + it('should set prototype values', function () { + var app = express() + app.enable('hasOwnProperty') + assert.strictEqual(app.get('hasOwnProperty'), true) + }) + }) + + describe('.disable()', function(){ + it('should set the value to false', function(){ + var app = express(); + assert.equal(app.disable('tobi'), app); + assert.strictEqual(app.get('tobi'), false); + }) + + it('should set prototype values', function () { + var app = express() + app.disable('hasOwnProperty') + assert.strictEqual(app.get('hasOwnProperty'), 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); + }) + + it('should default to false for prototype values', function () { + var app = express() + assert.strictEqual(app.enabled('hasOwnProperty'), false) + }) + }) + + 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'); + assert.strictEqual(app.disabled('foo'), false); + }) + + it('should default to true for prototype values', function () { + var app = express() + assert.strictEqual(app.disabled('hasOwnProperty'), true) + }) + }) +}) diff --git a/tests/express-tests/test/exports.js b/tests/express-tests/test/exports.js new file mode 100644 index 00000000..3bbb1b48 --- /dev/null +++ b/tests/express-tests/test/exports.js @@ -0,0 +1,87 @@ +'use strict' + +var assert = require('assert') +var express = require("express"); +var request = require('supertest'); + +describe('exports', function(){ + it('should expose Router', function(){ + assert.strictEqual(typeof express.Router, '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(){ + assert.strictEqual(typeof express.application, 'object') + assert.strictEqual(typeof express.application.set, 'function') + }) + + it('should expose the request prototype', function(){ + assert.strictEqual(typeof express.request, 'object') + assert.strictEqual(typeof express.request.accepts, 'function') + }) + + it('should expose the response prototype', function(){ + assert.strictEqual(typeof express.response, 'object') + assert.strictEqual(typeof express.response.send, 'function') + }) + + it('should permit modifying the .application prototype', function(){ + express.application.foo = function(){ return 'bar'; }; + assert.strictEqual(express().foo(), 'bar') + }) + + it('should permit modifying the .request prototype', function(done){ + express.request.foo = function(){ return 'bar'; }; + var app = express(); + + app.use(function(req, res, next){ + res.end(req.foo()); + }); + + request(app) + .get('/') + .expect('bar', done); + }) + + it('should permit modifying the .response prototype', function(done){ + express.response.foo = function(){ this.send('bar'); }; + var app = express(); + + app.use(function(req, res, next){ + res.foo(); + }); + + request(app) + .get('/') + .expect('bar', done); + }) + + it('should throw on old middlewares', function(){ + assert.throws(function () { express.bodyParser() }, /Error:.*middleware.*bodyParser/) + assert.throws(function () { express.limit() }, /Error:.*middleware.*limit/) + }) +}) diff --git a/tests/express-tests/test/express.json.js b/tests/express-tests/test/express.json.js new file mode 100644 index 00000000..7872f890 --- /dev/null +++ b/tests/express-tests/test/express.json.js @@ -0,0 +1,790 @@ +'use strict' + +var assert = require('assert') +var asyncHooks = tryRequire('async_hooks') +var Buffer = require('safe-buffer').Buffer +var express = require("express") +var request = require('supertest') + +var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' + ? describe + : describe.skip + +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 only whitespace', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .send(' \n') + .expect(400, '[entity.parse.failed] ' + parseError(' '), 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 500 if stream not readable', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.on('end', next) + req.resume() + }) + + app.use(express.json()) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(500, '[stream.not.readable] stream is not readable', 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, '[entity.parse.failed] ' + parseError('{:'), done) + }) + + it('should 400 for incomplete', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user"') + .expect(400, '[entity.parse.failed] ' + parseError('{"user"'), 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, '[entity.too.large] request entity too large', done) + }) + + it('should 413 when over limit with chunked encoding', function (done) { + var app = createApp({ limit: '1kb' }) + var buf = Buffer.alloc(1024, '.') + var test = request(app).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 413 when inflated body over limit', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000aab562a2e2952b252d21b05a360148c58a0540b0066f7ce1e0a040000', 'hex')) + 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 app = createApp(options) + + options.limit = '100kb' + + request(app) + .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 app = createApp({ limit: '8kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/json') + test.write(buf) + test.write(buf) + test.write(buf) + test.expect(413, done) + }) + + it('should not error when inflating', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000aab562a2e2952b252d21b05a360148c58a0540b0066f7ce1e0a0400', 'hex')) + 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, '[encoding.unsupported] 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, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, '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, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, '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, '[entity.parse.failed] ' + parseError(' #rue').replace(/#/g, '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 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(/#/g, '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, '[entity.verify.failed] no arrays', 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, '[entity.verify.failed] 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') + .send('["tobi"]') + .expect(403, '[foo.bar] no arrays', 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, '[charset.unsupported] unsupported charset "X-BOGUS"', done) + }) + }) + + describeAsyncHooks('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(express.json()) + + app.use(function (req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + next() + }) + + app.use(function (err, req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + this.app = app + }) + + it('should presist store', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('{"user":"tobi"}') + .end(done) + }) + + it('should presist store when unmatched content-type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/fizzbuzz') + .send('buzz') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('{}') + .end(done) + }) + + it('should presist store when inflated', 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) + test.expect('x-store-foo', 'bar') + test.expect('{"name":"论"}') + test.end(done) + }) + + it('should presist store when inflate error', 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) + test.expect('x-store-foo', 'bar') + test.end(done) + }) + + it('should presist store when parse error', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":') + .expect(400) + .expect('x-store-foo', 'bar') + .end(done) + }) + + it('should presist store when limit exceeded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"' + Buffer.alloc(1024 * 100, '.').toString() + '"}') + .expect(413) + .expect('x-store-foo', 'bar') + .end(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, '[charset.unsupported] unsupported charset "KOI8-R"', 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, '[encoding.unsupported] unsupported content encoding "nulls"', 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(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.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 + '\'') + } +} + +function tryRequire (name) { + try { + return require(name) + } catch (e) { + return {} + } +} diff --git a/tests/express-tests/test/express.raw.js b/tests/express-tests/test/express.raw.js new file mode 100644 index 00000000..87cd20e0 --- /dev/null +++ b/tests/express-tests/test/express.raw.js @@ -0,0 +1,555 @@ +'use strict' + +var assert = require('assert') +var asyncHooks = tryRequire('async_hooks') +var Buffer = require('safe-buffer').Buffer +var express = require("express") +var request = require('supertest') + +var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' + ? describe + : describe.skip + +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 500 if stream not readable', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.on('end', next) + req.resume() + }) + + app.use(express.raw()) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + 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(500, '[stream.not.readable] stream is not readable', 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 413 when inflated body over limit', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a14704040000', 'hex')) + 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) + }) + + it('should not error when inflating', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a147040400', 'hex')) + 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, '[encoding.unsupported] 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, '[entity.verify.failed] 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, '[entity.verify.failed] 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) + }) + }) + + describeAsyncHooks('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(express.raw()) + + app.use(function (req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + next() + }) + + app.use(function (err, req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + this.app = app + }) + + it('should presist store', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is tobi') + .expect(200) + .expect('x-store-foo', 'bar') + .expect({ buf: '746865207573657220697320746f6269' }) + .end(done) + }) + + it('should presist store when unmatched content-type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/fizzbuzz') + .send('buzz') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('{}') + .end(done) + }) + + it('should presist store when inflated', 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) + test.expect('x-store-foo', 'bar') + test.expect({ buf: '6e616d653de8aeba' }) + test.end(done) + }) + + it('should presist store when inflate error', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad6080000', 'hex')) + test.expect(400) + test.expect('x-store-foo', 'bar') + test.end(done) + }) + + it('should presist store when limit exceeded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is ' + Buffer.alloc(1024 * 100, '.').toString()) + .expect(413) + .expect('x-store-foo', 'bar') + .end(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 415 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, '[encoding.unsupported] 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(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.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 +} + +function tryRequire (name) { + try { + return require(name) + } catch (e) { + return {} + } +} diff --git a/tests/express-tests/test/express.static.js b/tests/express-tests/test/express.static.js new file mode 100644 index 00000000..1ed16e16 --- /dev/null +++ b/tests/express-tests/test/express.static.js @@ -0,0 +1,814 @@ +'use strict' + +var assert = require('assert') +var Buffer = require('safe-buffer').Buffer +var express = require("express") +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, /\/users\//, 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\/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, '[entity.verify.failed] 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, '[entity.verify.failed] 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, '[charset.unsupported] unsupported charset "X-BOGUS"', done) + }) + }) + + describeAsyncHooks('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(express.text()) + + app.use(function (req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + next() + }) + + app.use(function (err, req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + this.app = app + }) + + it('should presist store', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/plain') + .send('user is tobi') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('"user is tobi"') + .end(done) + }) + + it('should presist store when unmatched content-type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/fizzbuzz') + .send('buzz') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('{}') + .end(done) + }) + + it('should presist store when inflated', 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) + test.expect('x-store-foo', 'bar') + test.expect('"name is 论"') + test.end(done) + }) + + it('should presist store when inflate error', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b0000', 'hex')) + test.expect(400) + test.expect('x-store-foo', 'bar') + test.end(done) + }) + + it('should presist store when limit exceeded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/plain') + .send('user is ' + Buffer.alloc(1024 * 100, '.').toString()) + .expect(413) + .expect('x-store-foo', 'bar') + .end(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, '[charset.unsupported] 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 415 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, '[encoding.unsupported] 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(String(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.message))) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + return app +} + +function tryRequire (name) { + try { + return require(name) + } catch (e) { + return {} + } +} diff --git a/tests/express-tests/test/express.urlencoded.js b/tests/express-tests/test/express.urlencoded.js new file mode 100644 index 00000000..de50c4f7 --- /dev/null +++ b/tests/express-tests/test/express.urlencoded.js @@ -0,0 +1,866 @@ +'use strict' + +var assert = require('assert') +var asyncHooks = tryRequire('async_hooks') +var Buffer = require('safe-buffer').Buffer +var express = require("express") +var request = require('supertest') + +var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' + ? describe + : describe.skip + +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 500 if stream not readable', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.on('end', next) + req.resume() + }) + + app.use(express.urlencoded()) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(500, '[stream.not.readable] stream is not readable', 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 < 32; 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, 32) + }) + .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, '[encoding.unsupported] 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 app = createApp({ limit: '1kb' }) + var buf = Buffer.alloc(1024, '.') + 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 413 when inflated body over limit', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f9204040000', 'hex')) + 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 app = createApp({ limit: '8kb' }) + var buf = Buffer.alloc(10240, '.') + 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) + }) + + it('should not error when inflating', function (done) { + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f92040400', 'hex')) + 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, '[parameters.too.many] too many parameters', 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, '[parameters.too.many] too many parameters', 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, '[entity.verify.failed] 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', 'application/x-www-form-urlencoded') + .send(' user=tobi') + .expect(400, '[entity.verify.failed] 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') + .send(' user=tobi') + .expect(403, '[foo.bar] no leading space', 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, '[charset.unsupported] unsupported charset "X-BOGUS"', done) + }) + }) + + describeAsyncHooks('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(express.urlencoded()) + + app.use(function (req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + next() + }) + + app.use(function (err, req, res, next) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + this.app = app + }) + + it('should presist store', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('{"user":"tobi"}') + .end(done) + }) + + it('should presist store when unmatched content-type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/fizzbuzz') + .send('buzz') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('{}') + .end(done) + }) + + it('should presist store when inflated', 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) + test.expect('x-store-foo', 'bar') + test.expect('{"name":"论"}') + test.end(done) + }) + + it('should presist store when inflate error', 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('1f8b080000000000000bcb4bcc4db57db16e170099a4bad6080000', 'hex')) + test.expect(400) + test.expect('x-store-foo', 'bar') + test.end(done) + }) + + it('should presist store when limit exceeded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=' + Buffer.alloc(1024 * 100, '.').toString()) + .expect(413) + .expect('x-store-foo', 'bar') + .end(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, '[charset.unsupported] 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 415 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, '[encoding.unsupported] 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(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.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) + } +} + +function tryRequire (name) { + try { + return require(name) + } catch (e) { + return {} + } +} diff --git a/tests/express-tests/test/fixtures/% of dogs.txt b/tests/express-tests/test/fixtures/% of dogs.txt new file mode 100644 index 00000000..3a4d1342 --- /dev/null +++ b/tests/express-tests/test/fixtures/% of dogs.txt @@ -0,0 +1 @@ +20% \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/.name b/tests/express-tests/test/fixtures/.name new file mode 100644 index 00000000..fa66f37f --- /dev/null +++ b/tests/express-tests/test/fixtures/.name @@ -0,0 +1 @@ +tobi \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/blog/index.html b/tests/express-tests/test/fixtures/blog/index.html new file mode 100644 index 00000000..bcc6b182 --- /dev/null +++ b/tests/express-tests/test/fixtures/blog/index.html @@ -0,0 +1 @@ +index \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/blog/post/index.tmpl b/tests/express-tests/test/fixtures/blog/post/index.tmpl new file mode 100644 index 00000000..a9a2a3b3 --- /dev/null +++ b/tests/express-tests/test/fixtures/blog/post/index.tmpl @@ -0,0 +1 @@ +

      blog post

      \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/broken.send b/tests/express-tests/test/fixtures/broken.send new file mode 100644 index 00000000..e69de29b diff --git a/tests/express-tests/test/fixtures/default_layout/name.tmpl b/tests/express-tests/test/fixtures/default_layout/name.tmpl new file mode 100644 index 00000000..0c49bf6b --- /dev/null +++ b/tests/express-tests/test/fixtures/default_layout/name.tmpl @@ -0,0 +1 @@ +

      $name

      \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/default_layout/user.tmpl b/tests/express-tests/test/fixtures/default_layout/user.tmpl new file mode 100644 index 00000000..67ef5102 --- /dev/null +++ b/tests/express-tests/test/fixtures/default_layout/user.tmpl @@ -0,0 +1 @@ +

      $user.name

      \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/email.tmpl b/tests/express-tests/test/fixtures/email.tmpl new file mode 100644 index 00000000..8a2cb77a --- /dev/null +++ b/tests/express-tests/test/fixtures/email.tmpl @@ -0,0 +1 @@ +

      This is an email

      \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/empty.txt b/tests/express-tests/test/fixtures/empty.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/express-tests/test/fixtures/local_layout/user.tmpl b/tests/express-tests/test/fixtures/local_layout/user.tmpl new file mode 100644 index 00000000..e9c8684e --- /dev/null +++ b/tests/express-tests/test/fixtures/local_layout/user.tmpl @@ -0,0 +1 @@ +$user.name \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/name.tmpl b/tests/express-tests/test/fixtures/name.tmpl new file mode 100644 index 00000000..0c49bf6b --- /dev/null +++ b/tests/express-tests/test/fixtures/name.tmpl @@ -0,0 +1 @@ +

      $name

      \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/name.txt b/tests/express-tests/test/fixtures/name.txt new file mode 100644 index 00000000..fa66f37f --- /dev/null +++ b/tests/express-tests/test/fixtures/name.txt @@ -0,0 +1 @@ +tobi \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/nums.txt b/tests/express-tests/test/fixtures/nums.txt new file mode 100644 index 00000000..e2e107ac --- /dev/null +++ b/tests/express-tests/test/fixtures/nums.txt @@ -0,0 +1 @@ +123456789 \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/pets/names.txt b/tests/express-tests/test/fixtures/pets/names.txt new file mode 100644 index 00000000..91407a3e --- /dev/null +++ b/tests/express-tests/test/fixtures/pets/names.txt @@ -0,0 +1 @@ +tobi,loki \ No newline at end of file diff --git "a/tests/express-tests/test/fixtures/snow \342\230\203/.gitkeep" "b/tests/express-tests/test/fixtures/snow \342\230\203/.gitkeep" new file mode 100644 index 00000000..e69de29b diff --git a/tests/express-tests/test/fixtures/todo.html b/tests/express-tests/test/fixtures/todo.html new file mode 100644 index 00000000..e7af6d79 --- /dev/null +++ b/tests/express-tests/test/fixtures/todo.html @@ -0,0 +1 @@ +
    • groceries
    • \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/todo.txt b/tests/express-tests/test/fixtures/todo.txt new file mode 100644 index 00000000..8c3539d9 --- /dev/null +++ b/tests/express-tests/test/fixtures/todo.txt @@ -0,0 +1 @@ +- groceries \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/user.html b/tests/express-tests/test/fixtures/user.html new file mode 100644 index 00000000..f5b9962c --- /dev/null +++ b/tests/express-tests/test/fixtures/user.html @@ -0,0 +1 @@ +

      {{user.name}}

      \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/user.tmpl b/tests/express-tests/test/fixtures/user.tmpl new file mode 100644 index 00000000..67ef5102 --- /dev/null +++ b/tests/express-tests/test/fixtures/user.tmpl @@ -0,0 +1 @@ +

      $user.name

      \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/users/index.html b/tests/express-tests/test/fixtures/users/index.html new file mode 100644 index 00000000..00a2db41 --- /dev/null +++ b/tests/express-tests/test/fixtures/users/index.html @@ -0,0 +1 @@ +

      tobi, loki, jane

      \ No newline at end of file diff --git a/tests/express-tests/test/fixtures/users/tobi.txt b/tests/express-tests/test/fixtures/users/tobi.txt new file mode 100644 index 00000000..9d9529d4 --- /dev/null +++ b/tests/express-tests/test/fixtures/users/tobi.txt @@ -0,0 +1 @@ +ferret \ No newline at end of file diff --git a/tests/express-tests/test/middleware.basic.js b/tests/express-tests/test/middleware.basic.js new file mode 100644 index 00000000..1a9dc011 --- /dev/null +++ b/tests/express-tests/test/middleware.basic.js @@ -0,0 +1,42 @@ +'use strict' + +var assert = require('assert') +var express = require("express"); +var request = require('supertest'); + +describe('middleware', function(){ + describe('.next()', function(){ + it('should behave like connect', function(done){ + var app = express() + , calls = []; + + app.use(function(req, res, next){ + calls.push('one'); + next(); + }); + + app.use(function(req, res, next){ + calls.push('two'); + next(); + }); + + app.use(function(req, res){ + var buf = ''; + res.setHeader('Content-Type', 'application/json'); + req.setEncoding('utf8'); + req.on('data', function(chunk){ buf += chunk }); + req.on('end', function(){ + res.end(buf); + }); + }); + + 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/tests/express-tests/test/regression.js b/tests/express-tests/test/regression.js new file mode 100644 index 00000000..914df92b --- /dev/null +++ b/tests/express-tests/test/regression.js @@ -0,0 +1,20 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('throw after .end()', function(){ + it('should fail gracefully', function(done){ + var app = express(); + + app.get('/', function(req, res){ + res.end('yay'); + throw new Error('boom'); + }); + + request(app) + .get('/') + .expect('yay') + .expect(200, done); + }) +}) diff --git a/tests/express-tests/test/req.accepts.js b/tests/express-tests/test/req.accepts.js new file mode 100644 index 00000000..f378b133 --- /dev/null +++ b/tests/express-tests/test/req.accepts.js @@ -0,0 +1,125 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.accepts(type)', function(){ + it('should return true when Accept is not present', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts('json') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .expect('yes', done); + }) + + it('should return true when present', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts('json') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('Accept', 'application/json') + .expect('yes', done); + }) + + it('should return false otherwise', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts('json') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('Accept', 'text/html') + .expect('no', done); + }) + }) + + it('should accept an argument list of type names', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts('json', 'html')); + }); + + request(app) + .get('/') + .set('Accept', 'application/json') + .expect('json', done); + }) + + describe('.accepts(types)', function(){ + it('should return the first when Accept is not present', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts(['json', 'html'])); + }); + + request(app) + .get('/') + .expect('json', done); + }) + + it('should return the first acceptable type', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts(['json', 'html'])); + }); + + request(app) + .get('/') + .set('Accept', 'text/html') + .expect('html', done); + }) + + it('should return false when no match is made', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts(['text/html', 'application/json']) ? 'yup' : 'nope'); + }); + + request(app) + .get('/') + .set('Accept', 'foo/bar, bar/baz') + .expect('nope', done); + }) + + it('should take quality into account', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts(['text/html', 'application/json'])); + }); + + request(app) + .get('/') + .set('Accept', '*/html; q=.5, application/json') + .expect('application/json', done); + }) + + it('should return the first acceptable type with canonical mime types', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.accepts(['application/json', 'text/html'])); + }); + + request(app) + .get('/') + .set('Accept', '*/html') + .expect('text/html', done); + }) + }) +}) diff --git a/tests/express-tests/test/req.acceptsCharset.js b/tests/express-tests/test/req.acceptsCharset.js new file mode 100644 index 00000000..800e9234 --- /dev/null +++ b/tests/express-tests/test/req.acceptsCharset.js @@ -0,0 +1,50 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.acceptsCharset(type)', function(){ + describe('when Accept-Charset is not present', function(){ + it('should return true', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.acceptsCharset('utf-8') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .expect('yes', done); + }) + }) + + describe('when Accept-Charset is present', function () { + it('should return true', function (done) { + var app = express(); + + app.use(function(req, res, next){ + res.end(req.acceptsCharset('utf-8') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('Accept-Charset', 'foo, bar, utf-8') + .expect('yes', done); + }) + + it('should return false otherwise', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.acceptsCharset('utf-8') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('Accept-Charset', 'foo, bar') + .expect('no', done); + }) + }) + }) +}) diff --git a/tests/express-tests/test/req.acceptsCharsets.js b/tests/express-tests/test/req.acceptsCharsets.js new file mode 100644 index 00000000..ab830778 --- /dev/null +++ b/tests/express-tests/test/req.acceptsCharsets.js @@ -0,0 +1,50 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.acceptsCharsets(type)', function(){ + describe('when Accept-Charset is not present', function(){ + it('should return true', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.acceptsCharsets('utf-8') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .expect('yes', done); + }) + }) + + describe('when Accept-Charset is present', function () { + it('should return true', function (done) { + var app = express(); + + app.use(function(req, res, next){ + res.end(req.acceptsCharsets('utf-8') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('Accept-Charset', 'foo, bar, utf-8') + .expect('yes', done); + }) + + it('should return false otherwise', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.end(req.acceptsCharsets('utf-8') ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('Accept-Charset', 'foo, bar') + .expect('no', done); + }) + }) + }) +}) diff --git a/tests/express-tests/test/req.acceptsEncoding.js b/tests/express-tests/test/req.acceptsEncoding.js new file mode 100644 index 00000000..c7e372ec --- /dev/null +++ b/tests/express-tests/test/req.acceptsEncoding.js @@ -0,0 +1,39 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.acceptsEncoding', function(){ + it('should return encoding if accepted', function (done) { + var app = express(); + + 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, { gzip: 'gzip', deflate: 'deflate' }, done) + }) + + it('should be false if encoding not accepted', function(done){ + var app = express(); + + app.get('/', function (req, res) { + res.send({ + bogus: req.acceptsEncoding('bogus') + }) + }) + + request(app) + .get('/') + .set('Accept-Encoding', ' gzip, deflate') + .expect(200, { bogus: false }, done) + }) + }) +}) diff --git a/tests/express-tests/test/req.acceptsEncodings.js b/tests/express-tests/test/req.acceptsEncodings.js new file mode 100644 index 00000000..f239dfea --- /dev/null +++ b/tests/express-tests/test/req.acceptsEncodings.js @@ -0,0 +1,39 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.acceptsEncodings', function () { + it('should return encoding if accepted', function (done) { + var app = express(); + + app.get('/', function (req, res) { + res.send({ + gzip: req.acceptsEncodings('gzip'), + deflate: req.acceptsEncodings('deflate') + }) + }) + + request(app) + .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.get('/', function (req, res) { + res.send({ + bogus: req.acceptsEncodings('bogus') + }) + }) + + request(app) + .get('/') + .set('Accept-Encoding', ' gzip, deflate') + .expect(200, { bogus: false }, done) + }) + }) +}) diff --git a/tests/express-tests/test/req.acceptsLanguage.js b/tests/express-tests/test/req.acceptsLanguage.js new file mode 100644 index 00000000..a3f02b13 --- /dev/null +++ b/tests/express-tests/test/req.acceptsLanguage.js @@ -0,0 +1,57 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.acceptsLanguage', function(){ + it('should return language if accepted', function (done) { + var app = express(); + + app.get('/', function (req, res) { + res.send({ + 'en-us': req.acceptsLanguage('en-us'), + en: req.acceptsLanguage('en') + }) + }) + + request(app) + .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.get('/', function (req, res) { + res.send({ + es: req.acceptsLanguage('es') + }) + }) + + request(app) + .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 language', function (done) { + var app = express(); + + app.get('/', function (req, res) { + res.send({ + en: req.acceptsLanguage('en'), + es: req.acceptsLanguage('es'), + jp: req.acceptsLanguage('jp') + }) + }) + + request(app) + .get('/') + .expect(200, { en: 'en', es: 'es', jp: 'jp' }, done) + }) + }) + }) +}) diff --git a/tests/express-tests/test/req.acceptsLanguages.js b/tests/express-tests/test/req.acceptsLanguages.js new file mode 100644 index 00000000..782a389e --- /dev/null +++ b/tests/express-tests/test/req.acceptsLanguages.js @@ -0,0 +1,57 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.acceptsLanguages', function(){ + it('should return language if accepted', function (done) { + var app = express(); + + 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, { 'en-us': 'en-us', en: 'en' }, done) + }) + + it('should be false if language not accepted', function(done){ + var app = express(); + + app.get('/', function (req, res) { + res.send({ + es: req.acceptsLanguages('es') + }) + }) + + request(app) + .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 language', function (done) { + var app = express(); + + app.get('/', function (req, res) { + res.send({ + en: req.acceptsLanguages('en'), + es: req.acceptsLanguages('es'), + jp: req.acceptsLanguages('jp') + }) + }) + + request(app) + .get('/') + .expect(200, { en: 'en', es: 'es', jp: 'jp' }, done) + }) + }) + }) +}) diff --git a/tests/express-tests/test/req.baseUrl.js b/tests/express-tests/test/req.baseUrl.js new file mode 100644 index 00000000..dbf1b030 --- /dev/null +++ b/tests/express-tests/test/req.baseUrl.js @@ -0,0 +1,88 @@ +'use strict' + +var express = require("express") +var request = require('supertest') + +describe('req', function(){ + describe('.baseUrl', function(){ + it('should be empty for top-level route', function(done){ + var app = express() + + app.get('/:a', function(req, res){ + res.end(req.baseUrl) + }) + + request(app) + .get('/foo') + .expect(200, '', done) + }) + + it('should contain lower path', function(done){ + var app = express() + var sub = express.Router() + + sub.get('/:b', function(req, res){ + res.end(req.baseUrl) + }) + app.use('/:a', sub) + + request(app) + .get('/foo/bar') + .expect(200, '/foo', done); + }) + + it('should contain full lower path', function(done){ + var app = express() + var sub1 = express.Router() + var sub2 = express.Router() + var sub3 = express.Router() + + sub3.get('/:d', function(req, res){ + res.end(req.baseUrl) + }) + sub2.use('/:c', sub3) + sub1.use('/:b', sub2) + app.use('/:a', sub1) + + request(app) + .get('/foo/bar/baz/zed') + .expect(200, '/foo/bar/baz', done); + }) + + it('should travel through routers correctly', function(done){ + var urls = [] + var app = express() + var sub1 = express.Router() + var sub2 = express.Router() + var sub3 = express.Router() + + sub3.get('/:d', function(req, res, next){ + urls.push('0@' + req.baseUrl) + next() + }) + sub2.use('/:c', sub3) + sub1.use('/', function(req, res, next){ + urls.push('1@' + req.baseUrl) + next() + }) + sub1.use('/bar', sub2) + sub1.use('/bar', function(req, res, next){ + urls.push('2@' + req.baseUrl) + next() + }) + app.use(function(req, res, next){ + urls.push('3@' + req.baseUrl) + next() + }) + app.use('/:a', sub1) + app.use(function(req, res, next){ + urls.push('4@' + req.baseUrl) + res.end(urls.join(',')) + }) + + request(app) + .get('/foo/bar/baz/zed') + .expect(200, '3@,1@/foo,0@/foo/bar/baz,2@/foo/bar,4@', done); + }) + }) +}) diff --git a/tests/express-tests/test/req.fresh.js b/tests/express-tests/test/req.fresh.js new file mode 100644 index 00000000..0af3e77e --- /dev/null +++ b/tests/express-tests/test/req.fresh.js @@ -0,0 +1,50 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.fresh', function(){ + it('should return true when the resource is not modified', function(done){ + var app = express(); + var etag = '"12345"'; + + app.use(function(req, res){ + res.set('ETag', etag); + res.send(req.fresh); + }); + + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, done); + }) + + it('should return false when the resource is modified', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('ETag', '"123"'); + res.send(req.fresh); + }); + + request(app) + .get('/') + .set('If-None-Match', '"12345"') + .expect(200, 'false', done); + }) + + it('should return false without response headers', function(done){ + var app = express(); + + app.disable('x-powered-by') + app.use(function(req, res){ + res.send(req.fresh); + }); + + request(app) + .get('/') + .expect(200, 'false', done); + }) + }) +}) diff --git a/tests/express-tests/test/req.get.js b/tests/express-tests/test/req.get.js new file mode 100644 index 00000000..20133f0e --- /dev/null +++ b/tests/express-tests/test/req.get.js @@ -0,0 +1,60 @@ +'use strict' + +var express = require("express") + , request = require('supertest') + , assert = require('assert'); + +describe('req', function(){ + describe('.get(field)', function(){ + it('should return the header field value', function(done){ + var app = express(); + + app.use(function(req, res){ + assert(req.get('Something-Else') === undefined); + res.end(req.get('Content-Type')); + }); + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .expect('application/json', done); + }) + + it('should special-case Referer', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.get('Referer')); + }); + + request(app) + .post('/') + .set('Referrer', 'http://foobar.com') + .expect('http://foobar.com', done); + }) + + it('should throw missing header name', function (done) { + var app = express() + + app.use(function (req, res) { + res.end(req.get()) + }) + + request(app) + .get('/') + .expect(500, /TypeError: name argument is required to req.get/, done) + }) + + it('should throw for non-string header name', function (done) { + var app = express() + + app.use(function (req, res) { + res.end(req.get(42)) + }) + + request(app) + .get('/') + .expect(500, /TypeError: name must be a string to req.get/, done) + }) + }) +}) diff --git a/tests/express-tests/test/req.host.js b/tests/express-tests/test/req.host.js new file mode 100644 index 00000000..5a616cfb --- /dev/null +++ b/tests/express-tests/test/req.host.js @@ -0,0 +1,156 @@ +'use strict' + +var express = require("express") + , request = require('supertest') + +describe('req', function(){ + describe('.host', function(){ + it('should return the Host when present', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .post('/') + .set('Host', 'example.com') + .expect('example.com', done); + }) + + it('should strip port number', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .post('/') + .set('Host', 'example.com:3000') + .expect('example.com', done); + }) + + it('should return undefined otherwise', function(done){ + var app = express(); + + app.use(function(req, res){ + req.headers.host = null; + res.end(String(req.host)); + }); + + request(app) + .post('/') + .expect('undefined', done); + }) + + it('should work with IPv6 Host', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .post('/') + .set('Host', '[::1]') + .expect('[::1]', done); + }) + + it('should work with IPv6 Host and port', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .post('/') + .set('Host', '[::1]:3000') + .expect('[::1]', done); + }) + + describe('when "trust proxy" is enabled', function(){ + it('should respect X-Forwarded-Host', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com') + .expect('example.com', done); + }) + + it('should ignore X-Forwarded-Host if socket addr not trusted', function(done){ + var app = express(); + + app.set('trust proxy', '10.0.0.1'); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com') + .expect('localhost', done); + }) + + it('should default to Host', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .get('/') + .set('Host', 'example.com') + .expect('example.com', done); + }) + + describe('when trusting hop count', function () { + it('should respect X-Forwarded-Host', function (done) { + var app = express(); + + app.set('trust proxy', 1); + + app.use(function (req, res) { + res.end(req.host); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com') + .expect('example.com', done); + }) + }) + }) + + describe('when "trust proxy" is disabled', function(){ + it('should ignore X-Forwarded-Host', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.host); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'evil') + .expect('localhost', done); + }) + }) + }) +}) diff --git a/tests/express-tests/test/req.hostname.js b/tests/express-tests/test/req.hostname.js new file mode 100644 index 00000000..cf6cd285 --- /dev/null +++ b/tests/express-tests/test/req.hostname.js @@ -0,0 +1,188 @@ +'use strict' + +var express = require("express") + , request = require('supertest') + +describe('req', function(){ + describe('.hostname', function(){ + it('should return the Host when present', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .post('/') + .set('Host', 'example.com') + .expect('example.com', done); + }) + + it('should strip port number', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .post('/') + .set('Host', 'example.com:3000') + .expect('example.com', done); + }) + + it('should return undefined otherwise', function(done){ + var app = express(); + + app.use(function(req, res){ + req.headers.host = null; + res.end(String(req.hostname)); + }); + + request(app) + .post('/') + .expect('undefined', done); + }) + + it('should work with IPv6 Host', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .post('/') + .set('Host', '[::1]') + .expect('[::1]', done); + }) + + it('should work with IPv6 Host and port', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .post('/') + .set('Host', '[::1]:3000') + .expect('[::1]', done); + }) + + describe('when "trust proxy" is enabled', function(){ + it('should respect X-Forwarded-Host', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com:3000') + .expect('example.com', done); + }) + + it('should ignore X-Forwarded-Host if socket addr not trusted', function(done){ + var app = express(); + + app.set('trust proxy', '10.0.0.1'); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com') + .expect('localhost', done); + }) + + it('should default to Host', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .get('/') + .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(){ + it('should ignore X-Forwarded-Host', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.hostname); + }); + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'evil') + .expect('localhost', done); + }) + }) + }) +}) diff --git a/tests/express-tests/test/req.ip.js b/tests/express-tests/test/req.ip.js new file mode 100644 index 00000000..0c9ba842 --- /dev/null +++ b/tests/express-tests/test/req.ip.js @@ -0,0 +1,113 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.ip', function(){ + describe('when X-Forwarded-For is present', function(){ + describe('when "trust proxy" is enabled', function(){ + it('should return the client addr', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res, next){ + res.send(req.ip); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .expect('client', done); + }) + + it('should return the addr after trusted proxy based on count', function (done) { + var app = express(); + + app.set('trust proxy', 2); + + app.use(function(req, res, next){ + res.send(req.ip); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .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(); + + app.set('trust proxy', 2); + app.use(sub); + + sub.use(function (req, res, next) { + res.send(req.ip); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .expect(200, 'p1', done); + }) + }) + + describe('when "trust proxy" is disabled', function(){ + it('should return the remote address', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.send(req.ip); + }); + + var test = request(app).get('/') + test.set('X-Forwarded-For', 'client, p1, p2') + test.expect(200, getExpectedClientAddress(test._server), done); + }) + }) + }) + + describe('when X-Forwarded-For is not present', function(){ + it('should return the remote address', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res, next){ + res.send(req.ip); + }); + + var test = request(app).get('/') + test.expect(200, getExpectedClientAddress(test._server), done) + }) + }) + }) +}) + +/** + * Get the local client address depending on AF_NET of server + */ + +function getExpectedClientAddress(server) { + return server.address().address === '::' + ? '::ffff:127.0.0.1' + : '127.0.0.1'; +} diff --git a/tests/express-tests/test/req.ips.js b/tests/express-tests/test/req.ips.js new file mode 100644 index 00000000..73420dd4 --- /dev/null +++ b/tests/express-tests/test/req.ips.js @@ -0,0 +1,71 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.ips', function(){ + describe('when X-Forwarded-For is present', function(){ + describe('when "trust proxy" is enabled', function(){ + it('should return an array of the specified addresses', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res, next){ + res.send(req.ips); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .expect('["client","p1","p2"]', done); + }) + + it('should stop at first untrusted', function(done){ + var app = express(); + + app.set('trust proxy', 2); + + app.use(function(req, res, next){ + res.send(req.ips); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .expect('["p1","p2"]', done); + }) + }) + + describe('when "trust proxy" is disabled', function(){ + it('should return an empty array', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.send(req.ips); + }); + + request(app) + .get('/') + .set('X-Forwarded-For', 'client, p1, p2') + .expect('[]', done); + }) + }) + }) + + describe('when X-Forwarded-For is not present', function(){ + it('should return []', function(done){ + var app = express(); + + app.use(function(req, res, next){ + res.send(req.ips); + }); + + request(app) + .get('/') + .expect('[]', done); + }) + }) + }) +}) diff --git a/tests/express-tests/test/req.is.js b/tests/express-tests/test/req.is.js new file mode 100644 index 00000000..aba3ffe0 --- /dev/null +++ b/tests/express-tests/test/req.is.js @@ -0,0 +1,169 @@ +'use strict' + +var express = require("express") +var request = require('supertest') + +describe('req.is()', function () { + describe('when given a mime type', function () { + it('should return the type when matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('application/json')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, '"application/json"', done) + }) + + it('should return false when not matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('image/jpeg')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, 'false', done) + }) + + it('should ignore charset', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('application/json')) + }) + + request(app) + .post('/') + .type('application/json; charset=UTF-8') + .send('{}') + .expect(200, '"application/json"', done) + }) + }) + + describe('when content-type is not present', function(){ + it('should return false', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('application/json')) + }) + + request(app) + .post('/') + .send('{}') + .expect(200, 'false', done) + }) + }) + + describe('when given an extension', function(){ + it('should lookup the mime type', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('json')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, '"json"', done) + }) + }) + + describe('when given */subtype', function(){ + it('should return the full type when matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('*/json')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, '"application/json"', done) + }) + + it('should return false when not matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('*/html')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, 'false', done) + }) + + it('should ignore charset', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('*/json')) + }) + + request(app) + .post('/') + .type('application/json; charset=UTF-8') + .send('{}') + .expect(200, '"application/json"', done) + }) + }) + + describe('when given type/*', function(){ + it('should return the full type when matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('application/*')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, '"application/json"', done) + }) + + it('should return false when not matching', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('text/*')) + }) + + request(app) + .post('/') + .type('application/json') + .send('{}') + .expect(200, 'false', done) + }) + + it('should ignore charset', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.is('application/*')) + }) + + request(app) + .post('/') + .type('application/json; charset=UTF-8') + .send('{}') + .expect(200, '"application/json"', done) + }) + }) +}) diff --git a/tests/express-tests/test/req.param.js b/tests/express-tests/test/req.param.js new file mode 100644 index 00000000..cc2bcec9 --- /dev/null +++ b/tests/express-tests/test/req.param.js @@ -0,0 +1,61 @@ +'use strict' + +var express = require("express") + , request = require('supertest') + +describe('req', function(){ + describe('.param(name, default)', function(){ + it('should use the default value unless defined', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.param('name', 'tj')); + }); + + request(app) + .get('/') + .expect('tj', done); + }) + }) + + describe('.param(name)', function(){ + it('should check req.query', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.param('name')); + }); + + request(app) + .get('/?name=tj') + .expect('tj', done); + }) + + it('should check req.body', function(done){ + var app = express(); + + app.use(express.json()) + + app.use(function(req, res){ + res.end(req.param('name')); + }); + + request(app) + .post('/') + .send({ name: 'tj' }) + .expect('tj', done); + }) + + it('should check req.params', function(done){ + var app = express(); + + app.get('/user/:name', function(req, res){ + res.end(req.param('filter') + req.param('name')); + }); + + request(app) + .get('/user/tj') + .expect('undefinedtj', done); + }) + }) +}) diff --git a/tests/express-tests/test/req.path.js b/tests/express-tests/test/req.path.js new file mode 100644 index 00000000..30335957 --- /dev/null +++ b/tests/express-tests/test/req.path.js @@ -0,0 +1,20 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.path', function(){ + it('should return the parsed pathname', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.path); + }); + + request(app) + .get('/login?redirect=/post/1/comments') + .expect('/login', done); + }) + }) +}) diff --git a/tests/express-tests/test/req.protocol.js b/tests/express-tests/test/req.protocol.js new file mode 100644 index 00000000..81f02c54 --- /dev/null +++ b/tests/express-tests/test/req.protocol.js @@ -0,0 +1,113 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.protocol', function(){ + it('should return the protocol string', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.protocol); + }); + + request(app) + .get('/') + .expect('http', done); + }) + + describe('when "trust proxy" is enabled', function(){ + it('should respect X-Forwarded-Proto', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.protocol); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('https', done); + }) + + it('should default to the socket addr if X-Forwarded-Proto not present', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + req.connection.encrypted = true; + res.end(req.protocol); + }); + + request(app) + .get('/') + .expect('https', done); + }) + + it('should ignore X-Forwarded-Proto if socket addr not trusted', function(done){ + var app = express(); + + app.set('trust proxy', '10.0.0.1'); + + app.use(function(req, res){ + res.end(req.protocol); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('http', done); + }) + + it('should default to http', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.use(function(req, res){ + res.end(req.protocol); + }); + + request(app) + .get('/') + .expect('http', done); + }) + + describe('when trusting hop count', function () { + it('should respect X-Forwarded-Proto', function (done) { + var app = express(); + + app.set('trust proxy', 1); + + app.use(function (req, res) { + res.end(req.protocol); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('https', done); + }) + }) + }) + + describe('when "trust proxy" is disabled', function(){ + it('should ignore X-Forwarded-Proto', function(done){ + var app = express(); + + app.use(function(req, res){ + res.end(req.protocol); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('http', done); + }) + }) + }) +}) diff --git a/tests/express-tests/test/req.query.js b/tests/express-tests/test/req.query.js new file mode 100644 index 00000000..d48292fe --- /dev/null +++ b/tests/express-tests/test/req.query.js @@ -0,0 +1,123 @@ +'use strict' + +var assert = require('assert') +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.query', function(){ + it('should default to {}', function(done){ + var app = createApp(); + + request(app) + .get('/') + .expect(200, '{}', done); + }); + + it('should default to parse complex keys', function (done) { + var app = createApp(); + + request(app) + .get('/?user[name]=tj') + .expect(200, '{"user":{"name":"tj"}}', done); + }); + + describe('when "query parser" is extended', function () { + it('should parse complex keys', function (done) { + var app = createApp('extended'); + + request(app) + .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) { + var app = createApp('extended'); + + request(app) + .get('/?user.name=tj') + .expect(200, '{"user.name":"tj"}', done); + }); + }); + + describe('when "query parser" is simple', function () { + it('should not parse complex keys', function (done) { + var app = createApp('simple'); + + request(app) + .get('/?user%5Bname%5D=tj') + .expect(200, '{"user[name]":"tj"}', done); + }); + }); + + describe('when "query parser" is a function', function () { + it('should parse using function', function (done) { + var app = createApp(function (str) { + return {'length': (str || '').length}; + }); + + request(app) + .get('/?user%5Bname%5D=tj') + .expect(200, '{"length":17}', done); + }); + }); + + describe('when "query parser" disabled', function () { + it('should not parse query', function (done) { + var app = createApp(false); + + request(app) + .get('/?user%5Bname%5D=tj') + .expect(200, '{}', done); + }); + }); + + describe('when "query parser" enabled', function () { + it('should not parse complex keys', function (done) { + var app = createApp(true); + + request(app) + .get('/?user%5Bname%5D=tj') + .expect(200, '{"user[name]":"tj"}', done); + }); + }); + + describe('when "query parser fn" is missing', function () { + it('should act like "extended"', function (done) { + var app = express(); + + delete app.settings['query parser']; + delete app.settings['query parser fn']; + + app.use(function (req, res) { + res.send(req.query); + }); + + request(app) + .get('/?user[name]=tj&user.name=tj') + .expect(200, '{"user":{"name":"tj"},"user.name":"tj"}', done); + }); + }); + + describe('when "query parser" an unknown value', function () { + it('should throw', function () { + assert.throws(createApp.bind(null, 'bogus'), + /unknown value.*query parser/) + }); + }); + }) +}) + +function createApp(setting) { + var app = express(); + + if (setting !== undefined) { + app.set('query parser', setting); + } + + app.use(function (req, res) { + res.send(req.query); + }); + + return app; +} diff --git a/tests/express-tests/test/req.range.js b/tests/express-tests/test/req.range.js new file mode 100644 index 00000000..2c76b742 --- /dev/null +++ b/tests/express-tests/test/req.range.js @@ -0,0 +1,104 @@ +'use strict' + +var express = require("express"); +var request = require('supertest') + +describe('req', function(){ + describe('.range(size)', function(){ + it('should return parsed ranges', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(120)) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-50,51-100') + .expect(200, '[{"start":0,"end":50},{"start":51,"end":100}]', done) + }) + + it('should cap to the given size', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(75)) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-100') + .expect(200, '[{"start":0,"end":74}]', done) + }) + + it('should cap to the given size when open-ended', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(75)) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-') + .expect(200, '[{"start":0,"end":74}]', done) + }) + + it('should have a .type', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(120).type) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-100') + .expect(200, '"bytes"', done) + }) + + it('should accept any type', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(120).type) + }) + + request(app) + .get('/') + .set('Range', 'users=0-2') + .expect(200, '"users"', done) + }) + + it('should return undefined if no range', function (done) { + var app = express() + + app.use(function (req, res) { + res.send(String(req.range(120))) + }) + + request(app) + .get('/') + .expect(200, 'undefined', done) + }) + }) + + describe('.range(size, options)', function(){ + describe('with "combine: true" option', function(){ + it('should return combined ranges', function (done) { + var app = express() + + app.use(function (req, res) { + res.json(req.range(120, { + combine: true + })) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-50,51-100') + .expect(200, '[{"start":0,"end":100}]', done) + }) + }) + }) +}) diff --git a/tests/express-tests/test/req.route.js b/tests/express-tests/test/req.route.js new file mode 100644 index 00000000..4f75276e --- /dev/null +++ b/tests/express-tests/test/req.route.js @@ -0,0 +1,28 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.route', function(){ + it('should be the executed Route', function(done){ + var app = express(); + + app.get('/user/:id/:op?', function(req, res, next){ + res.header('path-1', req.route.path) + next(); + }); + + app.get('/user/:id/edit', function(req, res){ + res.header('path-2', req.route.path) + res.end(); + }); + + request(app) + .get('/user/12/edit') + .expect('path-1', '/user/:id/:op?') + .expect('path-2', '/user/:id/edit') + .expect(200, done) + }) + }) +}) diff --git a/tests/express-tests/test/req.secure.js b/tests/express-tests/test/req.secure.js new file mode 100644 index 00000000..23356ba3 --- /dev/null +++ b/tests/express-tests/test/req.secure.js @@ -0,0 +1,101 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.secure', function(){ + describe('when X-Forwarded-Proto is missing', function(){ + it('should return false when http', function(done){ + var app = express(); + + app.get('/', function(req, res){ + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .expect('no', done) + }) + }) + }) + + describe('.secure', function(){ + describe('when X-Forwarded-Proto is present', function(){ + it('should return false when http', function(done){ + var app = express(); + + app.get('/', function(req, res){ + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('no', done) + }) + + it('should return true when "trust proxy" is enabled', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.get('/', function(req, res){ + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('yes', done) + }) + + it('should return false when initial proxy is http', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.get('/', function(req, res){ + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'http, https') + .expect('no', done) + }) + + it('should return true when initial proxy is https', function(done){ + var app = express(); + + app.enable('trust proxy'); + + app.get('/', function(req, res){ + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https, http') + .expect('yes', done) + }) + + describe('when "trust proxy" trusting hop count', function () { + it('should respect X-Forwarded-Proto', function (done) { + var app = express(); + + app.set('trust proxy', 1); + + app.get('/', function (req, res) { + res.send(req.secure ? 'yes' : 'no'); + }); + + request(app) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect('yes', done) + }) + }) + }) + }) +}) diff --git a/tests/express-tests/test/req.signedCookies.js b/tests/express-tests/test/req.signedCookies.js new file mode 100644 index 00000000..d24c17ff --- /dev/null +++ b/tests/express-tests/test/req.signedCookies.js @@ -0,0 +1,37 @@ +'use strict' + +var express = require("express") + , request = require('supertest') + , cookieParser = require('cookie-parser') + +describe('req', function(){ + describe('.signedCookies', function(){ + it('should return a signed JSON cookie', function(done){ + var app = express(); + + app.use(cookieParser('secret')); + + app.use(function(req, res){ + if (req.path === '/set') { + res.cookie('obj', { foo: 'bar' }, { signed: true }); + res.end(); + } else { + res.send(req.signedCookies); + } + }); + + request(app) + .get('/set') + .end(function(err, res){ + if (err) return done(err); + var cookie = res.header['set-cookie']; + + request(app) + .get('/') + .set('Cookie', cookie) + .expect(200, { obj: { foo: 'bar' } }, done) + }); + }) + }) +}) + diff --git a/tests/express-tests/test/req.stale.js b/tests/express-tests/test/req.stale.js new file mode 100644 index 00000000..58679bd6 --- /dev/null +++ b/tests/express-tests/test/req.stale.js @@ -0,0 +1,50 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.stale', function(){ + it('should return false when the resource is not modified', function(done){ + var app = express(); + var etag = '"12345"'; + + app.use(function(req, res){ + res.set('ETag', etag); + res.send(req.stale); + }); + + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, done); + }) + + it('should return true when the resource is modified', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('ETag', '"123"'); + res.send(req.stale); + }); + + request(app) + .get('/') + .set('If-None-Match', '"12345"') + .expect(200, 'true', done); + }) + + it('should return true without response headers', function(done){ + var app = express(); + + app.disable('x-powered-by') + app.use(function(req, res){ + res.send(req.stale); + }); + + request(app) + .get('/') + .expect(200, 'true', done); + }) + }) +}) diff --git a/tests/express-tests/test/req.subdomains.js b/tests/express-tests/test/req.subdomains.js new file mode 100644 index 00000000..4897d835 --- /dev/null +++ b/tests/express-tests/test/req.subdomains.js @@ -0,0 +1,173 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.subdomains', function(){ + describe('when present', function(){ + it('should return an array', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', 'tobi.ferrets.example.com') + .expect(200, ['ferrets', 'tobi'], done); + }) + + it('should work with IPv4 address', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', '127.0.0.1') + .expect(200, [], done); + }) + + it('should work with IPv6 address', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', '[::1]') + .expect(200, [], done); + }) + }) + + describe('otherwise', function(){ + it('should return an empty array', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', 'example.com') + .expect(200, [], done); + }) + }) + + describe('with no host', function(){ + it('should return an empty array', function(done){ + var app = express(); + + app.use(function(req, res){ + req.headers.host = null; + res.send(req.subdomains); + }); + + request(app) + .get('/') + .expect(200, [], done); + }) + }) + + describe('with trusted X-Forwarded-Host', function () { + it('should return an array', function (done) { + var app = express(); + + app.set('trust proxy', true); + app.use(function (req, res) { + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('X-Forwarded-Host', 'tobi.ferrets.example.com') + .expect(200, ['ferrets', 'tobi'], done); + }) + }) + + describe('when subdomain offset is set', function(){ + describe('when subdomain offset is zero', function(){ + it('should return an array with the whole domain', function(done){ + var app = express(); + app.set('subdomain offset', 0); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', 'tobi.ferrets.sub.example.com') + .expect(200, ['com', 'example', 'sub', 'ferrets', 'tobi'], done); + }) + + it('should return an array with the whole IPv4', function (done) { + var app = express(); + app.set('subdomain offset', 0); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', '127.0.0.1') + .expect(200, ['127.0.0.1'], done); + }) + + it('should return an array with the whole IPv6', function (done) { + var app = express(); + app.set('subdomain offset', 0); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', '[::1]') + .expect(200, ['[::1]'], done); + }) + }) + + describe('when present', function(){ + it('should return an array', function(done){ + var app = express(); + app.set('subdomain offset', 3); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', 'tobi.ferrets.sub.example.com') + .expect(200, ['ferrets', 'tobi'], done); + }) + }) + + describe('otherwise', function(){ + it('should return an empty array', function(done){ + var app = express(); + app.set('subdomain offset', 3); + + app.use(function(req, res){ + res.send(req.subdomains); + }); + + request(app) + .get('/') + .set('Host', 'sub.example.com') + .expect(200, [], done); + }) + }) + }) + }) +}) diff --git a/tests/express-tests/test/req.xhr.js b/tests/express-tests/test/req.xhr.js new file mode 100644 index 00000000..c2097427 --- /dev/null +++ b/tests/express-tests/test/req.xhr.js @@ -0,0 +1,42 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('req', function(){ + describe('.xhr', function(){ + before(function () { + this.app = express() + this.app.get('/', function (req, res) { + res.send(req.xhr) + }) + }) + + 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) + }) + + 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){ + request(this.app) + .get('/') + .set('X-Requested-With', 'blahblah') + .expect(200, 'false', done) + }) + + it('should return false when not present', function(done){ + request(this.app) + .get('/') + .expect(200, 'false', done) + }) + }) +}) diff --git a/tests/express-tests/test/res.append.js b/tests/express-tests/test/res.append.js new file mode 100644 index 00000000..e36f441d --- /dev/null +++ b/tests/express-tests/test/res.append.js @@ -0,0 +1,116 @@ +'use strict' + +var assert = require('assert') +var express = require("express") +var request = require('supertest') + +describe('res', function () { + describe('.append(field, val)', function () { + it('should append multiple headers', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.append('Set-Cookie', 'foo=bar') + next() + }) + + app.use(function (req, res) { + res.append('Set-Cookie', 'fizz=buzz') + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar', 'fizz=buzz'])) + .end(done) + }) + + it('should accept array of values', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.append('Set-Cookie', ['foo=bar', 'fizz=buzz']) + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar', 'fizz=buzz'])) + .end(done) + }) + + it('should get reset by res.set(field, val)', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.append('Set-Cookie', 'foo=bar') + res.append('Set-Cookie', 'fizz=buzz') + next() + }) + + app.use(function (req, res) { + res.set('Set-Cookie', 'pet=tobi') + res.end() + }); + + request(app) + .get('/') + .expect(200) + .expect(shouldHaveHeaderValues('Set-Cookie', ['pet=tobi'])) + .end(done) + }) + + it('should work with res.set(field, val) first', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.set('Set-Cookie', 'foo=bar') + next() + }) + + app.use(function(req, res){ + res.append('Set-Cookie', 'fizz=buzz') + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar', 'fizz=buzz'])) + .end(done) + }) + + it('should work together with res.cookie', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.cookie('foo', 'bar') + next() + }) + + app.use(function (req, res) { + res.append('Set-Cookie', 'fizz=buzz') + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar; Path=/', 'fizz=buzz'])) + .end(done) + }) + }) +}) + +function shouldHaveHeaderValues (key, values) { + return function (res) { + var headers = res.headers[key.toLowerCase()] + assert.ok(headers, 'should have header "' + key + '"') + assert.strictEqual(headers.length, values.length, 'should have ' + values.length + ' occurances of "' + key + '"') + for (var i = 0; i < values.length; i++) { + assert.strictEqual(headers[i], values[i]) + } + } +} diff --git a/tests/express-tests/test/res.attachment.js b/tests/express-tests/test/res.attachment.js new file mode 100644 index 00000000..aff44959 --- /dev/null +++ b/tests/express-tests/test/res.attachment.js @@ -0,0 +1,78 @@ +'use strict' + +var Buffer = require('safe-buffer').Buffer +var express = require("express") + , request = require('supertest'); + +describe('res', function(){ + describe('.attachment()', function(){ + it('should Content-Disposition to attachment', function(done){ + var app = express(); + + app.use(function(req, res){ + res.attachment().send('foo'); + }); + + request(app) + .get('/') + .expect('Content-Disposition', 'attachment', done); + }) + }) + + describe('.attachment(filename)', function(){ + it('should add the filename param', function(done){ + var app = express(); + + app.use(function(req, res){ + res.attachment('/path/to/image.png'); + res.send('foo'); + }); + + request(app) + .get('/') + .expect('Content-Disposition', 'attachment; filename="image.png"', done); + }) + + it('should set the Content-Type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.attachment('/path/to/image.png'); + res.send(Buffer.alloc(4, '.')) + }); + + request(app) + .get('/') + .expect('Content-Type', 'image/png', done); + }) + }) + + describe('.attachment(utf8filename)', function(){ + it('should add the filename and filename* params', function(done){ + var app = express(); + + app.use(function(req, res){ + res.attachment('/locales/日本語.txt'); + res.send('japanese'); + }); + + request(app) + .get('/') + .expect('Content-Disposition', 'attachment; filename="???.txt"; filename*=UTF-8\'\'%E6%97%A5%E6%9C%AC%E8%AA%9E.txt') + .expect(200, done); + }) + + it('should set the Content-Type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.attachment('/locales/日本語.txt'); + res.send('japanese'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8', done); + }) + }) +}) diff --git a/tests/express-tests/test/res.clearCookie.js b/tests/express-tests/test/res.clearCookie.js new file mode 100644 index 00000000..a3ef077e --- /dev/null +++ b/tests/express-tests/test/res.clearCookie.js @@ -0,0 +1,68 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('res', function(){ + describe('.clearCookie(name)', function(){ + it('should set a cookie passed expiry', function(done){ + var app = express(); + + app.use(function(req, res){ + res.clearCookie('sid').end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'sid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT') + .expect(200, done) + }) + }) + + describe('.clearCookie(name, options)', function(){ + it('should set the given params', function(done){ + var app = express(); + + app.use(function(req, res){ + res.clearCookie('sid', { path: '/admin' }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'sid=; Path=/admin; Expires=Thu, 01 Jan 1970 00:00:00 GMT') + .expect(200, done) + }) + + it('should set expires when passed', function(done) { + var expiresAt = new Date() + var app = express(); + + app.use(function(req, res){ + res.clearCookie('sid', { expires: expiresAt }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'sid=; Path=/; Expires=' + expiresAt.toUTCString() ) + .expect(200, done) + }) + + it('should set both maxAge and expires when passed', function(done) { + var maxAgeInMs = 10000 + var expiresAt = new Date() + var expectedExpires = new Date(expiresAt.getTime() + maxAgeInMs) + var app = express(); + + app.use(function(req, res){ + res.clearCookie('sid', { expires: expiresAt, maxAge: maxAgeInMs }).end(); + }); + + request(app) + .get('/') + // yes, this is the behavior. When we set a max-age, we also set expires to a date 10 sec ahead of expires + // even if we set max-age only, we will also set an expires 10 sec in the future + .expect('Set-Cookie', 'sid=; Max-Age=10; Path=/; Expires=' + expectedExpires.toUTCString()) + .expect(200, done) + }) + }) +}) diff --git a/tests/express-tests/test/res.cookie.js b/tests/express-tests/test/res.cookie.js new file mode 100644 index 00000000..1c901f0a --- /dev/null +++ b/tests/express-tests/test/res.cookie.js @@ -0,0 +1,296 @@ +'use strict' + +var express = require("express") + , request = require('supertest') + , cookieParser = require('cookie-parser') +var merge = require('utils-merge'); + +describe('res', function(){ + describe('.cookie(name, object)', function(){ + it('should generate a JSON cookie', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('user', { name: 'tobi' }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'user=j%3A%7B%22name%22%3A%22tobi%22%7D; Path=/') + .expect(200, done) + }) + }) + + describe('.cookie(name, string)', function(){ + it('should set a cookie', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('name', 'tobi').end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'name=tobi; Path=/') + .expect(200, done) + }) + + it('should allow multiple calls', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('name', 'tobi'); + res.cookie('age', 1); + res.cookie('gender', '?'); + res.end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'name=tobi; Path=/,age=1; Path=/,gender=%3F; Path=/') + .expect(200, done) + }) + }) + + describe('.cookie(name, string, options)', function(){ + it('should set params', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('name', 'tobi', { httpOnly: true, secure: true }); + res.end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'name=tobi; Path=/; HttpOnly; Secure') + .expect(200, done) + }) + + describe('expires', function () { + it('should throw on invalid date', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { expires: new Date(NaN) }) + res.end() + }) + + request(app) + .get('/') + .expect(500, /option expires is invalid/, done) + }) + }) + + describe('partitioned', function () { + it('should set partitioned', function (done) { + var app = express(); + + app.use(function (req, res) { + res.cookie('name', 'tobi', { partitioned: true }); + res.end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'name=tobi; Path=/; Partitioned') + .expect(200, done) + }) + }) + + describe('maxAge', function(){ + it('should set relative expires', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('name', 'tobi', { maxAge: 1000 }); + res.end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', /name=tobi; Max-Age=1; Path=\/; Expires=/) + .expect(200, done) + }) + + it('should set max-age', function(done){ + var app = express(); + + app.use(function(req, res){ + res.cookie('name', 'tobi', { maxAge: 1000 }); + res.end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', /Max-Age=1/, done) + }) + + it('should not mutate the options object', function(done){ + var app = express(); + + var options = { maxAge: 1000 }; + var optionsCopy = merge({}, options); + + app.use(function(req, res){ + res.cookie('name', 'tobi', options) + res.json(options) + }); + + request(app) + .get('/') + .expect(200, optionsCopy, done) + }) + + it('should not throw on null', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { maxAge: null }) + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect('Set-Cookie', 'name=tobi; Path=/') + .end(done) + }) + + it('should not throw on undefined', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { maxAge: undefined }) + res.end() + }) + + request(app) + .get('/') + .expect(200) + .expect('Set-Cookie', 'name=tobi; Path=/') + .end(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) + }) + }) + + describe('priority', function () { + it('should set low priority', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { priority: 'low' }) + res.end() + }) + + request(app) + .get('/') + .expect('Set-Cookie', /Priority=Low/) + .expect(200, done) + }) + + it('should set medium priority', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { priority: 'medium' }) + res.end() + }) + + request(app) + .get('/') + .expect('Set-Cookie', /Priority=Medium/) + .expect(200, done) + }) + + it('should set high priority', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { priority: 'high' }) + res.end() + }) + + request(app) + .get('/') + .expect('Set-Cookie', /Priority=High/) + .expect(200, done) + }) + + it('should throw with invalid priority', function (done) { + var app = express() + + app.use(function (req, res) { + res.cookie('name', 'tobi', { priority: 'foobar' }) + res.end() + }) + + request(app) + .get('/') + .expect(500, /option priority is invalid/, done) + }) + }) + + describe('signed', function(){ + it('should generate a signed JSON cookie', function(done){ + var app = express(); + + app.use(cookieParser('foo bar baz')); + + app.use(function(req, res){ + res.cookie('user', { name: 'tobi' }, { signed: true }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'user=s%3Aj%3A%7B%22name%22%3A%22tobi%22%7D.K20xcwmDS%2BPb1rsD95o5Jm5SqWs1KteqdnynnB7jkTE; Path=/') + .expect(200, done) + }) + }) + + describe('signed without secret', function(){ + it('should throw an error', function(done){ + var app = express(); + + app.use(cookieParser()); + + app.use(function(req, res){ + res.cookie('name', 'tobi', { signed: true }).end(); + }); + + request(app) + .get('/') + .expect(500, /secret\S+ required for signed cookies/, done); + }) + }) + + describe('.signedCookie(name, string)', function(){ + it('should set a signed cookie', function(done){ + var app = express(); + + app.use(cookieParser('foo bar baz')); + + app.use(function(req, res){ + res.cookie('name', 'tobi', { signed: true }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'name=s%3Atobi.xJjV2iZ6EI7C8E5kzwbfA9PVLl1ZR07UTnuTgQQ4EnQ; Path=/') + .expect(200, done) + }) + }) + }) +}) diff --git a/tests/express-tests/test/res.download.js b/tests/express-tests/test/res.download.js new file mode 100644 index 00000000..a10a0296 --- /dev/null +++ b/tests/express-tests/test/res.download.js @@ -0,0 +1,498 @@ +'use strict' + +var after = require('after'); +var assert = require('assert') +var asyncHooks = tryRequire('async_hooks') +var Buffer = require('safe-buffer').Buffer +var express = require("express"); +var path = require('path') +var request = require('supertest'); +var utils = require('./support/utils') + +var FIXTURES_PATH = path.join(__dirname, 'fixtures') + +var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' + ? describe + : describe.skip + +describe('res', function(){ + describe('.download(path)', function(){ + it('should transfer as an attachment', function(done){ + var app = express(); + + app.use(function(req, res){ + res.download('tests/express-tests/test/fixtures/user.html'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=UTF-8') + .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('tests/express-tests/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('tests/express-tests/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(){ + it('should provide an alternate filename', function(done){ + var app = express(); + + app.use(function(req, res){ + res.download('tests/express-tests/test/fixtures/user.html', 'document'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=UTF-8') + .expect('Content-Disposition', 'attachment; filename="document"') + .expect(200, done) + }) + }) + + describe('.download(path, fn)', function(){ + it('should invoke the callback', function(done){ + var app = express(); + var cb = after(2, done); + + app.use(function(req, res){ + res.download('tests/express-tests/test/fixtures/user.html', cb); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=UTF-8') + .expect('Content-Disposition', 'attachment; filename="user.html"') + .expect(200, cb); + }) + + describeAsyncHooks('async local storage', function () { + it('should presist store', function (done) { + var app = express() + var cb = after(2, done) + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(function (req, res) { + res.download('tests/express-tests/test/fixtures/name.txt', function (err) { + if (err) return cb(err) + + var local = req.asyncLocalStorage.getStore() + + assert.strictEqual(local.foo, 'bar') + cb() + }) + }) + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect('Content-Disposition', 'attachment; filename="name.txt"') + .expect(200, 'tobi', cb) + }) + + it('should presist store on error', function (done) { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(function (req, res) { + res.download('tests/express-tests/test/fixtures/does-not-exist', function (err) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.send(err ? 'got ' + err.status + ' error' : 'no error') + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('got 404 error') + .end(done) + }) + }) + }) + + describe('.download(path, options)', function () { + it('should allow options to res.sendFile()', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('tests/express-tests/test/fixtures/.name', { + dotfiles: 'allow', + maxAge: '4h' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename=".name"') + .expect('Cache-Control', 'public, max-age=14400') + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + + describe('with "headers" option', function () { + it('should set headers on response', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('tests/express-tests/test/fixtures/user.html', { + headers: { + 'X-Foo': 'Bar', + 'X-Bar': 'Foo' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('X-Foo', 'Bar') + .expect('X-Bar', 'Foo') + .end(done) + }) + + it('should use last header when duplicated', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('tests/express-tests/test/fixtures/user.html', { + headers: { + 'X-Foo': 'Bar', + 'x-foo': 'bar' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('X-Foo', 'bar') + .end(done) + }) + + it('should override Content-Type', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('tests/express-tests/test/fixtures/user.html', { + headers: { + 'Content-Type': 'text/x-custom' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'text/x-custom') + .end(done) + }) + + it('should not set headers on 404', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('tests/express-tests/test/fixtures/does-not-exist', { + headers: { + 'X-Foo': 'Bar' + } + }) + }) + + request(app) + .get('/') + .expect(404) + .expect(utils.shouldNotHaveHeader('X-Foo')) + .end(done) + }) + + describe('when headers contains Content-Disposition', function () { + it('should be ignored', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('tests/express-tests/test/fixtures/user.html', { + headers: { + 'Content-Disposition': 'inline' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename="user.html"') + .end(done) + }) + + it('should be ignored case-insensitively', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('tests/express-tests/test/fixtures/user.html', { + headers: { + 'content-disposition': 'inline' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename="user.html"') + .end(done) + }) + }) + }) + + describe('with "root" option', function () { + it('should allow relative path', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('name.txt', { + root: FIXTURES_PATH + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename="name.txt"') + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + + it('should allow up within root', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('fake/../name.txt', { + root: FIXTURES_PATH + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Disposition', 'attachment; filename="name.txt"') + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + + it('should reject up outside root', function (done) { + var app = express() + + app.use(function (req, res) { + var p = '..' + path.sep + + path.relative(path.dirname(FIXTURES_PATH), path.join(FIXTURES_PATH, 'name.txt')) + + res.download(p, { + root: FIXTURES_PATH + }) + }) + + request(app) + .get('/') + .expect(403) + .expect(utils.shouldNotHaveHeader('Content-Disposition')) + .end(done) + }) + + it('should reject reading outside root', function (done) { + var app = express() + + app.use(function (req, res) { + res.download('../name.txt', { + root: FIXTURES_PATH + }) + }) + + request(app) + .get('/') + .expect(403) + .expect(utils.shouldNotHaveHeader('Content-Disposition')) + .end(done) + }) + }) + }) + + describe('.download(path, filename, fn)', function(){ + it('should invoke the callback', function(done){ + var app = express(); + var cb = after(2, done); + + app.use(function(req, res){ + res.download('tests/express-tests/test/fixtures/user.html', 'document', cb) + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=UTF-8') + .expect('Content-Disposition', 'attachment; filename="document"') + .expect(200, cb); + }) + }) + + 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('tests/express-tests/test/fixtures/user.html', 'document', options, cb) + }) + + 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('tests/express-tests/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(utils.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('tests/express-tests/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('tests/express-tests/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(); + + app.use(function (req, res, next) { + res.download('tests/express-tests/test/fixtures/foobar.html', function(err){ + if (!err) return next(new Error('expected error')); + res.send('got ' + err.status + ' ' + err.code); + }); + }); + + request(app) + .get('/') + .expect(200, 'got 404 ENOENT', done); + }) + + it('should remove Content-Disposition', function(done){ + var app = express() + + app.use(function (req, res, next) { + res.download('tests/express-tests/test/fixtures/foobar.html', function(err){ + if (!err) return next(new Error('expected error')); + res.end('failed'); + }); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('Content-Disposition')) + .expect(200, 'failed', done) + }) + }) +}) + +function tryRequire (name) { + try { + return require(name) + } catch (e) { + return {} + } +} diff --git a/tests/express-tests/test/res.format.js b/tests/express-tests/test/res.format.js new file mode 100644 index 00000000..6c1cbfb0 --- /dev/null +++ b/tests/express-tests/test/res.format.js @@ -0,0 +1,243 @@ +'use strict' + +var after = require('after') +var express = require("express") + , request = require('supertest') + , assert = require('assert'); + +var app1 = express(); + +app1.use(function(req, res, next){ + res.format({ + 'text/plain': function(){ + res.send('hey'); + }, + + 'text/html': function(){ + res.send('

      hey

      '); + }, + + 'application/json': function(a, b, c){ + assert(req === a) + assert(res === b) + assert(next === c) + res.send({ message: 'hey' }); + } + }); +}); + +app1.use(function(err, req, res, next){ + if (!err.types) throw err; + res.send(err.status, 'Supports: ' + err.types.join(', ')); +}) + +var app2 = express(); + +app2.use(function(req, res, next){ + res.format({ + text: function(){ res.send('hey') }, + html: function(){ res.send('

      hey

      ') }, + json: function(){ res.send({ message: 'hey' }) } + }); +}); + +app2.use(function(err, req, res, next){ + res.send(err.status, 'Supports: ' + err.types.join(', ')); +}) + +var app3 = express(); + +app3.use(function(req, res, next){ + res.format({ + text: function(){ res.send('hey') }, + default: function (a, b, c) { + assert(req === a) + assert(res === b) + assert(next === c) + res.send('default') + } + }) +}); + +var app4 = express(); + +app4.get('/', function (req, res) { + res.format({ + text: function(){ res.send('hey') }, + html: function(){ res.send('

      hey

      ') }, + json: function(){ res.send({ message: 'hey' }) } + }); +}); + +app4.use(function(err, req, res, next){ + res.send(err.status, 'Supports: ' + err.types.join(', ')); +}) + +var app5 = express(); + +app5.use(function (req, res, next) { + res.format({ + default: function () { res.send('hey') } + }); +}); + +describe('res', function(){ + describe('.format(obj)', function(){ + describe('with canonicalized mime types', function(){ + test(app1); + }) + + describe('with extnames', function(){ + test(app2); + }) + + describe('with parameters', function(){ + var app = express(); + + app.use(function(req, res, next){ + res.format({ + 'text/plain; charset=utf-8': function(){ res.send('hey') }, + 'text/html; foo=bar; bar=baz': function(){ res.send('

      hey

      ') }, + 'application/json; q=0.5': function(){ res.send({ message: 'hey' }) } + }); + }); + + app.use(function(err, req, res, next){ + res.send(err.status, 'Supports: ' + err.types.join(', ')); + }); + + test(app); + }) + + describe('given .default', function(){ + it('should be invoked instead of auto-responding', function(done){ + request(app3) + .get('/') + .set('Accept', 'text/html') + .expect('default', done); + }) + + it('should work when only .default is provided', function (done) { + request(app5) + .get('/') + .set('Accept', '*/*') + .expect('hey', done); + }) + + it('should be able to invoke other formatter', function (done) { + var app = express() + + app.use(function (req, res, next) { + res.format({ + json: function () { res.send('json') }, + default: function () { + res.header('x-default', '1') + this.json() + } + }) + }) + + request(app) + .get('/') + .set('Accept', 'text/plain') + .expect(200) + .expect('x-default', '1') + .expect('json') + .end(done) + }) + }) + + describe('in router', function(){ + test(app4); + }) + + describe('in router', function(){ + var app = express(); + var router = express.Router(); + + router.get('/', function (req, res) { + res.format({ + text: function(){ res.send('hey') }, + html: function(){ res.send('

      hey

      ') }, + json: function(){ res.send({ message: 'hey' }) } + }); + }); + + router.use(function(err, req, res, next){ + res.send(err.status, 'Supports: ' + err.types.join(', ')); + }) + + app.use(router) + + test(app) + }) + }) +}) + +function test(app) { + it('should utilize qvalues in negotiation', function(done){ + request(app) + .get('/') + .set('Accept', 'text/html; q=.5, application/json, */*; q=.1') + .expect({"message":"hey"}, done); + }) + + it('should allow wildcard type/subtypes', function(done){ + request(app) + .get('/') + .set('Accept', 'text/html; q=.5, application/*, */*; q=.1') + .expect({"message":"hey"}, done); + }) + + it('should default the Content-Type', function(done){ + request(app) + .get('/') + .set('Accept', 'text/html; q=.5, text/plain') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('hey', done); + }) + + 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', cb) + + request(app) + .get('/') + .set('Accept', 'text/plain') + .expect('Content-Type', 'text/plain; charset=utf-8', cb) + + request(app) + .get('/') + .set('Accept', 'application/json') + .expect('Content-Type', 'application/json; charset=utf-8', cb) + }) + + it('should Vary: Accept', function(done){ + request(app) + .get('/') + .set('Accept', 'text/html; q=.5, text/plain') + .expect('Vary', 'Accept', done); + }) + + describe('when Accept is not present', function(){ + it('should invoke the first callback', function(done){ + request(app) + .get('/') + .expect('hey', done); + }) + }) + + describe('when no match is made', function(){ + it('should should respond with 406 not acceptable', function(done){ + request(app) + .get('/') + .set('Accept', 'foo/bar') + .expect('Supports: text/plain, text/html, application/json') + .expect(406, done) + }) + }) +} diff --git a/tests/express-tests/test/res.get.js b/tests/express-tests/test/res.get.js new file mode 100644 index 00000000..2398c485 --- /dev/null +++ b/tests/express-tests/test/res.get.js @@ -0,0 +1,21 @@ +'use strict' + +var express = require("express"); +var request = require('supertest'); + +describe('res', function(){ + describe('.get(field)', function(){ + it('should get the response header field', function (done) { + var app = express(); + + app.use(function (req, res) { + res.setHeader('Content-Type', 'text/x-foo'); + res.send(res.get('Content-Type')); + }); + + request(app) + .get('/') + .expect(200, 'text/x-foo', done); + }) + }) +}) diff --git a/tests/express-tests/test/res.json.js b/tests/express-tests/test/res.json.js new file mode 100644 index 00000000..6043cd1d --- /dev/null +++ b/tests/express-tests/test/res.json.js @@ -0,0 +1,229 @@ +'use strict' + +var express = require("express") + , request = require('supertest') + , assert = require('assert'); + +describe('res', function(){ + describe('.json(object)', function(){ + it('should not support jsonp callbacks', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json({ foo: 'bar' }); + }); + + request(app) + .get('/?callback=foo') + .expect('{"foo":"bar"}', done); + }) + + it('should not override previous Content-Types', function(done){ + var app = express(); + + app.get('/', function(req, res){ + res.type('application/vnd.example+json'); + res.json({ hello: 'world' }); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/vnd.example+json; charset=utf-8') + .expect(200, '{"hello":"world"}', done); + }) + + describe('when given primitives', function(){ + it('should respond with json for null', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json(null); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, 'null', done) + }) + + it('should respond with json for Number', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json(300); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '300', done) + }) + + it('should respond with json for String', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json('str'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '"str"', done) + }) + }) + + describe('when given an array', function(){ + it('should respond with json', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json(['foo', 'bar', 'baz']); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '["foo","bar","baz"]', done) + }) + }) + + describe('when given an object', function(){ + it('should respond with json', function(done){ + var app = express(); + + app.use(function(req, res){ + res.json({ name: 'tobi' }); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '{"name":"tobi"}', done) + }) + }) + + 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({ '&': ''); + }); + + request(app) + .get('/') + .set('Host', 'http://example.com') + .set('Accept', 'text/plain, */*') + .expect('Content-Type', /plain/) + .expect('Location', 'http://example.com/?param=%3Cscript%3Ealert(%22hax%22);%3C/script%3E') + .expect(302, 'Found. Redirecting to http://example.com/?param=%3Cscript%3Ealert(%22hax%22);%3C/script%3E', done) + }) + + it('should include the redirect type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.redirect(301, 'http://google.com'); + }); + + request(app) + .get('/') + .set('Accept', 'text/plain, */*') + .expect('Content-Type', /plain/) + .expect('Location', 'http://google.com') + .expect(301, 'Moved Permanently. Redirecting to http://google.com', done); + }) + }) + + describe('when accepting neither text or html', function(){ + it('should respond with an empty body', function(done){ + var app = express(); + + app.use(function(req, res){ + res.redirect('http://google.com'); + }); + + request(app) + .get('/') + .set('Accept', 'application/octet-stream') + .expect(302) + .expect('location', 'http://google.com') + .expect('content-length', '0') + .expect(utils.shouldNotHaveHeader('Content-Type')) + .expect(utils.shouldNotHaveBody()) + .end(done) + }) + }) +}) diff --git a/tests/express-tests/test/res.render.js b/tests/express-tests/test/res.render.js new file mode 100644 index 00000000..dd195a4b --- /dev/null +++ b/tests/express-tests/test/res.render.js @@ -0,0 +1,367 @@ +'use strict' + +var express = require("express"); +var path = require('path') +var request = require('supertest'); +var tmpl = require('./support/tmpl'); + +describe('res', function(){ + describe('.render(name)', function(){ + it('should support absolute paths', function(done){ + var app = createApp(); + + app.locals.user = { name: 'tobi' }; + + app.use(function(req, res){ + res.render(path.join(__dirname, 'fixtures', 'user.tmpl')) + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should support absolute paths with "view engine"', function(done){ + var app = createApp(); + + app.locals.user = { name: 'tobi' }; + app.set('view engine', 'tmpl'); + + app.use(function(req, res){ + res.render(path.join(__dirname, 'fixtures', 'user')) + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should error without "view engine" set and file extension to a non-engine module', function (done) { + var app = createApp() + + app.locals.user = { name: 'tobi' } + + app.use(function (req, res) { + res.render(path.join(__dirname, 'fixtures', 'broken.send')) + }) + + request(app) + .get('/') + .expect(500, /does not provide a view engine/, done) + }) + + it('should error without "view engine" set and no file extension', function (done) { + var app = createApp(); + + app.locals.user = { name: 'tobi' }; + + app.use(function(req, res){ + res.render(path.join(__dirname, 'fixtures', 'user')) + }); + + request(app) + .get('/') + .expect(500, /No default engine was specified/, done); + }) + + it('should expose app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + app.use(function(req, res){ + res.render('user.tmpl'); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should expose app.locals with `name` property', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.name = 'tobi'; + + app.use(function(req, res){ + res.render('name.tmpl'); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should support index.', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.set('view engine', 'tmpl'); + + app.use(function(req, res){ + res.render('blog/post'); + }); + + request(app) + .get('/') + .expect('

      blog post

      ', done); + }) + + describe('when an error occurs', function(){ + it('should next(err)', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + res.render('user.tmpl'); + }); + + app.use(function(err, req, res, next){ + res.status(500).send('got error: ' + err.name) + }); + + request(app) + .get('/') + .expect(500, 'got error: RenderError', done) + }) + }) + + describe('when "view engine" is given', function(){ + it('should render the template', function(done){ + var app = createApp(); + + app.set('view engine', 'tmpl'); + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + res.render('email'); + }); + + request(app) + .get('/') + .expect('

      This is an email

      ', done); + }) + }) + + describe('when "views" is given', function(){ + it('should lookup the file in the path', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures', 'default_layout')) + + app.use(function(req, res){ + res.render('user.tmpl', { user: { name: 'tobi' } }); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + describe('when array of paths', function(){ + it('should lookup the file in the path', function(done){ + var app = createApp(); + var views = [ + path.join(__dirname, 'fixtures', 'local_layout'), + path.join(__dirname, 'fixtures', 'default_layout') + ] + + app.set('views', views); + + app.use(function(req, res){ + res.render('user.tmpl', { user: { name: 'tobi' } }); + }); + + request(app) + .get('/') + .expect('tobi', done); + }) + + it('should lookup in later paths until found', function(done){ + var app = createApp(); + var views = [ + path.join(__dirname, 'fixtures', 'local_layout'), + path.join(__dirname, 'fixtures', 'default_layout') + ] + + app.set('views', views); + + app.use(function(req, res){ + res.render('name.tmpl', { name: 'tobi' }); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + }) + }) + }) + + describe('.render(name, option)', function(){ + it('should render the template', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + var user = { name: 'tobi' }; + + app.use(function(req, res){ + res.render('user.tmpl', { user: user }); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should expose app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + app.use(function(req, res){ + res.render('user.tmpl'); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should expose res.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + res.locals.user = { name: 'tobi' }; + res.render('user.tmpl'); + }); + + request(app) + .get('/') + .expect('

      tobi

      ', done); + }) + + it('should give precedence to res.locals over app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + + app.use(function(req, res){ + res.locals.user = { name: 'jane' }; + res.render('user.tmpl', {}); + }); + + request(app) + .get('/') + .expect('

      jane

      ', done); + }) + + it('should give precedence to res.render() locals over res.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + var jane = { name: 'jane' }; + + app.use(function(req, res){ + res.locals.user = { name: 'tobi' }; + res.render('user.tmpl', { user: jane }); + }); + + request(app) + .get('/') + .expect('

      jane

      ', done); + }) + + it('should give precedence to res.render() locals over app.locals', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + app.locals.user = { name: 'tobi' }; + var jane = { name: 'jane' }; + + app.use(function(req, res){ + res.render('user.tmpl', { user: jane }); + }); + + request(app) + .get('/') + .expect('

      jane

      ', done); + }) + }) + + describe('.render(name, options, fn)', function(){ + it('should pass the resulting string', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + var tobi = { name: 'tobi' }; + res.render('user.tmpl', { user: tobi }, function (err, html) { + html = html.replace('tobi', 'loki'); + res.end(html); + }); + }); + + request(app) + .get('/') + .expect('

      loki

      ', done); + }) + }) + + describe('.render(name, fn)', function(){ + it('should pass the resulting string', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + res.locals.user = { name: 'tobi' }; + res.render('user.tmpl', function (err, html) { + html = html.replace('tobi', 'loki'); + res.end(html); + }); + }); + + request(app) + .get('/') + .expect('

      loki

      ', done); + }) + + describe('when an error occurs', function(){ + it('should pass it to the callback', function(done){ + var app = createApp(); + + app.set('views', path.join(__dirname, 'fixtures')) + + app.use(function(req, res){ + res.render('user.tmpl', function (err) { + if (err) { + res.status(500).send('got error: ' + err.name) + } + }); + }); + + request(app) + .get('/') + .expect(500, 'got error: RenderError', done) + }) + }) + }) +}) + +function createApp() { + var app = express(); + + app.engine('.tmpl', tmpl); + + return app; +} diff --git a/tests/express-tests/test/res.send.js b/tests/express-tests/test/res.send.js new file mode 100644 index 00000000..dbd05a00 --- /dev/null +++ b/tests/express-tests/test/res.send.js @@ -0,0 +1,601 @@ +'use strict' + +var assert = require('assert') +var Buffer = require('safe-buffer').Buffer +var express = require("express"); +var methods = require('methods'); +var request = require('supertest'); +var utils = require('./support/utils'); + +var shouldSkipQuery = require('./support/utils').shouldSkipQuery + +describe('res', function(){ + describe('.send()', function(){ + it('should set body to ""', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(); + }); + + request(app) + .get('/') + .expect(200, '', done); + }) + }) + + describe('.send(null)', function(){ + it('should set body to ""', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(null); + }); + + request(app) + .get('/') + .expect('Content-Length', '0') + .expect(200, '', done); + }) + }) + + describe('.send(undefined)', function(){ + it('should set body to ""', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(undefined); + }); + + request(app) + .get('/') + .expect(200, '', done); + }) + }) + + describe('.send(code)', function(){ + it('should set .statusCode', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(201) + }); + + request(app) + .get('/') + .expect('Created') + .expect(201, done); + }) + }) + + describe('.send(code, body)', function(){ + it('should set .statusCode and body', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(201, 'Created :)'); + }); + + request(app) + .get('/') + .expect('Created :)') + .expect(201, done); + }) + }) + + describe('.send(body, code)', function(){ + it('should be supported for backwards compat', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send('Bad!', 400); + }); + + request(app) + .get('/') + .expect('Bad!') + .expect(400, done); + }) + }) + + describe('.send(code, number)', function(){ + it('should send number as json', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(200, 0.123); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '0.123', done); + }) + }) + + describe('.send(String)', function(){ + it('should send as html', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send('

      hey

      '); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=utf-8') + .expect(200, '

      hey

      ', done); + }) + + it('should set ETag', function (done) { + var app = express(); + + app.use(function (req, res) { + var str = Array(1000).join('-'); + res.send(str); + }); + + request(app) + .get('/') + .expect('ETag', 'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"') + .expect(200, done); + }) + + it('should not override Content-Type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Content-Type', 'text/plain').send('hey'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect(200, 'hey', done); + }) + + it('should override charset in Content-Type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Content-Type', 'text/plain; charset=iso-8859-1').send('hey'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect(200, 'hey', done); + }) + + it('should keep charset in Content-Type for Buffers', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Content-Type', 'text/plain; charset=iso-8859-1').send(Buffer.from('hi')) + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=iso-8859-1') + .expect(200, 'hi', done); + }) + }) + + describe('.send(Buffer)', function(){ + it('should send as octet-stream', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send(Buffer.from('hello')) + }); + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'application/octet-stream') + .expect(utils.shouldHaveBody(Buffer.from('hello'))) + .end(done) + }) + + it('should set ETag', function (done) { + var app = express(); + + app.use(function (req, res) { + res.send(Buffer.alloc(999, '-')) + }); + + request(app) + .get('/') + .expect('ETag', 'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"') + .expect(200, done); + }) + + it('should not override Content-Type', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Content-Type', 'text/plain').send(Buffer.from('hey')) + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect(200, 'hey', done); + }) + + it('should not override ETag', function (done) { + var app = express() + + app.use(function (req, res) { + res.type('text/plain').set('ETag', '"foo"').send(Buffer.from('hey')) + }) + + request(app) + .get('/') + .expect('ETag', '"foo"') + .expect(200, 'hey', done) + }) + }) + + describe('.send(Object)', function(){ + it('should send as application/json', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send({ name: 'tobi' }); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200, '{"name":"tobi"}', done) + }) + }) + + describe('when the request method is HEAD', function(){ + it('should ignore the body', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send('yay'); + }); + + request(app) + .head('/') + .expect(200) + .expect(utils.shouldNotHaveBody()) + .end(done) + }) + }) + + describe('when .statusCode is 204', function(){ + it('should strip Content-* fields, Transfer-Encoding field, and body', function(done){ + var app = express(); + + app.use(function(req, res){ + res.status(204).set('Transfer-Encoding', 'chunked').send('foo'); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('Content-Type')) + .expect(utils.shouldNotHaveHeader('Content-Length')) + .expect(utils.shouldNotHaveHeader('Transfer-Encoding')) + .expect(204, '', done); + }) + }) + + describe('when .statusCode is 205', function () { + it('should strip Transfer-Encoding field and body, set Content-Length', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(205).set('Transfer-Encoding', 'chunked').send('foo') + }) + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('Transfer-Encoding')) + .expect('Content-Length', '0') + .expect(205, '', done) + }) + }) + + describe('when .statusCode is 304', function(){ + it('should strip Content-* fields, Transfer-Encoding field, and body', function(done){ + var app = express(); + + app.use(function(req, res){ + res.status(304).set('Transfer-Encoding', 'chunked').send('foo'); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('Content-Type')) + .expect(utils.shouldNotHaveHeader('Content-Length')) + .expect(utils.shouldNotHaveHeader('Transfer-Encoding')) + .expect(304, '', done); + }) + }) + + it('should always check regardless of length', function(done){ + var app = express(); + var etag = '"asdf"'; + + app.use(function(req, res, next){ + res.set('ETag', etag); + res.send('hey'); + }); + + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, done); + }) + + it('should respond with 304 Not Modified when fresh', function(done){ + var app = express(); + var etag = '"asdf"'; + + app.use(function(req, res){ + var str = Array(1000).join('-'); + res.set('ETag', etag); + res.send(str); + }); + + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, done); + }) + + it('should not perform freshness check unless 2xx or 304', function(done){ + var app = express(); + var etag = '"asdf"'; + + app.use(function(req, res, next){ + res.status(500); + res.set('ETag', etag); + res.send('hey'); + }); + + request(app) + .get('/') + .set('If-None-Match', etag) + .expect('hey') + .expect(500, done); + }) + + it('should not support jsonp callbacks', function(done){ + var app = express(); + + app.use(function(req, res){ + res.send({ foo: 'bar' }); + }); + + request(app) + .get('/?callback=foo') + .expect('{"foo":"bar"}', done); + }) + + it('should be chainable', function (done) { + var app = express() + + app.use(function (req, res) { + assert.equal(res.send('hey'), res) + }) + + request(app) + .get('/') + .expect(200, 'hey', done) + }) + + describe('"etag" setting', function () { + describe('when enabled', function () { + it('should send ETag', function (done) { + var app = express(); + + app.use(function (req, res) { + res.send('kajdslfkasdf'); + }); + + app.enable('etag'); + + request(app) + .get('/') + .expect('ETag', 'W/"c-IgR/L5SF7CJQff4wxKGF/vfPuZ0"') + .expect(200, done); + }); + + methods.forEach(function (method) { + if (method === 'connect') return; + + it('should send ETag in response to ' + method.toUpperCase() + ' request', function (done) { + if (method === 'query' && shouldSkipQuery(process.versions.node)) { + this.skip() + } + var app = express(); + + app[method]('/', function (req, res) { + res.send('kajdslfkasdf'); + }); + + request(app) + [method]('/') + .expect('ETag', 'W/"c-IgR/L5SF7CJQff4wxKGF/vfPuZ0"') + .expect(200, done); + }) + }); + + it('should send ETag for empty string response', function (done) { + var app = express(); + + app.use(function (req, res) { + res.send(''); + }); + + app.enable('etag'); + + request(app) + .get('/') + .expect('ETag', 'W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"') + .expect(200, done); + }) + + it('should send ETag for long response', function (done) { + var app = express(); + + app.use(function (req, res) { + var str = Array(1000).join('-'); + res.send(str); + }); + + app.enable('etag'); + + request(app) + .get('/') + .expect('ETag', 'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"') + .expect(200, done); + }); + + it('should not override ETag when manually set', function (done) { + var app = express(); + + app.use(function (req, res) { + res.set('etag', '"asdf"'); + res.send(200); + }); + + app.enable('etag'); + + request(app) + .get('/') + .expect('ETag', '"asdf"') + .expect(200, done); + }); + + it('should not send ETag for res.send()', function (done) { + var app = express(); + + app.use(function (req, res) { + res.send(); + }); + + app.enable('etag'); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('ETag')) + .expect(200, done); + }) + }); + + describe('when disabled', function () { + it('should send no ETag', function (done) { + var app = express(); + + app.use(function (req, res) { + var str = Array(1000).join('-'); + res.send(str); + }); + + app.disable('etag'); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('ETag')) + .expect(200, done); + }); + + it('should send ETag when manually set', function (done) { + var app = express(); + + app.disable('etag'); + + app.use(function (req, res) { + res.set('etag', '"asdf"'); + res.send(200); + }); + + request(app) + .get('/') + .expect('ETag', '"asdf"') + .expect(200, done); + }); + }); + + describe('when "strong"', function () { + it('should send strong ETag', function (done) { + var app = express(); + + app.set('etag', 'strong'); + + app.use(function (req, res) { + res.send('hello, world!'); + }); + + request(app) + .get('/') + .expect('ETag', '"d-HwnTDHB9U/PRbFMN1z1wps51lqk"') + .expect(200, done); + }) + }) + + describe('when "weak"', function () { + it('should send weak ETag', function (done) { + var app = express(); + + app.set('etag', 'weak'); + + app.use(function (req, res) { + res.send('hello, world!'); + }); + + request(app) + .get('/') + .expect('ETag', 'W/"d-HwnTDHB9U/PRbFMN1z1wps51lqk"') + .expect(200, done) + }) + }) + + describe('when a function', function () { + it('should send custom ETag', function (done) { + var app = express(); + + app.set('etag', function (body, encoding) { + var chunk = !Buffer.isBuffer(body) + ? Buffer.from(body, encoding) + : body; + assert.strictEqual(chunk.toString(), 'hello, world!') + return '"custom"'; + }); + + app.use(function (req, res) { + res.send('hello, world!'); + }); + + request(app) + .get('/') + .expect('ETag', '"custom"') + .expect(200, done); + }) + + it('should not send falsy ETag', function (done) { + var app = express(); + + app.set('etag', function (body, encoding) { + return undefined; + }); + + app.use(function (req, res) { + res.send('hello, world!'); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('ETag')) + .expect(200, done); + }) + }) + }) +}) diff --git a/tests/express-tests/test/res.sendFile.js b/tests/express-tests/test/res.sendFile.js new file mode 100644 index 00000000..da758c98 --- /dev/null +++ b/tests/express-tests/test/res.sendFile.js @@ -0,0 +1,1412 @@ +'use strict' + +var after = require('after'); +var asyncHooks = tryRequire('async_hooks') +var Buffer = require('safe-buffer').Buffer +var express = require("express") + , request = require('supertest') + , assert = require('assert'); +var onFinished = require('on-finished'); +var path = require('path'); +var fixtures = path.join(__dirname, 'fixtures'); +var utils = require('./support/utils'); + +var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' + ? describe + : describe.skip + +describe('res', function(){ + describe('.sendFile(path)', function () { + it('should error missing path', function (done) { + var app = createApp(); + + request(app) + .get('/') + .expect(500, /path.*required/, done); + }); + + it('should error for non-string path', function (done) { + var app = createApp(42) + + request(app) + .get('/') + .expect(500, /TypeError: path must be a string to res.sendFile/, done) + }) + + it('should error for non-absolute path', function (done) { + var app = createApp('name.txt') + + request(app) + .get('/') + .expect(500, /TypeError: path must be absolute/, done) + }) + + it('should transfer a file', function (done) { + var app = createApp(path.resolve(fixtures, 'name.txt')); + + request(app) + .get('/') + .expect(200, 'tobi', done); + }); + + it('should transfer a file with special characters in string', function (done) { + var app = createApp(path.resolve(fixtures, '% of dogs.txt')); + + request(app) + .get('/') + .expect(200, '20%', done); + }); + + it('should include ETag', function (done) { + var app = createApp(path.resolve(fixtures, 'name.txt')); + + request(app) + .get('/') + .expect('ETag', /^(?:W\/)?"[^"]+"$/) + .expect(200, 'tobi', done); + }); + + it('should 304 when ETag matches', function (done) { + var app = createApp(path.resolve(fixtures, 'name.txt')); + + request(app) + .get('/') + .expect('ETag', /^(?:W\/)?"[^"]+"$/) + .expect(200, 'tobi', function (err, res) { + if (err) return done(err); + var etag = res.headers.etag; + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, done); + }); + }); + + it('should 404 for directory', function (done) { + var app = createApp(path.resolve(fixtures, 'blog')); + + request(app) + .get('/') + .expect(404, done); + }); + + it('should 404 when not found', function (done) { + var app = createApp(path.resolve(fixtures, 'does-no-exist')); + + app.use(function (req, res) { + res.statusCode = 200; + res.send('no!'); + }); + + request(app) + .get('/') + .expect(404, done); + }); + + it('should send cache-control by default', function (done) { + var app = createApp(path.resolve(__dirname, 'fixtures/name.txt')) + + request(app) + .get('/') + .expect('Cache-Control', 'public, max-age=0') + .expect(200, done) + }) + + it('should not serve dotfiles by default', function (done) { + var app = createApp(path.resolve(__dirname, 'fixtures/.name')) + + request(app) + .get('/') + .expect(404, done) + }) + + it('should not override manual content-types', function (done) { + var app = express(); + + app.use(function (req, res) { + res.contentType('application/x-bogus'); + res.sendFile(path.resolve(fixtures, 'name.txt')); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/x-bogus') + .end(done); + }) + + it('should not error if the client aborts', function (done) { + var app = express(); + var cb = after(2, done) + var error = null + + app.use(function (req, res) { + setImmediate(function () { + res.sendFile(path.resolve(fixtures, 'name.txt')); + setTimeout(function () { + cb(error) + }, 10) + }) + test.req.abort() + }); + + app.use(function (err, req, res, next) { + error = err + next(err) + }); + + var server = app.listen() + var test = request(server).get('/') + test.end(function (err) { + assert.ok(err) + server.close(cb) + }) + }) + }) + + describe('.sendFile(path, fn)', function () { + it('should invoke the callback when complete', function (done) { + var cb = after(2, done); + var app = createApp(path.resolve(fixtures, 'name.txt'), cb); + + request(app) + .get('/') + .expect(200, cb); + }) + + it('should invoke the callback when client aborts', function (done) { + var cb = after(2, done) + var app = express(); + + app.use(function (req, res) { + setImmediate(function () { + res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) { + assert.ok(err) + assert.strictEqual(err.code, 'ECONNABORTED') + cb() + }); + }); + test.req.abort() + }); + + var server = app.listen() + var test = request(server).get('/') + test.end(function (err) { + assert.ok(err) + server.close(cb) + }) + }) + + it('should invoke the callback when client already aborted', function (done) { + var cb = after(2, done) + var app = express(); + + app.use(function (req, res) { + onFinished(res, function () { + res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) { + assert.ok(err) + assert.strictEqual(err.code, 'ECONNABORTED') + cb() + }); + }); + test.req.abort() + }); + + var server = app.listen() + var test = request(server).get('/') + test.end(function (err) { + assert.ok(err) + server.close(cb) + }) + }) + + it('should invoke the callback without error when HEAD', function (done) { + var app = express(); + var cb = after(2, done); + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'name.txt'), cb); + }); + + request(app) + .head('/') + .expect(200, cb); + }); + + it('should invoke the callback without error when 304', function (done) { + var app = express(); + var cb = after(3, done); + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'name.txt'), cb); + }); + + request(app) + .get('/') + .expect('ETag', /^(?:W\/)?"[^"]+"$/) + .expect(200, 'tobi', function (err, res) { + if (err) return cb(err); + var etag = res.headers.etag; + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, cb); + }); + }); + + it('should invoke the callback on 404', function(done){ + var app = express(); + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'does-not-exist'), function (err) { + res.send(err ? 'got ' + err.status + ' error' : 'no error') + }); + }); + + request(app) + .get('/') + .expect(200, 'got 404 error', done) + }) + + describeAsyncHooks('async local storage', function () { + it('should presist store', function (done) { + var app = express() + var cb = after(2, done) + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) { + if (err) return cb(err) + + var local = req.asyncLocalStorage.getStore() + + assert.strictEqual(local.foo, 'bar') + cb() + }) + }) + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect(200, 'tobi', cb) + }) + + it('should presist store on error', function (done) { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'does-not-exist'), function (err) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.send(err ? 'got ' + err.status + ' error' : 'no error') + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('got 404 error') + .end(done) + }) + }) + }) + + describe('.sendFile(path, options)', function () { + it('should pass options to send module', function (done) { + request(createApp(path.resolve(fixtures, 'name.txt'), { start: 0, end: 1 })) + .get('/') + .expect(200, 'to', done) + }) + + describe('with "acceptRanges" option', function () { + describe('when true', function () { + it('should advertise byte range accepted', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'nums.txt'), { + acceptRanges: true + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Accept-Ranges', 'bytes') + .expect('123456789') + .end(done) + }) + + it('should respond to range request', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'nums.txt'), { + acceptRanges: true + }) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-4') + .expect(206, '12345', done) + }) + }) + + describe('when false', function () { + it('should not advertise accept-ranges', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'nums.txt'), { + acceptRanges: false + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldNotHaveHeader('Accept-Ranges')) + .end(done) + }) + + it('should not honor range requests', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'nums.txt'), { + acceptRanges: false + }) + }) + + request(app) + .get('/') + .set('Range', 'bytes=0-4') + .expect(200, '123456789', done) + }) + }) + }) + + describe('with "cacheControl" option', function () { + describe('when true', function () { + it('should send cache-control header', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + cacheControl: true + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=0') + .end(done) + }) + }) + + describe('when false', function () { + it('should not send cache-control header', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + cacheControl: false + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldNotHaveHeader('Cache-Control')) + .end(done) + }) + }) + }) + + describe('with "dotfiles" option', function () { + describe('when "allow"', function () { + it('should allow dotfiles', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, '.name'), { + dotfiles: 'allow' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + }) + + describe('when "deny"', function () { + it('should deny dotfiles', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, '.name'), { + dotfiles: 'deny' + }) + }) + + request(app) + .get('/') + .expect(403) + .expect(/Forbidden/) + .end(done) + }) + }) + + describe('when "ignore"', function () { + it('should ignore dotfiles', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, '.name'), { + dotfiles: 'ignore' + }) + }) + + request(app) + .get('/') + .expect(404) + .expect(/Not Found/) + .end(done) + }) + }) + }) + + describe('with "headers" option', function () { + it('should set headers on response', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + headers: { + 'X-Foo': 'Bar', + 'X-Bar': 'Foo' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('X-Foo', 'Bar') + .expect('X-Bar', 'Foo') + .end(done) + }) + + it('should use last header when duplicated', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + headers: { + 'X-Foo': 'Bar', + 'x-foo': 'bar' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('X-Foo', 'bar') + .end(done) + }) + + it('should override Content-Type', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + headers: { + 'Content-Type': 'text/x-custom' + } + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Content-Type', 'text/x-custom') + .end(done) + }) + + it('should not set headers on 404', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'does-not-exist'), { + headers: { + 'X-Foo': 'Bar' + } + }) + }) + + request(app) + .get('/') + .expect(404) + .expect(utils.shouldNotHaveHeader('X-Foo')) + .end(done) + }) + }) + + describe('with "immutable" option', function () { + describe('when true', function () { + it('should send cache-control header with immutable', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + immutable: true + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=0, immutable') + .end(done) + }) + }) + + describe('when false', function () { + it('should not send cache-control header with immutable', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + immutable: false + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=0') + .end(done) + }) + }) + }) + + describe('with "lastModified" option', function () { + describe('when true', function () { + it('should send last-modified header', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + lastModified: true + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldHaveHeader('Last-Modified')) + .end(done) + }) + + it('should conditionally respond with if-modified-since', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + lastModified: true + }) + }) + + request(app) + .get('/') + .set('If-Modified-Since', (new Date(Date.now() + 99999).toUTCString())) + .expect(304, done) + }) + }) + + describe('when false', function () { + it('should not have last-modified header', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + lastModified: false + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldNotHaveHeader('Last-Modified')) + .end(done) + }) + + it('should not honor if-modified-since', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + lastModified: false + }) + }) + + request(app) + .get('/') + .set('If-Modified-Since', (new Date(Date.now() + 99999).toUTCString())) + .expect(200) + .expect(utils.shouldNotHaveHeader('Last-Modified')) + .end(done) + }) + }) + }) + + describe('with "maxAge" option', function () { + it('should set cache-control max-age to milliseconds', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: 20000 + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=20') + .end(done) + }) + + it('should cap cache-control max-age to 1 year', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: 99999999999 + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=31536000') + .end(done) + }) + + it('should min cache-control max-age to 0', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: -20000 + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=0') + .end(done) + }) + + it('should floor cache-control max-age', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: 21911.23 + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=21') + .end(done) + }) + + describe('when cacheControl: false', function () { + it('should not send cache-control', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + cacheControl: false, + maxAge: 20000 + }) + }) + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldNotHaveHeader('Cache-Control')) + .end(done) + }) + }) + + describe('when string', function () { + it('should accept plain number as milliseconds', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: '20000' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=20') + .end(done) + }) + + it('should accept suffix "s" for seconds', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: '20s' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=20') + .end(done) + }) + + it('should accept suffix "m" for minutes', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: '20m' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=1200') + .end(done) + }) + + it('should accept suffix "d" for days', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile(path.resolve(fixtures, 'user.html'), { + maxAge: '20d' + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('Cache-Control', 'public, max-age=1728000') + .end(done) + }) + }) + }) + + describe('with "root" option', function () { + it('should allow relative path', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile('name.txt', { + root: fixtures + }) + }) + + request(app) + .get('/') + .expect(200, 'tobi', done) + }) + + it('should allow up within root', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile('fake/../name.txt', { + root: fixtures + }) + }) + + request(app) + .get('/') + .expect(200, 'tobi', done) + }) + + it('should reject up outside root', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile('..' + path.sep + path.relative(path.dirname(fixtures), path.join(fixtures, 'name.txt')), { + root: fixtures + }) + }) + + request(app) + .get('/') + .expect(403, done) + }) + + it('should reject reading outside root', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendFile('../name.txt', { + root: fixtures + }) + }) + + request(app) + .get('/') + .expect(403, done) + }) + }) + }) + + describe('.sendfile(path, fn)', function(){ + it('should invoke the callback when complete', function(done){ + var app = express(); + var cb = after(2, done); + + app.use(function(req, res){ + res.sendfile('tests/express-tests/test/fixtures/user.html', cb) + }); + + request(app) + .get('/') + .expect(200, cb); + }) + + it('should utilize the same options as express.static()', function(done){ + var app = express(); + + app.use(function(req, res){ + res.sendfile('tests/express-tests/test/fixtures/user.html', { maxAge: 60000 }); + }); + + request(app) + .get('/') + .expect('Cache-Control', 'public, max-age=60') + .end(done); + }) + + it('should invoke the callback when client aborts', function (done) { + var cb = after(2, done) + var app = express(); + + app.use(function (req, res) { + setImmediate(function () { + res.sendfile('tests/express-tests/test/fixtures/name.txt', function (err) { + assert.ok(err) + assert.strictEqual(err.code, 'ECONNABORTED') + cb() + }); + }); + test.req.abort() + }); + + var server = app.listen() + var test = request(server).get('/') + test.end(function (err) { + assert.ok(err) + server.close(cb) + }) + }) + + it('should invoke the callback when client already aborted', function (done) { + var cb = after(2, done) + var app = express(); + + app.use(function (req, res) { + onFinished(res, function () { + res.sendfile('tests/express-tests/test/fixtures/name.txt', function (err) { + assert.ok(err) + assert.strictEqual(err.code, 'ECONNABORTED') + cb() + }); + }); + test.req.abort() + }); + + var server = app.listen() + var test = request(server).get('/') + test.end(function (err) { + assert.ok(err) + server.close(cb) + }) + }) + + it('should invoke the callback without error when HEAD', function (done) { + var app = express(); + var cb = after(2, done); + + app.use(function (req, res) { + res.sendfile('tests/express-tests/test/fixtures/name.txt', cb); + }); + + request(app) + .head('/') + .expect(200, cb); + }); + + it('should invoke the callback without error when 304', function (done) { + var app = express(); + var cb = after(3, done); + + app.use(function (req, res) { + res.sendfile('tests/express-tests/test/fixtures/name.txt', cb); + }); + + request(app) + .get('/') + .expect('ETag', /^(?:W\/)?"[^"]+"$/) + .expect(200, 'tobi', function (err, res) { + if (err) return cb(err); + var etag = res.headers.etag; + request(app) + .get('/') + .set('If-None-Match', etag) + .expect(304, cb); + }); + }); + + it('should invoke the callback on 404', function(done){ + var app = express(); + var calls = 0; + + app.use(function(req, res){ + res.sendfile('tests/express-tests/test/fixtures/nope.html', function(err){ + assert.equal(calls++, 0); + assert(!res.headersSent); + res.send(err.message); + }); + }); + + request(app) + .get('/') + .expect(200, /^ENOENT.*?, stat/, done); + }) + + it('should not override manual content-types', function(done){ + var app = express(); + + app.use(function(req, res){ + res.contentType('txt'); + res.sendfile('tests/express-tests/test/fixtures/user.html'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=utf-8') + .end(done); + }) + + it('should invoke the callback on 403', function(done){ + var app = express() + + app.use(function(req, res){ + res.sendfile('tests/express-tests/test/fixtures/foo/../user.html', function(err){ + assert(!res.headersSent); + res.send(err.message); + }); + }); + + request(app) + .get('/') + .expect('Forbidden') + .expect(200, done); + }) + + it('should invoke the callback on socket error', function(done){ + var app = express() + + app.use(function(req, res){ + res.sendfile('tests/express-tests/test/fixtures/user.html', function(err){ + assert.ok(err) + assert.ok(!res.headersSent) + assert.strictEqual(err.message, 'broken!') + done(); + }); + + req.socket.destroy(new Error('broken!')) + }); + + request(app) + .get('/') + .end(function(){}); + }) + + describeAsyncHooks('async local storage', function () { + it('should presist store', function (done) { + var app = express() + var cb = after(2, done) + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(function (req, res) { + res.sendfile('tests/express-tests/test/fixtures/name.txt', function (err) { + if (err) return cb(err) + + var local = req.asyncLocalStorage.getStore() + + assert.strictEqual(local.foo, 'bar') + cb() + }) + }) + + request(app) + .get('/') + .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect(200, 'tobi', cb) + }) + + it('should presist store on error', function (done) { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(function (req, res) { + res.sendfile('tests/express-tests/test/fixtures/does-not-exist', function (err) { + var local = req.asyncLocalStorage.getStore() + + if (local) { + res.setHeader('x-store-foo', String(local.foo)) + } + + res.send(err ? 'got ' + err.status + ' error' : 'no error') + }) + }) + + request(app) + .get('/') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('got 404 error') + .end(done) + }) + }) + }) + + describe('.sendfile(path)', function(){ + it('should not serve dotfiles', function(done){ + var app = express(); + + app.use(function(req, res){ + res.sendfile('tests/express-tests/test/fixtures/.name'); + }); + + request(app) + .get('/') + .expect(404, done); + }) + + it('should accept dotfiles option', function(done){ + var app = express(); + + app.use(function(req, res){ + res.sendfile('tests/express-tests/test/fixtures/.name', { dotfiles: 'allow' }); + }); + + request(app) + .get('/') + .expect(200) + .expect(utils.shouldHaveBody(Buffer.from('tobi'))) + .end(done) + }) + + it('should accept headers option', function(done){ + var app = express(); + var headers = { + 'x-success': 'sent', + 'x-other': 'done' + }; + + app.use(function(req, res){ + res.sendfile('tests/express-tests/test/fixtures/user.html', { headers: headers }); + }); + + request(app) + .get('/') + .expect('x-success', 'sent') + .expect('x-other', 'done') + .expect(200, done); + }) + + it('should ignore headers option on 404', function(done){ + var app = express(); + var headers = { 'x-success': 'sent' }; + + app.use(function(req, res){ + res.sendfile('tests/express-tests/test/fixtures/user.nothing', { headers: headers }); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('X-Success')) + .expect(404, done); + }) + + it('should transfer a file', function (done) { + var app = express(); + + app.use(function (req, res) { + res.sendfile('tests/express-tests/test/fixtures/name.txt'); + }); + + request(app) + .get('/') + .expect(200, 'tobi', done); + }); + + it('should transfer a directory index file', function (done) { + var app = express(); + + app.use(function (req, res) { + res.sendfile('tests/express-tests/test/fixtures/blog/'); + }); + + request(app) + .get('/') + .expect(200, 'index', done); + }); + + it('should 404 for directory without trailing slash', function (done) { + var app = express(); + + app.use(function (req, res) { + res.sendfile('tests/express-tests/test/fixtures/blog'); + }); + + request(app) + .get('/') + .expect(404, done); + }); + + it('should transfer a file with urlencoded name', function (done) { + var app = express(); + + app.use(function (req, res) { + res.sendfile('tests/express-tests/test/fixtures/%25%20of%20dogs.txt'); + }); + + request(app) + .get('/') + .expect(200, '20%', done); + }); + + it('should not error if the client aborts', function (done) { + var app = express(); + var cb = after(2, done) + var error = null + + app.use(function (req, res) { + setImmediate(function () { + res.sendfile(path.resolve(fixtures, 'name.txt')); + setTimeout(function () { + cb(error) + }, 10) + }); + test.req.abort() + }); + + app.use(function (err, req, res, next) { + error = err + next(err) + }); + + var server = app.listen() + var test = request(server).get('/') + test.end(function (err) { + assert.ok(err) + server.close(cb) + }) + }) + + describe('with an absolute path', function(){ + it('should transfer the file', function(done){ + var app = express(); + + app.use(function(req, res){ + res.sendfile(path.join(__dirname, '/fixtures/user.html')) + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=UTF-8') + .expect(200, '

      {{user.name}}

      ', done); + }) + }) + + describe('with a relative path', function(){ + it('should transfer the file', function(done){ + var app = express(); + + app.use(function(req, res){ + res.sendfile('tests/express-tests/test/fixtures/user.html'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=UTF-8') + .expect(200, '

      {{user.name}}

      ', done); + }) + + it('should serve relative to "root"', function(done){ + var app = express(); + + app.use(function(req, res){ + res.sendfile('user.html', { root: 'tests/express-tests/test/fixtures/' }); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=UTF-8') + .expect(200, '

      {{user.name}}

      ', done); + }) + + it('should consider ../ malicious when "root" is not set', function(done){ + var app = express(); + + app.use(function(req, res){ + res.sendfile('tests/express-tests/test/fixtures/foo/../user.html'); + }); + + request(app) + .get('/') + .expect(403, done); + }) + + it('should allow ../ when "root" is set', function(done){ + var app = express(); + + app.use(function(req, res){ + res.sendfile('foo/../user.html', { root: 'tests/express-tests/test/fixtures' }); + }); + + request(app) + .get('/') + .expect(200, done); + }) + + it('should disallow requesting out of "root"', function(done){ + var app = express(); + + app.use(function(req, res){ + res.sendfile('foo/../../user.html', { root: 'tests/express-tests/test/fixtures' }); + }); + + request(app) + .get('/') + .expect(403, done); + }) + + it('should next(404) when not found', function(done){ + var app = express() + , calls = 0; + + app.use(function(req, res){ + res.sendfile('user.html'); + }); + + app.use(function(req, res){ + assert(0, 'this should not be called'); + }); + + app.use(function(err, req, res, next){ + ++calls; + next(err); + }); + + request(app) + .get('/') + .expect(404, function (err) { + if (err) return done(err) + assert.strictEqual(calls, 1) + done() + }) + }) + + describe('with non-GET', function(){ + it('should still serve', function(done){ + var app = express() + + app.use(function(req, res){ + res.sendfile(path.join(__dirname, '/fixtures/name.txt')) + }); + + request(app) + .get('/') + .expect('tobi', done); + }) + }) + }) + }) + + describe('.sendfile(path, options)', function () { + it('should pass options to send module', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendfile(path.resolve(fixtures, 'name.txt'), { start: 0, end: 1 }) + }) + + request(app) + .get('/') + .expect(200, 'to', done) + }) + }) +}) + +function createApp(path, options, fn) { + var app = express(); + + app.use(function (req, res) { + res.sendFile(path, options, fn); + }); + + return app; +} + +function tryRequire (name) { + try { + return require(name) + } catch (e) { + return {} + } +} diff --git a/tests/express-tests/test/res.sendStatus.js b/tests/express-tests/test/res.sendStatus.js new file mode 100644 index 00000000..ce321ebb --- /dev/null +++ b/tests/express-tests/test/res.sendStatus.js @@ -0,0 +1,32 @@ +'use strict' + +var express = require("express") +var request = require('supertest') + +describe('res', function () { + describe('.sendStatus(statusCode)', function () { + it('should send the status code and message as body', function (done) { + var app = express(); + + app.use(function(req, res){ + res.sendStatus(201); + }); + + request(app) + .get('/') + .expect(201, 'Created', done); + }) + + it('should work with unknown code', function (done) { + var app = express(); + + app.use(function(req, res){ + res.sendStatus(599); + }); + + request(app) + .get('/') + .expect(599, '599', done); + }) + }) +}) diff --git a/tests/express-tests/test/res.set.js b/tests/express-tests/test/res.set.js new file mode 100644 index 00000000..2c794c41 --- /dev/null +++ b/tests/express-tests/test/res.set.js @@ -0,0 +1,124 @@ +'use strict' + +var express = require("express"); +var request = require('supertest'); + +describe('res', function(){ + describe('.set(field, value)', function(){ + it('should set the response header field', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Content-Type', 'text/x-foo; charset=utf-8').end(); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/x-foo; charset=utf-8') + .end(done); + }) + + it('should coerce to a string', function (done) { + var app = express(); + + app.use(function (req, res) { + res.set('X-Number', 123); + res.end(typeof res.get('X-Number')); + }); + + request(app) + .get('/') + .expect('X-Number', '123') + .expect(200, 'string', done); + }) + }) + + describe('.set(field, values)', function(){ + it('should set multiple response header fields', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set('Set-Cookie', ["type=ninja", "language=javascript"]); + res.send(res.get('Set-Cookie')); + }); + + request(app) + .get('/') + .expect('["type=ninja","language=javascript"]', done); + }) + + it('should coerce to an array of strings', function (done) { + var app = express(); + + app.use(function (req, res) { + res.set('X-Numbers', [123, 456]); + res.end(JSON.stringify(res.get('X-Numbers'))); + }); + + request(app) + .get('/') + .expect('X-Numbers', '123, 456') + .expect(200, '["123","456"]', done); + }) + + it('should not set a charset of one is already set', function (done) { + var app = express(); + + app.use(function (req, res) { + res.set('Content-Type', 'text/html; charset=lol'); + res.end(); + }); + + request(app) + .get('/') + .expect('Content-Type', 'text/html; charset=lol') + .expect(200, done); + }) + + it('should throw when Content-Type is an array', function (done) { + var app = express() + + app.use(function (req, res) { + res.set('Content-Type', ['text/html']) + res.end() + }); + + request(app) + .get('/') + .expect(500, /TypeError: Content-Type cannot be set to an Array/, done) + }) + }) + + describe('.set(object)', function(){ + it('should set multiple fields', function(done){ + var app = express(); + + app.use(function(req, res){ + res.set({ + 'X-Foo': 'bar', + 'X-Bar': 'baz' + }).end(); + }); + + request(app) + .get('/') + .expect('X-Foo', 'bar') + .expect('X-Bar', 'baz') + .end(done); + }) + + it('should coerce to a string', function (done) { + var app = express(); + + app.use(function (req, res) { + res.set({ 'X-Number': 123 }); + res.end(typeof res.get('X-Number')); + }); + + request(app) + .get('/') + .expect('X-Number', '123') + .expect(200, 'string', done); + }) + }) +}) diff --git a/tests/express-tests/test/res.status.js b/tests/express-tests/test/res.status.js new file mode 100644 index 00000000..1a1124f3 --- /dev/null +++ b/tests/express-tests/test/res.status.js @@ -0,0 +1,202 @@ +'use strict' + +var express = require("express") +var request = require('supertest') + +var isIoJs = process.release + ? process.release.name === 'io.js' + : ['v1.', 'v2.', 'v3.'].indexOf(process.version.slice(0, 3)) !== -1 + +describe('res', function () { + describe('.status(code)', function () { + describe('when "code" is undefined', function () { + it('should raise error for invalid status code', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(undefined).end() + }) + + request(app) + .get('/') + .expect(500, /Invalid status code/, function (err) { + if (isIoJs) { + done(err ? null : new Error('expected error')) + } else { + done(err) + } + }) + }) + }) + + describe('when "code" is null', function () { + it('should raise error for invalid status code', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(null).end() + }) + + request(app) + .get('/') + .expect(500, /Invalid status code/, function (err) { + if (isIoJs) { + done(err ? null : new Error('expected error')) + } else { + done(err) + } + }) + }) + }) + + describe('when "code" is 201', function () { + it('should set the response status code to 201', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(201).end() + }) + + request(app) + .get('/') + .expect(201, done) + }) + }) + + describe('when "code" is 302', function () { + it('should set the response status code to 302', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(302).end() + }) + + request(app) + .get('/') + .expect(302, done) + }) + }) + + describe('when "code" is 403', function () { + it('should set the response status code to 403', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(403).end() + }) + + request(app) + .get('/') + .expect(403, done) + }) + }) + + describe('when "code" is 501', function () { + it('should set the response status code to 501', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(501).end() + }) + + request(app) + .get('/') + .expect(501, done) + }) + }) + + describe('when "code" is "410"', function () { + it('should set the response status code to 410', function (done) { + var app = express() + + app.use(function (req, res) { + res.status('410').end() + }) + + request(app) + .get('/') + .expect(410, done) + }) + }) + + describe('when "code" is 410.1', function () { + it('should set the response status code to 410', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(410.1).end() + }) + + request(app) + .get('/') + .expect(410, function (err) { + if (isIoJs) { + done(err ? null : new Error('expected error')) + } else { + done(err) + } + }) + }) + }) + + describe('when "code" is 1000', function () { + it('should raise error for invalid status code', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(1000).end() + }) + + request(app) + .get('/') + .expect(500, /Invalid status code/, function (err) { + if (isIoJs) { + done(err ? null : new Error('expected error')) + } else { + done(err) + } + }) + }) + }) + + describe('when "code" is 99', function () { + it('should raise error for invalid status code', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(99).end() + }) + + request(app) + .get('/') + .expect(500, /Invalid status code/, function (err) { + if (isIoJs) { + done(err ? null : new Error('expected error')) + } else { + done(err) + } + }) + }) + }) + + describe('when "code" is -401', function () { + it('should raise error for invalid status code', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(-401).end() + }) + + request(app) + .get('/') + .expect(500, /Invalid status code/, function (err) { + if (isIoJs) { + done(err ? null : new Error('expected error')) + } else { + done(err) + } + }) + }) + }) + }) +}) diff --git a/tests/express-tests/test/res.type.js b/tests/express-tests/test/res.type.js new file mode 100644 index 00000000..d8ac1add --- /dev/null +++ b/tests/express-tests/test/res.type.js @@ -0,0 +1,46 @@ +'use strict' + +var express = require("express") + , request = require('supertest'); + +describe('res', function(){ + describe('.type(str)', function(){ + it('should set the Content-Type based on a filename', function(done){ + var app = express(); + + app.use(function(req, res){ + res.type('foo.js').end('var name = "tj";'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/javascript; charset=utf-8') + .end(done) + }) + + it('should default to application/octet-stream', function(done){ + var app = express(); + + app.use(function(req, res){ + res.type('rawr').end('var name = "tj";'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/octet-stream', done); + }) + + it('should set the Content-Type with type/subtype', function(done){ + var app = express(); + + app.use(function(req, res){ + res.type('application/vnd.amazon.ebook') + .end('var name = "tj";'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/vnd.amazon.ebook', done); + }) + }) +}) diff --git a/tests/express-tests/test/res.vary.js b/tests/express-tests/test/res.vary.js new file mode 100644 index 00000000..f61783a0 --- /dev/null +++ b/tests/express-tests/test/res.vary.js @@ -0,0 +1,91 @@ +'use strict' + +var express = require("express"); +var request = require('supertest'); +var utils = require('./support/utils'); + +describe('res.vary()', function(){ + describe('with no arguments', function(){ + it('should not set Vary', function (done) { + var app = express(); + + app.use(function (req, res) { + res.vary(); + res.end(); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('Vary')) + .expect(200, done); + }) + }) + + describe('with an empty array', function(){ + it('should not set Vary', function (done) { + var app = express(); + + app.use(function (req, res) { + res.vary([]); + res.end(); + }); + + request(app) + .get('/') + .expect(utils.shouldNotHaveHeader('Vary')) + .expect(200, done); + }) + }) + + describe('with an array', function(){ + it('should set the values', function (done) { + var app = express(); + + app.use(function (req, res) { + res.vary(['Accept', 'Accept-Language', 'Accept-Encoding']); + res.end(); + }); + + request(app) + .get('/') + .expect('Vary', 'Accept, Accept-Language, Accept-Encoding') + .expect(200, done); + }) + }) + + describe('with a string', function(){ + it('should set the value', function (done) { + var app = express(); + + app.use(function (req, res) { + res.vary('Accept'); + res.end(); + }); + + request(app) + .get('/') + .expect('Vary', 'Accept') + .expect(200, done); + }) + }) + + describe('when the value is present', function(){ + it('should not add it again', function (done) { + var app = express(); + + app.use(function (req, res) { + res.vary('Accept'); + res.vary('Accept-Encoding'); + res.vary('Accept-Encoding'); + res.vary('Accept-Encoding'); + res.vary('Accept'); + res.end(); + }); + + request(app) + .get('/') + .expect('Vary', 'Accept, Accept-Encoding') + .expect(200, done); + }) + }) +}) diff --git a/tests/express-tests/test/support/env.js b/tests/express-tests/test/support/env.js new file mode 100644 index 00000000..000638ce --- /dev/null +++ b/tests/express-tests/test/support/env.js @@ -0,0 +1,3 @@ + +process.env.NODE_ENV = 'test'; +process.env.NO_DEPRECATION = 'body-parser,express'; diff --git a/tests/express-tests/test/support/tmpl.js b/tests/express-tests/test/support/tmpl.js new file mode 100644 index 00000000..bab65669 --- /dev/null +++ b/tests/express-tests/test/support/tmpl.js @@ -0,0 +1,36 @@ +var fs = require('fs'); + +var variableRegExp = /\$([0-9a-zA-Z\.]+)/g; + +module.exports = function renderFile(fileName, options, callback) { + function onReadFile(err, str) { + if (err) { + callback(err); + return; + } + + try { + str = str.replace(variableRegExp, generateVariableLookup(options)); + } catch (e) { + err = e; + err.name = 'RenderError' + } + + callback(err, str); + } + + fs.readFile(fileName, 'utf8', onReadFile); +}; + +function generateVariableLookup(data) { + return function variableLookup(str, path) { + var parts = path.split('.'); + var value = data; + + for (var i = 0; i < parts.length; i++) { + value = value[parts[i]]; + } + + return value; + }; +} diff --git a/tests/express-tests/test/support/utils.js b/tests/express-tests/test/support/utils.js new file mode 100644 index 00000000..5ad4ca98 --- /dev/null +++ b/tests/express-tests/test/support/utils.js @@ -0,0 +1,86 @@ + +/** + * Module dependencies. + * @private + */ + +var assert = require('assert'); +var Buffer = require('safe-buffer').Buffer + +/** + * Module exports. + * @public + */ + +exports.shouldHaveBody = shouldHaveBody +exports.shouldHaveHeader = shouldHaveHeader +exports.shouldNotHaveBody = shouldNotHaveBody +exports.shouldNotHaveHeader = shouldNotHaveHeader; +exports.shouldSkipQuery = shouldSkipQuery + +/** + * Assert that a supertest response has a specific body. + * + * @param {Buffer} buf + * @returns {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')) + } +} + +/** + * Assert that a supertest response does have a header. + * + * @param {string} header Header name to check + * @returns {function} + */ + +function shouldHaveHeader (header) { + return function (res) { + assert.ok((header.toLowerCase() in res.headers), 'should have header ' + header) + } +} + +/** + * Assert that a supertest response does not have a body. + * + * @returns {function} + */ + +function shouldNotHaveBody () { + return function (res) { + assert.ok(res.text === '' || res.text === undefined) + } +} + +/** + * Assert that a supertest response does not have a header. + * + * @param {string} header Header name to check + * @returns {function} + */ +function shouldNotHaveHeader(header) { + return function (res) { + assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header); + }; +} + +function getMajorVersion(versionString) { + return versionString.split('.')[0]; +} + +function shouldSkipQuery(versionString) { + // Skipping HTTP QUERY tests on Node 21, it is reported in http.METHODS on 21.7.2 but not supported + // update this implementation to run on supported versions of 21 once they exist + // upstream tracking https://github.com/nodejs/node/issues/51562 + // express tracking issue: https://github.com/expressjs/express/issues/5615 + return Number(getMajorVersion(versionString)) === 21 +} + diff --git a/tests/express-tests/test/utils.js b/tests/express-tests/test/utils.js new file mode 100644 index 00000000..45e93ec6 --- /dev/null +++ b/tests/express-tests/test/utils.js @@ -0,0 +1,103 @@ +// 'use strict' + +// var assert = require('assert'); +// var Buffer = require('safe-buffer').Buffer +// var utils = require('../lib/utils'); + +// describe('utils.etag(body, encoding)', function(){ +// it('should support strings', function(){ +// assert.strictEqual(utils.etag('express!'), +// '"8-O2uVAFaQ1rZvlKLT14RnuvjPIdg"') +// }) + +// it('should support utf8 strings', function(){ +// assert.strictEqual(utils.etag('express❤', 'utf8'), +// '"a-JBiXf7GyzxwcrxY4hVXUwa7tmks"') +// }) + +// it('should support buffer', function(){ +// assert.strictEqual(utils.etag(Buffer.from('express!')), +// '"8-O2uVAFaQ1rZvlKLT14RnuvjPIdg"') +// }) + +// it('should support empty string', function(){ +// assert.strictEqual(utils.etag(''), +// '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"') +// }) +// }) + +// describe('utils.setCharset(type, charset)', function () { +// it('should do anything without type', function () { +// assert.strictEqual(utils.setCharset(), undefined); +// }); + +// it('should return type if not given charset', function () { +// assert.strictEqual(utils.setCharset('text/html'), 'text/html'); +// }); + +// it('should keep charset if not given charset', function () { +// assert.strictEqual(utils.setCharset('text/html; charset=utf-8'), 'text/html; charset=utf-8'); +// }); + +// it('should set charset', function () { +// assert.strictEqual(utils.setCharset('text/html', 'utf-8'), 'text/html; charset=utf-8'); +// }); + +// it('should override charset', function () { +// assert.strictEqual(utils.setCharset('text/html; charset=iso-8859-1', 'utf-8'), 'text/html; charset=utf-8'); +// }); +// }); + +// describe('utils.wetag(body, encoding)', function(){ +// it('should support strings', function(){ +// assert.strictEqual(utils.wetag('express!'), +// 'W/"8-O2uVAFaQ1rZvlKLT14RnuvjPIdg"') +// }) + +// it('should support utf8 strings', function(){ +// assert.strictEqual(utils.wetag('express❤', 'utf8'), +// 'W/"a-JBiXf7GyzxwcrxY4hVXUwa7tmks"') +// }) + +// it('should support buffer', function(){ +// assert.strictEqual(utils.wetag(Buffer.from('express!')), +// 'W/"8-O2uVAFaQ1rZvlKLT14RnuvjPIdg"') +// }) + +// it('should support empty string', function(){ +// assert.strictEqual(utils.wetag(''), +// 'W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"') +// }) +// }) + +// describe('utils.isAbsolute()', function(){ +// it('should support windows', function(){ +// assert(utils.isAbsolute('c:\\')); +// assert(utils.isAbsolute('c:/')); +// assert(!utils.isAbsolute(':\\')); +// }) + +// it('should support windows unc', function(){ +// assert(utils.isAbsolute('\\\\foo\\bar')) +// }) + +// it('should support unices', function(){ +// assert(utils.isAbsolute('/foo/bar')); +// assert(!utils.isAbsolute('foo/bar')); +// }) +// }) + +// describe('utils.flatten(arr)', function(){ +// it('should flatten an array', function(){ +// var arr = ['one', ['two', ['three', 'four'], 'five']]; +// var flat = utils.flatten(arr) + +// assert.strictEqual(flat.length, 5) +// assert.strictEqual(flat[0], 'one') +// assert.strictEqual(flat[1], 'two') +// assert.strictEqual(flat[2], 'three') +// assert.strictEqual(flat[3], 'four') +// assert.strictEqual(flat[4], 'five') +// assert.ok(flat.every(function (v) { return typeof v === 'string' })) +// }) +// }) diff --git a/tests/index.js b/tests/index.js index 1c88bf5a..69d181ac 100644 --- a/tests/index.js +++ b/tests/index.js @@ -15,8 +15,8 @@ const testPath = path.join(__dirname, 'tests'); let testCategories = fs.readdirSync(testPath).sort((a, b) => parseInt(a) - parseInt(b)); const filterPath = process.argv[2]; -if(filterPath) { - if(!filterPath.endsWith('.js')) { +if (filterPath) { + if (!filterPath.endsWith('.js')) { testCategories = testCategories.filter(category => category.startsWith(path.basename(filterPath))); } else { testCategories = [path.dirname(filterPath).split(path.sep).pop()]; @@ -27,8 +27,8 @@ for (const testCategory of testCategories) { test(testCategory, async () => { let tests = fs.readdirSync(path.join(__dirname, 'tests', testCategory)).sort((a, b) => parseInt(a) - parseInt(b)); for (const testName of tests) { - if(filterPath && filterPath.endsWith('.js')) { - if(path.basename(testName) !== path.basename(filterPath)) { + if (filterPath && filterPath.endsWith('.js')) { + if (path.basename(testName) !== path.basename(filterPath)) { continue; } } @@ -36,7 +36,7 @@ for (const testCategory of testCategories) { let testCode = fs.readFileSync(testPath, 'utf8').replace(`const express = require("../../../src/index.js");`, 'const express = require("express");'); fs.writeFileSync(testPath, testCode); let testDescription = testCode.split('\n')[0].slice(2).trim(); - if(testDescription.endsWith('OFF')) { + if (testDescription.endsWith('OFF')) { return true; } @@ -71,4 +71,4 @@ for (const testCategory of testCategories) { }); } }); -} \ No newline at end of file +} diff --git a/tests/patch-express-tests.js b/tests/patch-express-tests.js new file mode 100644 index 00000000..ce6f7f43 --- /dev/null +++ b/tests/patch-express-tests.js @@ -0,0 +1,42 @@ +// Express tests +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const fsAsync = require('fs').promises; +// const spawnAsync = require('util').promisify(spawn); + +async function getAllJsFiles(dir) { + const entries = await fsAsync.readdir(dir, { withFileTypes: true }); + const files = await Promise.all(entries.map((entry) => { + const fullPath = path.join(dir, entry.name); + return entry.isDirectory() ? getAllJsFiles(fullPath) : fullPath; + })); + return Array.prototype.concat(...files).filter((file) => file.endsWith('.js')); +} + +const expressTestPath = path.join(__dirname, '/express-tests/'); + +// patch tests +async function runAllTests() { + const expressTestFiles = await getAllJsFiles(expressTestPath); + for (const testFile of expressTestFiles) { + let testCode = fs.readFileSync(testFile, 'utf8') + .replace(/express = require\(.*\)/, 'express = require("u-express-local")') // change to u-express-local later + .replace(/request = require\(.*\)/, 'request = require("uWSSupertest")') + + testCode = testCode + .replaceAll(`'test/fixtures`, '\'tests/express-tests/test/fixtures') + + // if files include /lib/utils, empty file + if (testCode.includes('lib/utils')) { + testCode = ''; + } + + fs.writeFileSync(testFile, testCode); + } + + // spawnAsync('node', ['--test', path.join(__dirname, 'express-tests/')], { stdio: 'inherit' }); + +} + +runAllTests().catch(console.error); \ No newline at end of file diff --git a/tests/supertest-bridge-test/usupertests.js b/tests/supertest-bridge-test/usupertests.js new file mode 100644 index 00000000..cfae82d8 --- /dev/null +++ b/tests/supertest-bridge-test/usupertests.js @@ -0,0 +1,29 @@ +const uWSSupertest = require('../uWSSupertest'); +const express = require('u-express-local'); +const app = express(); + +const after = require('after'); + +app.get('/hello', (req, res) => { + res.status(200).send('Hello World!'); +}); + +app.post('/hello', (req, res) => { + res.status(200).send('Hello World!'); +}) + +describe('GET /hello', () => { + it('should respond with Hello World', function (done) { + var cb = after(2, done) + + uWSSupertest(app) + .get('/hello') + .expect('Hello World!') + .expect(200, cb); + + uWSSupertest(app) + .post('/hello') + .expect('Hello World!') + .expect(200, cb); + }); +}); diff --git a/tests/uWSSupertest.js b/tests/uWSSupertest.js new file mode 100644 index 00000000..64cd3872 --- /dev/null +++ b/tests/uWSSupertest.js @@ -0,0 +1,45 @@ +const uWS = require('uWebSockets.js'); +const supertest = require('supertest'); +class AppWrapper { + constructor(app) { + // Get uws instance from ultimate or hyper express or plain uws + this.app = app.uwsApp || app.uws_instance || app; + this.token = null; + this.port = null; + } + + listen(port) { + this.app.listen(port, (token) => { + if (!token) { + throw new Error('Failed to start uWS.js app'); + } + this.token = token; + // This cb is invoked immediately so we can assume the port is stored + this.port = uWS.us_socket_local_port(token); + }); + + return { + _handle: true, + close: this.close.bind(this), + } + } + + close(fn) { + if (this.token) { + uWS.us_listen_socket_close(this.token); + this.token = null; + this.port = null; + } + fn?.(); + } + + address() { + return this.port ? { port: this.port } : null; + } +} + +function uWSSupertest(app) { + return supertest(new AppWrapper(app)) +} + +module.exports = uWSSupertest; \ No newline at end of file