Compare commits

...

28 Commits

Author SHA1 Message Date
Jonatan Nilsson d5459cbcb9 cors: Add specific support for supporting all origin
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-11-15 09:56:34 +00:00
Jonatan Nilsson 01a916eb2d socket: Set time out and forcibly close timed out sockets. Fix tests for different node versions
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-11-03 22:52:09 +00:00
Jonatan Nilsson 598548d97b random generator benchmark
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-10-07 11:49:41 +00:00
TheThing 8a49e38285 Update 'README.md'
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-09-28 10:39:12 +00:00
Jonatan Nilsson 6d4d62e79c test: Add test for buffer support
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-05-11 11:12:23 +00:00
Jonatan Nilsson 7401b3bd2c requestEnd: Add proper support for buffers in body
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-05-11 09:50:52 +00:00
Jonatan Nilsson 95e6c2dcac Test: Update tests on formidable errors
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-01-26 09:26:12 +00:00
Jonatan Nilsson 74771d92cf Formidable: Now return 400 BadRequest HttpError instead of generic error.
continuous-integration/appveyor/branch AppVeyor build failed Details
2023-01-26 09:23:10 +00:00
Jonatan Nilsson 2b69013c04 remove .only()
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-08-10 14:29:51 +00:00
Jonatan Nilsson 8a56969015 Fix FormidableHandler so it detects filetype based on extension if type is unknown or application octet-stream.
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-08-10 14:13:14 +00:00
Jonatan Nilsson 5f916e97ea Formidable: Better handling for file uploads. Now supports multiple keys
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-07-06 14:50:54 +00:00
Jonatan Nilsson baf2d896c1 Update eltro, clean up a few tests
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-07-04 13:24:19 +00:00
Jonatan Nilsson e9c600b869 Flaska: Add support for `appendHeaders` in constructor.
continuous-integration/appveyor/branch AppVeyor build succeeded Details
* Allows to append individual headers to the defaultHeaders without completely
  replacing the default values.
2022-06-16 09:59:30 +00:00
Jonatan Nilsson 568c620782 Flaska: Add support for appendHeaders to compliment default headers instead of completely replacing them 2022-06-16 09:58:11 +00:00
Jonatan Nilsson 0c22fe9577 Final fix unit tests
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-05-12 16:44:37 +00:00
Jonatan Nilsson 3a0064c563 Fix test for new default header
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-05-12 16:42:54 +00:00
Jonatan Nilsson e7909cc84b flaska: Add better default font-src with self and data: support.
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-05-12 16:40:14 +00:00
Jonatan Nilsson 4820347cfb req: Fix bug where it would call requestEnded when request was closed. This behavior is normal during the middle of a request
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-04-07 11:27:44 +00:00
Jonatan Nilsson cffca53eb6 test: Fix tests that were relying on aborted log message. These are spammy and normal and shouldn't be logged
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-04-02 19:47:39 +00:00
Jonatan Nilsson 3a454d44ce onreqerror: Do not log aborted "errors"
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-04-02 19:40:46 +00:00
Jonatan Nilsson 975646d336 Flaska: If http server supports listenAsync (service-core for example) then use that instead of callback listen()
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-27 14:48:11 +00:00
Jonatan Nilsson b97e34c1eb Flaska: HEAD request support across the board.
continuous-integration/appveyor/branch AppVeyor build succeeded Details
Flaska: Set status to 204 if status is 200 and body is null
Flaska: Fix implementation of how content-type is set
FileResponse: Don't open file if running HEAD request
2022-03-27 00:30:56 +00:00
Jonatan Nilsson d4bac4940e FileResponse: Pre-build range regex and re-use it for performance benefits
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-26 15:59:48 +00:00
Jonatan Nilsson 499cfa8ce0 FileResponse: Completely finished all HTTP/1.1 support for file handling including range, preconditions, modified and so much more.
continuous-integration/appveyor/branch AppVeyor build succeeded Details
Flaska: Refactored how headers and status are sent.
2022-03-26 15:50:18 +00:00
Jonatan Nilsson cfd56f83ae requestEnd: Now properly supports ctx.headers and properly sends all headers to res with status code using writeHead
continuous-integration/appveyor/branch AppVeyor build succeeded Details
FileResponse: Initial development started to serve files
2022-03-25 13:26:13 +00:00
Jonatan Nilsson acb099868b CorsHandler: Finished implementing a full CORS support
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-24 15:50:31 +00:00
Jonatan Nilsson 7b682e8e95 Constructor: Support for defaultHeaders was added with default secure options.
continuous-integration/appveyor/branch AppVeyor build succeeded Details
CSP: Added smart CSP support with nonce support as well. Can generate unique nonce values for each request.
CorsHandler: Started development of basic cors handler.
2022-03-24 09:29:54 +00:00
Jonatan Nilsson e50e9f8a94 Flaska: Add basic head support
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-21 07:34:01 +00:00
18 changed files with 3579 additions and 549 deletions

View File

@ -126,9 +126,9 @@ flaska.get('/api/test', function(ctx) {
})
```
### File stream/pipe
### pipe
In cases where the response body is a pipe object (detected from the existance of `.pipe` property), flaska will automatically pipe the file for you. In addition, if a file stream is used, it will read the extension of the file being streamed and automatically fill in the mime-type for you in the `Content-Type` header.
In cases where the response body is a pipe object (detected from the existance of `.pipe` property), flaska will automatically pipe it for you. In addition, if a file stream is used, it will read the extension of the file being streamed and automatically fill in the mime-type for you in the `Content-Type` header.
```
flaska.get('/test.png', function(ctx) {
@ -138,6 +138,22 @@ flaska.get('/test.png', function(ctx) {
Flaska will automatically close the file stream for you so you don't have to worry about that.
### FileResponse
Alternatively, if you want proper file support, I recommend using FileResponse object:
```
import { FileResponse } from '../flaska.mjs'
flaska.get('/test.txt', function(ctx) {
return fs.stat('./test/test.txt').then(function(stat) {
ctx.body = new FileResponse('./test/test.txt', stat)
})
})
```
This performs a real file stream support, uses pipes and supports all the HTTP shenanigans when it comes to dealing with files, including sending proper etag header, supporting partial response and lots of other things. This is one of the few libraries that actually implements full HTTP partial and etag support in a proper way, almost all other have one or two quirks that don't follow the spec properly.
### String
In other instances, Flaska will `.toString()` the body and send it in response with the specified type or default to `text/plain` if unspecified.

View File

@ -1,2 +1 @@
start /B /WAIT /REALTIME node index.js
pause
start /B /WAIT /REALTIME node index.js

View File

@ -1,3 +1,4 @@
import crypto from 'crypto'
import assert from 'assert'
import Benchmark from 'benchmarkjs-pretty'
import { koaRouter1, koaRouter2 } from './router_koa.js'
@ -176,7 +177,89 @@ function TestObjectAssign() {
ctx.router2 = flaskaRouter2
}
function registerHeader(ctx) {
ctx.headers['Server'] = 'nginx/1.16.1'
ctx.headers['Date'] = 'Mon, 21 Mar 2022 07:26:01 GMT'
ctx.headers['Content-Type'] = 'application/json; charset=utf-8'
ctx.headers['Content-Length'] = '1646'
ctx.headers['Connection'] = 'keep-alive'
ctx.headers['vary'] = 'Origin'
ctx.headers['Link'] = '<http://kisildalur.is/api/categories?perPage=1250>; rel="current"; title="Page 1"'
ctx.headers['pagination_total'] = '7'
ctx.headers['X-Frame-Options'] = 'DENY'
ctx.headers['X-Content-Type-Options'] = 'nosniff'
}
function registerHeaderAlt(ctx) {
ctx.headers = {
'Server': 'nginx/1.16.1',
'Date': 'Mon, 21 Mar 2022 07:26:01 GMT',
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': '1646',
'Connection': 'keep-alive',
'vary': 'Origin',
'Link': '<http://kisildalur.is/api/categories?perPage=1250>; rel="current"; title="Page 1"',
'pagination_total': '7',
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
}
}
let baseHeaders = {
'Server': 'nginx/1.16.1',
'Date': 'Mon, 21 Mar 2022 07:26:01 GMT',
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': '1646',
'Connection': 'keep-alive',
'vary': 'Origin',
'Link': '<http://kisildalur.is/api/categories?perPage=1250>; rel="current"; title="Page 1"',
'pagination_total': '7',
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
}
let keys = Object.keys(baseHeaders)
return new Benchmark.default('test different method to initialize objects)')
.add('[HEADERS] Object.assign()', function() {
let ctx = {
headers: {}
}
Object.assign(ctx.headers, baseHeaders)
// assert.notStrictEqual(ctx.headers, baseHeaders)
})
.add('[HEADERS] ecmascript spread', function() {
let ctx = {
headers: {
...baseHeaders
}
}
// assert.notStrictEqual(ctx.headers, baseHeaders)
})
.add('[HEADERS] Basic clone lol', function() {
let ctx = {
headers: {}
}
for (let key of keys) {
ctx.headers[key] = baseHeaders[key]
}
// assert.notStrictEqual(ctx.headers, baseHeaders)
})
.add('[HEADERS] Use register function', function() {
let ctx = {
headers: {}
}
registerHeader(ctx)
// assert.notStrictEqual(ctx.headers, baseHeaders)
})
.add('[HEADERS] Use register function ALT', function() {
let ctx = {
headers: {}
}
registerHeaderAlt(ctx)
// assert.notStrictEqual(ctx.headers, baseHeaders)
})
/*
.add('Object.assign()', function() {
let ctx = {
req: req,
@ -196,7 +279,7 @@ function TestObjectAssign() {
register3(ctx)
ctx.log.info()
})
/*.add('Object.create() all props', function() {
.add('Object.create() all props', function() {
let ctx = Object.create({}, propMakerAlt)
ctx.log.info()
})
@ -212,7 +295,100 @@ function TestObjectAssign() {
let ctx = { }
Object.defineProperties(ctx, propMakerAlt)
ctx.log.info()
})*/
})
*/
.run()
.then(function() {}, function(e) {
console.error('error:', e)
process.exit(1)
})
}
function TestGenerateRandomString() {
return new Benchmark.default('test different method to generate random string)')
.add('crypto.randomBytes(16)', function() {
for (let i = 0; i < 25; i++) {
crypto.randomBytes(16).toString('base64')
}
})
.add('crypto.randomBytes(32)', function() {
for (let i = 0; i < 25; i++) {
crypto.randomBytes(32).toString('base64')
}
})
.add('random (22 characters long)', function() {
for (let i = 0; i < 25; i++) {
let out = Math.random().toString(36).substring(2, 24)
+ Math.random().toString(36).substring(2, 24)
}
})
.add('random (44 characters long)', function() {
for (let i = 0; i < 25; i++) {
let out = Math.random().toString(36).substring(2, 24)
+ Math.random().toString(36).substring(2, 24)
+ Math.random().toString(36).substring(2, 24)
+ Math.random().toString(36).substring(2, 24)
}
})
.run()
.then(function() {}, function(e) {
console.error('error:', e)
process.exit(1)
})
}
function TestArrayReduce() {
return new Benchmark.default('test different method to reduce array)')
.add('currIndex', function() {
const arr1 = new Array(100)
for (let i = 0; i < arr1.length; i++) {
arr1[i] = 'a'
}
let currIndex = arr1.length - 1
let out = ''
while (currIndex >= 0) {
out += arr1[currIndex]
currIndex--
}
})
.add('crypto.randomBytes(32)', function() {
const arr1 = new Array(100)
for (let i = 0; i < arr1.length; i++) {
arr1[i] = 'a'
}
let out = ''
while (arr1.length > 0) {
out += arr1.splice(0, 1)[0]
}
})
.run()
.then(function() {}, function(e) {
console.error('error:', e)
process.exit(1)
})
}
function TestStringCombination() {
let val1 = 'some'
let val2 = 'text'
let val3 = 'gose'
let val4 = 'here'
return new Benchmark.default('test different method to combine string)')
.add('ES6 with variable', function() {
let out = `Hello my friend ${val1} this goes to ${val2} all my homies ${val3} over at my ${val4} house`
return out
})
.add('String concatenation', function() {
let out = 'Hello my friend ' + val1 + ' this goes to ' + val2 + ' all my homies ' + val3 + ' over at my ' + val4 + ' house'
return out
})
.run()
.then(function() {}, function(e) {
console.error('error:', e)
@ -345,8 +521,8 @@ function TestLargeParamLargeUrlRoute() {
})
}
/*
TestSmallStaticRoute()
// TestObjectAssign()
// TestPromiseCreators()
.then(function() {
return TestSmallParamRoute()
@ -362,4 +538,12 @@ TestSmallStaticRoute()
})
.then(function() {
process.exit(0)
})
})*/
// TestObjectAssign()
// TestGenerateRandomString()
// TestArrayReduce()
TestStringCombination()
.then(function() {
process.exit(0)
})

View File

@ -1,8 +1,731 @@
{
"name": "benchmark",
"version": "1.0.0",
"lockfileVersion": 1,
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "benchmark",
"version": "1.0.0",
"license": "WTFPL",
"dependencies": {
"benchmarkjs-pretty": "^2.0.0",
"express": "^4.17.1",
"koa-router": "^8.0.8"
}
},
"node_modules/@types/benchmark": {
"version": "1.0.31",
"resolved": "https://registry.npmjs.org/@types/benchmark/-/benchmark-1.0.31.tgz",
"integrity": "sha512-F6fVNOkGEkSdo/19yWYOwVKGvzbTeWkR/XQYBKtGBQ9oGRjBN9f/L4aJI4sDcVPJO58Y1CJZN8va9V2BhrZapA=="
},
"node_modules/@types/chalk": {
"version": "0.4.31",
"resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-0.4.31.tgz",
"integrity": "sha1-ox10JBprHtu5c8822XooloNKUfk="
},
"node_modules/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"dependencies": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"node_modules/benchmark": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
"integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=",
"dependencies": {
"lodash": "^4.17.4",
"platform": "^1.3.3"
}
},
"node_modules/benchmarkjs-pretty": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/benchmarkjs-pretty/-/benchmarkjs-pretty-2.0.0.tgz",
"integrity": "sha512-t5a+ztdAuim1HPEbQwBELN9ugqe5WCSbjwPh79olqS+zgw44Bi/3qPz472LNPIfXlTernAo+meL8KULaXuWAeQ==",
"dependencies": {
"@types/benchmark": "^1.0.30",
"@types/chalk": "^0.4.31",
"benchmark": "^2.1.4",
"chalk": "^2.0.1"
}
},
"node_modules/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"dependencies": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/body-parser/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/body-parser/node_modules/http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/body-parser/node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"node_modules/content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"node_modules/debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"dependencies": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/express/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/finalhandler/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"engines": {
"node": ">=4"
}
},
"node_modules/http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"node_modules/koa-compose": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz",
"integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw=="
},
"node_modules/koa-router": {
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/koa-router/-/koa-router-8.0.8.tgz",
"integrity": "sha512-2rNF2cgu/EWi/NV8GlBE5+H/QBoaof83X6Z0dULmalkbt7W610/lyP2EOLVqVrUUFfjsVWL/Ju5TVBcGJDY9XQ==",
"dependencies": {
"debug": "^4.1.1",
"http-errors": "^1.7.3",
"koa-compose": "^4.1.0",
"methods": "^1.1.2",
"path-to-regexp": "1.x",
"urijs": "^1.19.2"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/koa-router/node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
"dependencies": {
"mime-db": "1.43.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"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": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"node_modules/platform": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz",
"integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q=="
},
"node_modules/proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"dependencies": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"dependencies": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/raw-body/node_modules/http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body/node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"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=="
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"dependencies": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/send/node_modules/debug/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/send/node_modules/ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"node_modules/serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/urijs": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.2.tgz",
"integrity": "sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w=="
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"engines": {
"node": ">= 0.8"
}
}
},
"dependencies": {
"@types/benchmark": {
"version": "1.0.31",

46
benchmark/random.js Normal file
View File

@ -0,0 +1,46 @@
import crypto from 'crypto'
import Benchmark from 'benchmarkjs-pretty'
function TestGenerateRandomString() {
return new Benchmark.default('test different method to generate random string)')
.add('crypto.randomBytes(16)', function() {
for (let i = 0; i < 25; i++) {
crypto.randomBytes(16).toString('base64')
}
})
.add('crypto.randomBytes(32)', function() {
for (let i = 0; i < 25; i++) {
crypto.randomBytes(32).toString('base64')
}
})
.add('random (11 characters long)', function() {
for (let i = 0; i < 25; i++) {
let out = Math.random().toString(36).substring(2, 14)
}
})
.add('random (22 characters long)', function() {
for (let i = 0; i < 25; i++) {
let out = Math.random().toString(36).substring(2, 24)
+ Math.random().toString(36).substring(2, 24)
}
})
.add('random (44 characters long)', function() {
for (let i = 0; i < 25; i++) {
let out = Math.random().toString(36).substring(2, 24)
+ Math.random().toString(36).substring(2, 24)
+ Math.random().toString(36).substring(2, 24)
+ Math.random().toString(36).substring(2, 24)
}
})
.run()
.then(function() {}, function(e) {
console.error('error:', e)
process.exit(1)
})
}
TestGenerateRandomString()
.then(function() {
process.exit(0)
})

View File

@ -1,8 +1,10 @@
import os from 'os'
import crypto from 'crypto'
import path from 'path'
import http from 'http'
import stream from 'stream'
import fs from 'fs/promises'
import fsSync from 'fs'
import { URL } from 'url'
import { Buffer } from 'buffer'
import zlib from 'zlib'
@ -128,6 +130,66 @@ export function JsonHandler(org = {}) {
}
}
export function CorsHandler(opts = {}) {
const options = {
allowedMethods: opts.allowedMethods || 'GET,HEAD,PUT,POST,DELETE,PATCH',
allowedOrigins: opts.allowedOrigins || [],
allowedHeaders: opts.allowedHeaders,
credentials: opts.credentials || false,
exposeHeaders: opts.exposeHeaders || '',
maxAge: opts.maxAge || '',
}
const allowAll = options.allowedOrigins.includes('*')
return function(ctx) {
// Always add vary header on origin. Prevent caches from
// accidentally caching wrong preflight request
ctx.headers['Vary'] = 'Origin'
// Set status to 204 if OPTIONS. Just handy for flaska and
// other checking.
if (ctx.method === 'OPTIONS') {
ctx.status = 204
}
// Check origin is specified. Nothing needs to be done if
// there is no origin or it doesn't match
let origin = ctx.req.headers['origin']
if (!origin || (!allowAll && !options.allowedOrigins.includes(origin))) {
return
}
// Set some extra headers if this is a pre-flight. Most of
// these are not needed during a normal request.
if (ctx.method === 'OPTIONS') {
if (!ctx.req.headers['access-control-request-method']) {
return
}
if (options.maxAge) {
ctx.headers['Access-Control-Max-Age'] = options.maxAge
}
let reqHeaders = options.allowedHeaders
|| ctx.req.headers['access-control-request-headers']
if (reqHeaders && options.allowedHeaders !== false) {
ctx.headers['Access-Control-Allow-Headers'] = reqHeaders
}
ctx.headers['Access-Control-Allow-Methods'] = options.allowedMethods
} else {
if (options.exposeHeaders) {
ctx.headers['Access-Control-Expose-Headers'] = options.exposeHeaders
}
}
ctx.headers['Access-Control-Allow-Origin'] = origin
if (options.credentials) {
ctx.headers['Access-Control-Allow-Credentials'] = 'true'
}
}
}
export function FormidableHandler(formidable, org = {}) {
let lastDateString = ''
let incrementor = 1
@ -176,7 +238,7 @@ export function FormidableHandler(formidable, org = {}) {
return new Promise(function(res, rej) {
form.parse(ctx.req, function(err, fields, files) {
if (err) return rej(err)
if (err) return rej(new HttpError(400, err.message))
if (opts.parseFields) {
Object.keys(fields).forEach(function(key) {
@ -187,29 +249,45 @@ export function FormidableHandler(formidable, org = {}) {
}
ctx.req.body = fields
ctx.req.file = files?.file || null
ctx.req.files = files
ctx.req.file = null
if (!ctx.req.file) {
if (!ctx.req.files) {
return res()
}
let filename
let target
let keys = Object.keys(files).filter(key => Boolean(ctx.req.files[key]))
try {
filename = opts.filename(ctx.req.file) || ctx.req.file.name
target = path.join(opts.uploadDir, filename)
} catch (err) {
return rej(err)
}
rename(ctx.req.file.path, target)
.then(function() {
ctx.req.file.path = target
ctx.req.file.filename = filename
})
.then(res, rej)
Promise.all(
keys.map(key => {
let filename
let target
try {
filename = opts.filename(ctx.req.files[key]) || ctx.req.files[key].name
target = path.join(opts.uploadDir, filename)
} catch (err) {
return Promise.reject(err)
}
return rename(ctx.req.files[key].path, target)
.then(function() {
if (!ctx.req.files[key].type || ctx.req.files[key].type === 'application/octet-stream') {
let found = MimeTypeDb[path.extname(filename).slice(1)]
ctx.req.files[key].type = found && found[0] || 'application/octet-stream'
}
ctx.req.files[key].path = target
ctx.req.files[key].filename = filename
})
})
)
.then(() => {
if (keys.length === 1 && keys[0] === 'file') {
ctx.req.file = ctx.req.files.file
}
res()
}, rej)
})
})
}
@ -217,15 +295,111 @@ export function FormidableHandler(formidable, org = {}) {
export class HttpError extends Error {
constructor(statusCode, message, body = null) {
super(message);
super(message);
Error.captureStackTrace(this, HttpError);
Error.captureStackTrace(this, HttpError);
let proto = Object.getPrototypeOf(this);
proto.name = 'HttpError';
let proto = Object.getPrototypeOf(this);
proto.name = 'HttpError';
this.status = statusCode
this.body = body
this.status = statusCode
this.body = body
}
}
const RangeRegexTester = /bytes=(\d+)-(\d+)?/
export class FileResponse {
constructor(filepath, stat) {
this.filepath = filepath
this.stat = stat
}
handleRequest(ctx, useFs = fsSync) {
let etag = '"' + this.stat.ino + '-' + this.stat.size + '-' + this.stat.mtime.getTime() + '"'
let lastModified = this.stat.mtime.toUTCString()
let lastModifiedRounded = Date.parse(lastModified)
if (ctx.req.headers['if-match'] && ctx.req.headers['if-match'] !== etag) {
throw new HttpError(412, `Request if-match pre-condition failed`)
}
if (ctx.req.headers['if-unmodified-since']) {
let check = Date.parse(ctx.req.headers['if-unmodified-since'])
if (!check || check < lastModifiedRounded) {
throw new HttpError(412, `Request if-unmodified-since pre-condition failed`)
}
}
ctx.headers['Etag'] = etag
if (ctx.req.headers['if-none-match']) {
let split = ctx.req.headers['if-none-match'].split(',')
for (let check of split) {
if (check.trim() === etag) {
ctx.status = 304
return null
}
}
} else if (ctx.req.headers['if-modified-since']) {
let check = Date.parse(ctx.req.headers['if-modified-since'])
if (check >= lastModifiedRounded) {
ctx.status = 304
return null
}
}
let readOptions = {}
let size = this.stat.size
if (ctx.req.headers['range']) {
let match = RangeRegexTester.exec(ctx.req.headers['range'])
let ifRange = ctx.req.headers['if-range']
if (ifRange) {
if (ifRange[0] === '"' && ifRange !== etag) {
match = null
} else if (ifRange[0] !== '"') {
let check = Date.parse(ifRange)
if (!check || check < lastModifiedRounded) {
match = null
}
}
}
if (match) {
let start = Number(match[1])
let end = size - 1
if (match[2]) {
end = Math.min(Number(match[2]), size - 1)
}
if (start >= size) {
throw new HttpError(416, `Out of range start ${start} outside of ${size} bounds`)
}
if (start <= end) {
size = end - start + 1
readOptions.start = start
readOptions.end = end
ctx.headers['Content-Range'] = start + '-' + end + '/' + this.stat.size
ctx.status = 206
}
}
}
let ext = path.extname(this.filepath).slice(1)
let found = MimeTypeDb[ext]
if (found) {
ctx.type = found[found.length - 1]
}
ctx.headers['Last-Modified'] = lastModified
ctx.headers['Content-Length'] = size
if (ctx.method !== 'HEAD') {
let stream = useFs.createReadStream(this.filepath, readOptions)
return stream
}
return null
}
}
@ -420,7 +594,7 @@ export class FlaskaRouter {
* Flaska
*/
export class Flaska {
constructor(opts, orgHttp = http, orgStream = stream) {
constructor(opts = {}, orgHttp = http, orgStream = stream) {
this._before = []
this._beforeCompiled = null
this._beforeAsync = []
@ -430,10 +604,12 @@ export class Flaska {
this._afterAsync = []
this._afterAsyncCompiled = null
this._on404 = function(ctx) {
ctx.status = 404
ctx.body = {
status: 404,
message: statuses[404],
if (ctx.body == null && ctx.status !== 204) {
ctx.status = 404
ctx.body = {
status: 404,
message: statuses[404],
}
}
}
this._backuperror = this._onerror = function(err, ctx) {
@ -453,29 +629,109 @@ export class Flaska {
}
}
this._onreqerror = function(err, ctx) {
if (err.message === 'aborted') {
ctx.log.info(err)
} else {
if (err.message !== 'aborted') {
ctx.log.error(err)
ctx.res.statusCode = ctx.statusCode = 400
}
ctx.res.statusCode = ctx.statusCode = 400
ctx.res.end()
}
this._onreserror = function(err, ctx) {
ctx.log.error(err)
}
let options = opts || {}
this.log = options.log || {
fatal: console.error.bind(console),
error: console.error.bind(console),
warn: console.log.bind(console),
info: console.log.bind(console),
debug: console.debug.bind(console),
trace: console.debug.bind(console),
log: console.log.bind(console),
let options = {
defaultHeaders: opts.defaultHeaders || {
'Server': 'Flaska',
'X-Content-Type-Options': 'nosniff',
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'`,
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
log: opts.log || {
fatal: console.error.bind(console),
error: console.error.bind(console),
warn: console.log.bind(console),
info: console.log.bind(console),
debug: console.debug.bind(console),
trace: console.debug.bind(console),
log: console.log.bind(console),
},
nonce: opts.nonce || [],
nonceCacheLength: opts.nonceCacheLength || 25
}
if (opts.appendHeaders) {
let appendKeys = Object.keys(opts.appendHeaders)
for (let key of appendKeys) {
options.defaultHeaders[key] = opts.appendHeaders[key]
}
}
if (!options.defaultHeaders && options.nonce.length) {
// throw error
}
let headerKeys = Object.keys(options.defaultHeaders)
let constructFunction = ''
if (options.nonce.length) {
this._nonces = new Array(options.nonceCacheLength)
this._noncesIndex = this._nonces.length - 1
for (let i = 0; i < this._nonces.length; i++) {
this._nonces[i] = crypto.randomBytes(16).toString('base64')
}
constructFunction += `
let nonce = this._nonces[this._noncesIndex] || crypto.randomBytes(16).toString('base64');
this._noncesIndex--;
ctx.state.nonce = nonce;
`
}
constructFunction += 'ctx.headers = {'
constructFunction += `'Date': new Date().toUTCString(),`
for (let key of headerKeys) {
if (key === 'Content-Security-Policy' && options.nonce.length) {
let groups = options.defaultHeaders[key].split(';')
for (let ni = 0; ni < options.nonce.length; ni++) {
let found = false
for (let x = 0; x < groups.length; x++) {
if (groups[x].trim().startsWith(options.nonce[ni])) {
groups[x] = groups[x].trimEnd() + ` 'nonce-$'`
found = true
break
}
}
if (!found) {
groups.push(` ${options.nonce[ni]} 'nonce-$'`)
}
}
groups = groups.join(';').replace(/\'/g, "\\'").split('$')
constructFunction += `'${key}': '${groups.join(`' + nonce + '`)}',`
} else {
constructFunction += `'${key}': '${options.defaultHeaders[key].replace(/\'/g, "\\'")}',`
}
}
constructFunction += '};'
// console.log(constructFunction)
if (options.nonce.length) {
this.before(new Function('crypto', 'ctx', constructFunction).bind(this, crypto))
this.after(new Function('crypto', 'ctx', `
this._noncesIndex = Math.max(this._noncesIndex, -1);
if (this._noncesIndex < this._nonces.length - 1) {
this._noncesIndex++;
this._nonces[this._noncesIndex] = crypto.randomBytes(16).toString('base64');
}
`).bind(this, crypto))
} else {
this.before(new Function('ctx', constructFunction).bind(this))
}
this.log = options.log
this.http = orgHttp
this.stream = orgStream
this.server = null
@ -487,6 +743,9 @@ export class Flaska {
'OPTIONS': new FlaskaRouter(),
'PATCH': new FlaskaRouter(),
}
// HEAD and GET should be identical
this.routers['HEAD'] = this.routers['GET']
this.get = this.routers.GET.addRoute.bind(this.routers.GET)
this.post = this.routers.POST.addRoute.bind(this.routers.POST)
this.put = this.routers.PUT.addRoute.bind(this.routers.PUT)
@ -598,11 +857,6 @@ export class Flaska {
this._onreserror(err, ctx)
})
req.on('close', () => {
ctx.closed = true
this.requestEnded(ctx)
})
res.on('finish', () => {
this.requestEnded(ctx)
})
@ -676,7 +930,16 @@ export class Flaska {
}
}
requestEnd(err, ctx) {
requestEnd(orgErr, ctx) {
let err = orgErr
let handleUsed = Boolean(ctx.body && ctx.body.handleRequest)
if (handleUsed) {
try {
ctx.body = ctx.body.handleRequest(ctx)
} catch (newErr) {
err = newErr
}
}
if (err) {
try {
this._onerror(err, ctx)
@ -689,13 +952,18 @@ export class Flaska {
return
}
ctx.res.statusCode = ctx.status
if (ctx.body === null && !handleUsed && ctx.status === 200) {
ctx.status = 204
}
if (statuses.empty[ctx.status]) {
ctx.res.writeHead(ctx.status, ctx.headers)
return ctx.res.end()
}
let body = ctx.body
let length = 0
// Special handling for files
if (body && typeof(body.pipe) === 'function') {
// Be smart when handling file handles, auto detect mime-type
// based off of the extension of the file.
@ -706,21 +974,48 @@ export class Flaska {
ctx.type = found[found.length - 1]
}
}
ctx.res.setHeader('Content-Type', ctx.type || 'application/octet-stream')
return this.stream.pipeline(body, ctx.res, function() { })
ctx.headers['Content-Type'] = ctx.type || 'application/octet-stream'
ctx.res.writeHead(ctx.status, ctx.headers)
if (ctx.method !== 'HEAD') {
return this.stream.pipeline(body, ctx.res, function() { })
} else {
try {
body.destroy()
} catch { }
return ctx.res.end()
}
}
if (typeof(body) === 'object') {
let length = 0
if (body instanceof Buffer) {
length = body.byteLength
ctx.type = ctx.type || 'application/octet-stream'
} else if (typeof(body) === 'object' && body) {
body = JSON.stringify(body)
length = Buffer.byteLength(body)
ctx.res.setHeader('Content-Type', 'application/json; charset=utf-8')
} else {
ctx.type = 'application/json; charset=utf-8'
} else if (body) {
body = body.toString()
length = Buffer.byteLength(body)
ctx.res.setHeader('Content-Type', ctx.type || 'text/plain; charset=utf-8')
ctx.type = ctx.type || 'text/plain; charset=utf-8'
}
if (ctx.type) {
ctx.headers['Content-Type'] = ctx.type
}
if (!ctx.headers['Content-Length']) {
ctx.headers['Content-Length'] = length
}
ctx.res.writeHead(ctx.status, ctx.headers)
if (body && ctx.method !== 'HEAD') {
ctx.res.end(body)
} else {
ctx.res.end()
}
ctx.res.setHeader('Content-Length', length)
ctx.res.end(body)
}
requestEnded(ctx) {
@ -767,6 +1062,22 @@ export class Flaska {
}
}
create() {
this.compile()
this.server = this.http.createServer(this.requestStart.bind(this))
this.server.on('connection', function (socket) {
// Set socket idle timeout in milliseconds
socket.setTimeout(1000 * 60 * 5) // 5 minutes
// Wait for timeout event (socket will emit it when idle timeout elapses)
socket.on('timeout', function () {
// Call destroy again
socket.destroy();
})
})
}
listen(port, orgIp, orgcb) {
let ip = orgIp
let cb = orgcb
@ -777,8 +1088,8 @@ export class Flaska {
if (typeof(port) !== 'number') {
throw new Error('Flaska.listen() called with non-number in port')
}
this.compile()
this.server = this.http.createServer(this.requestStart.bind(this))
this.create()
this.server.listen(port, ip, cb)
}
@ -788,8 +1099,11 @@ export class Flaska {
return Promise.reject(new Error('Flaska.listen() called with non-number in port'))
}
this.compile()
this.server = this.http.createServer(this.requestStart.bind(this))
this.create()
if (this.server.listenAsync && typeof(this.server.listenAsync) === 'function') {
return this.server.listenAsync(port, ip)
}
return new Promise((res, rej) => {
this.server.listen(port, ip, function(err) {

View File

@ -1,6 +1,6 @@
{
"name": "flaska",
"version": "1.0.0",
"version": "1.3.5",
"description": "Flaska is a micro web-framework for node. It is designed to be fast, simple and lightweight, and is distributed as a single file module with no dependencies.",
"main": "flaska.mjs",
"scripts": {
@ -39,7 +39,7 @@
},
"homepage": "https://git.nfp.is/TheThing/flaska/#readme",
"devDependencies": {
"eltro": "^1.2.3",
"eltro": "^1.3.2",
"formidable": "^1.2.2"
},
"files": [

View File

@ -29,6 +29,12 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options =
headers: {},
}))
if (options.agent) {
opts.agent = options.agent
}
// opts.agent = agent
const req = http.request(opts)
req.on('error', (err) => {
@ -56,13 +62,21 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options =
}
res.on('end', function () {
if (!output) return resolve(null)
try {
output = JSON.parse(output)
} catch (e) {
return reject(new Error(`${e.message} while decoding: ${output}`))
if (options.getRaw) {
output = {
status: res.statusCode,
data: output,
headers: res.headers,
}
} else {
if (!output) return resolve(null)
try {
output = JSON.parse(output)
} catch (e) {
return reject(new Error(`${e.message} while decoding: ${output}`))
}
}
if (output.status && typeof(output.status) === 'number') {
if (!options.getRaw && output.status && typeof(output.status) === 'number') {
let err = new Error(`Request failed [${output.status}]: ${output.message}`)
err.body = output
return reject(err)
@ -112,34 +126,54 @@ const random = (length = 8) => {
return str;
}
Client.prototype.upload = function(url, file, method = 'POST', body = {}) {
return fs.readFile(file).then(data => {
const crlf = '\r\n'
Client.prototype.upload = function(url, files, method = 'POST', body = {}, overrideType = null) {
const boundary = `---------${random(32)}`
const crlf = '\r\n'
let upload = files
if (typeof(upload) === 'string') {
upload = {
file: files
}
}
let keys = Object.keys(upload)
let uploadBody = []
return Promise.all(keys.map(key => {
let file = upload[key]
return fs.readFile(file).then(data => {
const filename = path.basename(file)
const boundary = `---------${random(32)}`
const multipartBody = Buffer.concat([
Buffer.from(
`${crlf}--${boundary}${crlf}` +
`Content-Disposition: form-data; name="file"; filename="${filename}"` + crlf + crlf
),
data,
Buffer.concat(Object.keys(body).map(function(key) {
return Buffer.from(''
+ `${crlf}--${boundary}${crlf}`
+ `Content-Disposition: form-data; name="${key}"` + crlf + crlf
+ JSON.stringify(body[key])
)
})),
Buffer.from(`${crlf}--${boundary}--`),
])
uploadBody.push(Buffer.from(
`${crlf}--${boundary}${crlf}`
+ `Content-Disposition: form-data; name="${key}"; filename="${filename}"`
+ (overrideType ? crlf + `Content-Type: ${overrideType}`: '')
+ crlf
+ crlf
))
uploadBody.push(data)
})
}))
.then(() => {
uploadBody.push(
Buffer.concat(Object.keys(body).map(function(key) {
return Buffer.from(''
+ `${crlf}--${boundary}${crlf}`
+ `Content-Disposition: form-data; name="${key}"` + crlf + crlf
+ JSON.stringify(body[key])
)
}))
)
uploadBody.push(Buffer.from(`${crlf}--${boundary}--`))
return this.customRequest(method, url, multipartBody, {
timeout: 5000,
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': multipartBody.length,
},
})
let multipartBody = Buffer.concat(uploadBody)
return this.customRequest(method, url, multipartBody, {
timeout: 5000,
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': multipartBody.length,
},
})
})
}

498
test/fileResponse.test.mjs Normal file
View File

@ -0,0 +1,498 @@
import { Eltro as t, assert, stub } from 'eltro'
import { FileResponse } from '../flaska.mjs'
import { createCtx } from './helper.mjs'
t.test('should add path and stat', function() {
const assertPath = 'some/path/here'
const assertStat = { a: 1 }
let fileRes = new FileResponse(assertPath, assertStat)
assert.strictEqual(fileRes.filepath, assertPath)
assert.strictEqual(fileRes.stat, assertStat)
})
t.describe('CreateReader()', function() {
t.describe('return fileReader', function() {
const assertPath = '/some/path/here.png'
const assertBody = { a: 1 }
const assertSize = 12498
const assertmTime = new Date('2021-04-02T11:22:33.777')
const roundedModified = new Date('2021-04-02T11:22:33')
const assertIno = 3569723027
const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
let tests = [
[
'if no range',
function() {},
],
[
'if range start is higher than end',
function(headers) { headers['range'] = 'bytes=2000-1000' },
],
[
'if pre-condition if-match passes',
function(headers) { headers['if-match'] = assertEtag },
],
[
'if pre-condition if-unmodified-since passes',
function(headers) { headers['if-unmodified-since'] = roundedModified.toUTCString(); },
],
[
'if both pre-condition pass',
function(headers) { headers['if-unmodified-since'] = new Date(roundedModified.getTime() + 1000).toUTCString(); headers['if-match'] = assertEtag },
],
[
'if range is specified but if-range etag does not match',
function(headers) {
headers['if-range'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() + 1}"`
headers['range'] = 'bytes=1000-2000'
}
],
[
'if range is specified but if-range modified by is older',
function(headers) {
headers['if-range'] = `${new Date(roundedModified.getTime() - 1000).toUTCString()}`
headers['range'] = 'bytes=1000-2000'
}
],
[
'if range is specified but if-range is garbage',
function(headers) {
headers['if-range'] = `asdf`
headers['range'] = 'bytes=1000-2000'
}
],
]
tests.forEach(function(test){
t.test(test[0], function() {
const stubCreateReadStream = stub().returns(assertBody)
let ctx = createCtx()
test[1](ctx.req.headers)
ctx.body = new FileResponse(assertPath, stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, assertBody)
assert.strictEqual(stubCreateReadStream.firstCall[0], assertPath)
assert.deepStrictEqual(stubCreateReadStream.firstCall[1], {})
assert.strictEqual(ctx.headers['Etag'], assertEtag)
assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString())
assert.strictEqual(ctx.headers['Content-Length'], assertSize)
assert.strictEqual(ctx.type, 'image/png')
assert.notOk(ctx.headers['Content-Range'])
assert.strictEqual(ctx.status, 200)
})
})
})
t.test('return fileReader with proper length with range', function() {
const assertPath = '/some/path/here.jpg'
const assertBody = { a: 1 }
const assertSize = 2000
const assertmTime = new Date('2021-04-03T11:22:33.777')
const assertIno = 3569723027
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
const stubCreateReadStream = stub().returns(assertBody)
let ctx = createCtx()
ctx.req.headers['range'] = 'bytes=0-1023'
ctx.body = new FileResponse(assertPath, stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, assertBody)
assert.strictEqual(stubCreateReadStream.firstCall[0], assertPath)
assert.strictEqual(stubCreateReadStream.firstCall[1].start, 0)
assert.strictEqual(stubCreateReadStream.firstCall[1].end, 1023)
assert.strictEqual(ctx.headers['Etag'], `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`)
assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString())
assert.strictEqual(ctx.headers['Content-Length'], 1024)
assert.strictEqual(ctx.type, 'image/jpeg')
assert.strictEqual(ctx.headers['Content-Range'], `0-1023/${assertSize}`)
assert.strictEqual(ctx.status, 206)
})
t.test('should not call createReadStream if HEAD request', function() {
const assertPath = '/some/path/here.png'
const assertNotBody = { a: 1 }
const assertSize = 12498
const assertmTime = new Date('2021-04-02T11:22:33.777')
const roundedModified = new Date('2021-04-02T11:22:33')
const assertIno = 3569723027
const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
let tests = [
[
'if no range',
function() {},
],
[
'if range start is higher than end',
function(headers) { headers['range'] = 'bytes=2000-1000' },
],
[
'if pre-condition if-match passes',
function(headers) { headers['if-match'] = assertEtag },
],
[
'if pre-condition if-unmodified-since passes',
function(headers) { headers['if-unmodified-since'] = roundedModified.toUTCString(); },
],
[
'if both pre-condition pass',
function(headers) { headers['if-unmodified-since'] = new Date(roundedModified.getTime() + 1000).toUTCString(); headers['if-match'] = assertEtag },
],
[
'if range is specified but if-range etag does not match',
function(headers) {
headers['if-range'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() + 1}"`
headers['range'] = 'bytes=1000-2000'
}
],
[
'if range is specified but if-range modified by is older',
function(headers) {
headers['if-range'] = `${new Date(roundedModified.getTime() - 1000).toUTCString()}`
headers['range'] = 'bytes=1000-2000'
}
],
[
'if range is specified but if-range is garbage',
function(headers) {
headers['if-range'] = `asdf`
headers['range'] = 'bytes=1000-2000'
}
],
]
tests.forEach(function(test){
const stubCreateReadStream = stub().returns(assertNotBody)
let ctx = createCtx({
method: 'HEAD',
})
test[1](ctx.req.headers)
ctx.body = new FileResponse(assertPath, stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, null)
assert.notOk(stubCreateReadStream.called)
assert.strictEqual(ctx.headers['Etag'], assertEtag)
assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString())
assert.strictEqual(ctx.headers['Content-Length'], assertSize)
assert.strictEqual(ctx.type, 'image/png')
assert.notOk(ctx.headers['Content-Range'])
assert.strictEqual(ctx.status, 200)
})
const testStart = [0, 1000, 1999]
testStart.forEach(function(start) {
const stubCreateReadStream = stub()
let ctx = createCtx({
method: 'HEAD',
})
ctx.req.headers['range'] = 'bytes=' + start + '-'
ctx.body = new FileResponse('file.png', stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, null)
assert.notOk(stubCreateReadStream.called)
assert.strictEqual(ctx.headers['Content-Length'], assertSize - start)
assert.strictEqual(ctx.headers['Content-Range'], `${start}-${assertSize - 1}/${assertSize}`)
assert.strictEqual(ctx.status, 206)
})
})
t.test('return fileReader with proper length with range and if-range passes', function() {
const assertPath = '/some/path/here.jpg'
const assertBody = { a: 1 }
const assertSize = 2000
const assertmTime = new Date('2021-04-03T11:22:33.777')
const roundedModified = new Date('2021-04-03T11:22:33')
const assertIno = 3569723027
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`
let tests = [
function(headers) { headers['if-range'] = roundedModified.toUTCString() },
function(headers) { headers['if-range'] = assertEtag },
]
tests.forEach(function(check, i) {
const stubCreateReadStream = stub().returns(assertBody)
let ctx = createCtx()
ctx.req.headers['range'] = 'bytes=0-1023'
check(ctx.req.headers)
ctx.body = new FileResponse(assertPath, stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, assertBody)
assert.strictEqual(stubCreateReadStream.firstCall[0], assertPath)
assert.strictEqual(stubCreateReadStream.firstCall[1].start, 0, `start missing in test ${i + 1}`)
assert.strictEqual(stubCreateReadStream.firstCall[1].end, 1023)
assert.strictEqual(ctx.headers['Etag'], `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`)
assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString())
assert.strictEqual(ctx.headers['Content-Length'], 1024)
assert.strictEqual(ctx.type, 'image/jpeg')
assert.strictEqual(ctx.headers['Content-Range'], `0-1023/${assertSize}`)
assert.strictEqual(ctx.status, 206)
})
})
t.test('return fileReader with proper start if only start is specified', function() {
const assertSize = 2000
const stat = {
size: assertSize,
mtime: new Date('2021-04-03T11:22:33.777'),
ino: 111,
}
const testStart = [0, 1000, 1999]
testStart.forEach(function(start) {
const stubCreateReadStream = stub()
let ctx = createCtx()
ctx.req.headers['range'] = 'bytes=' + start + '-'
ctx.body = new FileResponse('file.png', stat)
ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(stubCreateReadStream.firstCall[1].start, start)
assert.strictEqual(stubCreateReadStream.firstCall[1].end, assertSize - 1)
assert.strictEqual(ctx.headers['Content-Length'], assertSize - start)
assert.strictEqual(ctx.headers['Content-Range'], `${start}-${assertSize - 1}/${assertSize}`)
assert.strictEqual(ctx.status, 206)
})
})
t.test('should default to end if end overflows but start is valid', function() {
const assertSize = 2000
const stat = {
size: assertSize,
mtime: new Date('2021-04-03T11:22:33.777'),
ino: 111,
}
const testEnd = [2000, 3000, 99999]
testEnd.forEach(function(end) {
const stubCreateReadStream = stub()
let ctx = createCtx()
ctx.req.headers['range'] = 'bytes=1000-' + end
ctx.body = new FileResponse('file.png', stat)
ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(stubCreateReadStream.firstCall[1].start, 1000)
assert.strictEqual(stubCreateReadStream.firstCall[1].end, 1999)
assert.strictEqual(ctx.headers['Content-Length'], 1000)
assert.strictEqual(ctx.headers['Content-Range'], `1000-${assertSize - 1}/${assertSize}`)
assert.strictEqual(ctx.status, 206)
})
})
t.test('should throw 416 if start is out of range', function() {
const stubCreateReadStream = stub()
let tests = [1000, 2000, 9999]
tests.forEach(function(start) {
let ctx = createCtx()
ctx.body = new FileResponse('file.png', {
size: 1000,
mtime: new Date('2021-04-03T11:22:33.777'),
ino: 111,
})
ctx.req.headers['range'] = `bytes=${start}-`
assert.throws(function() {
ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
}, function(err) {
assert.strictEqual(err.status, 416)
assert.match(err.message, new RegExp(1000))
assert.match(err.message, new RegExp(start))
assert.ok(ctx.headers['Etag'])
assert.notOk(stubCreateReadStream.called)
assert.notOk(ctx.headers['Last-Modified'])
assert.notOk(ctx.headers['Content-Length'])
assert.notOk(ctx.type)
assert.notOk(ctx.headers['Content-Range'])
return true
})
})
})
t.test('should return 304 if etag is found', function() {
const assertPath = '/some/path/here.png'
const assertNotBody = { a: 1 }
const assertSize = 12498
const assertmTime = new Date('2021-04-02T11:22:33.777')
const assertIno = 3569723027
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`
let tests = [
assertEtag,
`"asdf", "herp", ${assertEtag}`,
`"asdf", ${assertEtag}, "bla"`,
`${assertEtag}, "hello world"`,
`"asdf","herp",${assertEtag}`,
`"asdf",${assertEtag},"bla"`,
`${assertEtag},"hello world"`,
]
tests.forEach(function(check) {
const stubCreateReadStream = stub().returns(assertNotBody)
let ctx = createCtx()
ctx.req.headers['if-none-match'] = check
ctx.body = new FileResponse(assertPath, stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, null)
assert.strictEqual(ctx.status, 304)
assert.strictEqual(ctx.headers['Etag'], `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`)
assert.notOk(stubCreateReadStream.called)
assert.notOk(ctx.headers['Last-Modified'])
assert.notOk(ctx.headers['Content-Length'])
assert.notOk(ctx.type)
assert.notOk(ctx.headers['Content-Range'])
})
})
t.test('should return 304 if-last-modified is found', function() {
const assertPath = '/some/path/here.png'
const assertNotBody = { a: 1 }
const assertSize = 12498
const assertmTime = new Date('2021-04-02T11:22:33.777')
const assertIno = 3569723027
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
let tests = [
assertmTime.toUTCString(),
'Fri, 02 Apr 2021 11:22:33 GMT',
'Fri, 02 Apr 2021 11:22:34 GMT',
'Fri, 02 Apr 2022 11:22:32 GMT',
]
tests.forEach(function(check) {
const stubCreateReadStream = stub().returns(assertNotBody)
let ctx = createCtx()
ctx.body = new FileResponse(assertPath, stat)
ctx.req.headers['if-modified-since'] = assertmTime.toUTCString()
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, null)
assert.strictEqual(ctx.status, 304)
assert.strictEqual(ctx.headers['Etag'], `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`)
assert.notOk(stubCreateReadStream.called)
assert.notOk(ctx.headers['Last-Modified'])
assert.notOk(ctx.headers['Content-Length'])
assert.notOk(ctx.type)
assert.notOk(ctx.headers['Content-Range'])
})
})
t.test('should return 200 if etag or modified-by is not found', function() {
const assertPath = '/some/path/here.png'
const assertBody = { a: 1 }
const assertSize = 12498
const assertmTime = new Date('2021-04-02T11:22:33.777')
const assertIno = 3569723027
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`
const roundedModified = new Date('2021-04-02T11:22:33')
let tests = [
function(headers) { headers['if-none-match'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() + 1}"`; },
function(headers) { headers['if-none-match'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() - 1}"`; },
function(headers) { headers['if-none-match'] = `"asdf"`; },
function(headers) { headers['if-none-match'] = `"asdf"`; headers['if-modified-since'] = roundedModified.toUTCString(); },
function(headers) { headers['if-modified-since'] = new Date(roundedModified.getTime() - 1).toUTCString(); },
function(headers) { headers['if-modified-since'] = 'asdfs' },
]
tests.forEach(function(check, i) {
const stubCreateReadStream = stub().returns(assertBody)
let ctx = createCtx()
check(ctx.req.headers)
ctx.body = new FileResponse(assertPath, stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, assertBody, 'missing body in test ' + (i + 1))
assert.strictEqual(stubCreateReadStream.firstCall[0], assertPath)
assert.deepStrictEqual(stubCreateReadStream.firstCall[1], {})
assert.strictEqual(ctx.headers['Etag'], assertEtag)
assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString())
assert.strictEqual(ctx.headers['Content-Length'], assertSize)
assert.strictEqual(ctx.type, 'image/png')
assert.notOk(ctx.headers['Content-Range'])
assert.strictEqual(ctx.status, 200)
})
})
t.test('should return 412 if precondition fails', function() {
const stubCreateReadStream = stub()
const assertmTime = new Date('2021-04-02T11:22:33.777')
const roundedModified = new Date('2021-04-02T11:22:33')
const assertIno = 3569723027
const assertSize = 12498
const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
let tests = [
function(headers) { headers['if-match'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() + 1}"` },
function(headers) { headers['if-match'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() - 1}"` },
function(headers) { headers['if-match'] = `asdf` },
function(headers) { headers['if-unmodified-since'] = new Date(roundedModified.getTime() - 1).toUTCString(); },
function(headers) { headers['if-unmodified-since'] = 'asdf'; },
function(headers) { headers['if-match'] = assertEtag; headers['if-unmodified-since'] = 'asdf'; },
function(headers) { headers['if-match'] = assertEtag; headers['if-unmodified-since'] = new Date(roundedModified.getTime() - 1000).toUTCString(); },
function(headers) { headers['if-match'] = '"bla"'; headers['if-unmodified-since'] = roundedModified.toUTCString(); },
]
tests.forEach(function(check) {
let ctx = createCtx()
ctx.body = new FileResponse('file.png', stat)
check(ctx.req.headers)
assert.throws(function() {
ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
}, function(err) {
assert.strictEqual(err.status, 412)
assert.notOk(stubCreateReadStream.called)
assert.notOk(ctx.headers['Last-Modified'])
assert.notOk(ctx.headers['Content-Length'])
assert.notOk(ctx.type)
assert.notOk(ctx.headers['Content-Range'])
return true
})
})
})
})

View File

@ -4,25 +4,292 @@ import { createCtx, fakeHttp } from './helper.mjs'
const faker = fakeHttp()
t.test('should be able to override the http', function() {
let flaska = new Flaska({}, faker)
assert.strictEqual(flaska.http, faker)
t.describe('#constructor', function() {
t.test('should be able to override the http', function() {
let flaska = new Flaska({}, faker)
assert.strictEqual(flaska.http, faker)
})
t.test('it should have all the common verbs', function() {
let flaska = new Flaska({}, faker)
assert.ok(flaska.get)
assert.strictEqual(typeof(flaska.get), 'function')
assert.ok(flaska.post)
assert.strictEqual(typeof(flaska.post), 'function')
assert.ok(flaska.put)
assert.strictEqual(typeof(flaska.put), 'function')
assert.ok(flaska.delete)
assert.strictEqual(typeof(flaska.delete), 'function')
assert.ok(flaska.options)
assert.strictEqual(typeof(flaska.options), 'function')
assert.ok(flaska.patch)
assert.strictEqual(typeof(flaska.patch), 'function')
})
t.test('the verbs GET and HEAD should be identical', function() {
let flaska = new Flaska({}, faker)
assert.ok(flaska.get)
assert.strictEqual(typeof(flaska.get), 'function')
assert.notOk(flaska.head)
assert.ok(flaska.routers['HEAD'])
assert.strictEqual(flaska.routers['GET'], flaska.routers['HEAD'])
})
t.test('should have before default header generator', function() {
let flaska = new Flaska({}, faker)
assert.strictEqual(flaska._before.length, 1)
let ctx = {}
flaska._before[0](ctx)
assert.deepEqual(
Object.keys(ctx.headers).sort(),
['Server','X-Content-Type-Options','Content-Security-Policy','Cross-Origin-Opener-Policy','Cross-Origin-Resource-Policy','Cross-Origin-Embedder-Policy','Date'].sort()
)
assert.strictEqual(ctx.headers['Server'], 'Flaska')
assert.strictEqual(ctx.headers['X-Content-Type-Options'], 'nosniff')
assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'`)
assert.strictEqual(ctx.headers['Cross-Origin-Opener-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Resource-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Embedder-Policy'], 'require-corp')
assert.ok(new Date(ctx.headers['Date']).getDate())
assert.strictEqual(flaska._after.length, 0)
})
t.test('should have before ready setting headers on context if defaultHeaders is specified', function() {
const defaultHeaders = {
'Server': 'nginx/1.16.1',
'Content-Type': 'applicat"ion/json; charset=utf-8',
'Content-Length': '1646',
'Connection': 'keep-alive',
'vary': 'Origin',
'Link': 'Link goes here',
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
}
let flaska = new Flaska({
defaultHeaders: defaultHeaders,
}, faker)
assert.strictEqual(flaska._before.length, 1)
let ctx = {}
flaska._before[0](ctx)
let keys = Object.keys(defaultHeaders)
assert.strictEqual(Object.keys(ctx.headers).length, keys.length + 1)
for (let key of keys) {
assert.strictEqual(ctx.headers[key], defaultHeaders[key])
}
assert.ok(ctx.headers['Date'])
assert.strictEqual(flaska._after.length, 0)
})
t.test('should have before ready setting headers on context if appendHeaders is specified', function() {
const appendHeaders = {
'Server': 'nginx/1.16.1',
'Herp': 'Derp',
}
let flaska = new Flaska({
appendHeaders: appendHeaders,
}, faker)
assert.strictEqual(flaska._before.length, 1)
let ctx = {}
flaska._before[0](ctx)
assert.deepEqual(
Object.keys(ctx.headers).sort(),
['Server', 'Herp', 'X-Content-Type-Options','Content-Security-Policy','Cross-Origin-Opener-Policy','Cross-Origin-Resource-Policy','Cross-Origin-Embedder-Policy','Date'].sort()
)
assert.notStrictEqual(ctx.headers['Server'], 'Flaska')
assert.strictEqual(ctx.headers['Server'], appendHeaders.Server)
assert.strictEqual(ctx.headers['Herp'], 'Derp')
assert.strictEqual(ctx.headers['X-Content-Type-Options'], 'nosniff')
assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'`)
assert.strictEqual(ctx.headers['Cross-Origin-Opener-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Resource-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Embedder-Policy'], 'require-corp')
assert.ok(new Date(ctx.headers['Date']).getDate())
assert.strictEqual(flaska._after.length, 0)
})
})
t.test('it should have all the common verbs', function() {
let flaska = new Flaska({}, faker)
assert.ok(flaska.get)
assert.strictEqual(typeof(flaska.get), 'function')
assert.ok(flaska.post)
assert.strictEqual(typeof(flaska.post), 'function')
assert.ok(flaska.put)
assert.strictEqual(typeof(flaska.put), 'function')
assert.ok(flaska.delete)
assert.strictEqual(typeof(flaska.delete), 'function')
assert.ok(flaska.options)
assert.strictEqual(typeof(flaska.options), 'function')
assert.ok(flaska.patch)
assert.strictEqual(typeof(flaska.patch), 'function')
t.describe('#_nonce', function() {
t.test('should support nonce parameter with cached pre-filled entries', function() {
let flaska = new Flaska({
nonce: ['script-src', 'style-src'],
}, faker)
assert.ok(flaska._nonces)
assert.strictEqual(flaska._noncesIndex + 1, flaska._nonces.length)
assert.strictEqual(flaska._nonces.length, 25)
assert.ok(flaska._nonces[flaska._noncesIndex])
//Check they're all unique
let set = new Set()
flaska._nonces.forEach(function(entry) {
set.add(entry)
})
assert.strictEqual(set.size, flaska._nonces.length)
let ctx = createCtx()
assert.notOk(ctx.state.nonce)
let oldIndex = flaska._noncesIndex
flaska._before[0](ctx)
assert.ok(ctx.state.nonce)
assert.strictEqual(flaska._noncesIndex, oldIndex - 1)
assert.strictEqual(flaska._nonces[oldIndex], ctx.state.nonce)
assert.strictEqual(ctx.headers['Server'], 'Flaska')
assert.strictEqual(ctx.headers['X-Content-Type-Options'], 'nosniff')
assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline' 'nonce-${ctx.state.nonce}'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`)
assert.strictEqual(ctx.headers['Cross-Origin-Opener-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Resource-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Embedder-Policy'], 'require-corp')
assert.ok(new Date(ctx.headers['Date']).getDate())
})
t.test('should always return nonce values even if it runs out in cache', function() {
let flaska = new Flaska({
nonce: ['script-src'],
nonceCacheLength: 5,
}, faker)
let ctx = createCtx()
for (let i = 0; i < 5; i++) {
let nextNonce = flaska._nonces[flaska._noncesIndex]
flaska._before[0](ctx)
assert.strictEqual(ctx.state.nonce, nextNonce)
assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`)
}
assert.notOk(flaska._nonces[flaska._noncesIndex])
flaska._before[0](ctx)
assert.notOk(flaska._nonces[flaska._noncesIndex])
assert.ok(ctx.state.nonce)
for (let i = 0; i < flaska._nonces.length; i++) {
assert.notStrictEqual(ctx.state.nonce, flaska._nonces[i])
}
assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`)
})
t.test('should have after that regenerates lost hashes', function() {
let flaska = new Flaska({
nonce: ['script-src'],
nonceCacheLength: 5,
}, faker)
let ctx = createCtx()
assert.strictEqual(flaska._after.length, 1)
//Check they're all unique
let set = new Set()
flaska._nonces.forEach(function(entry) {
set.add(entry)
})
assert.strictEqual(set.size, 5)
flaska._before[0](ctx)
flaska._before[0](ctx)
flaska._before[0](ctx)
assert.strictEqual(flaska._noncesIndex, 1)
flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 2)
set.add(flaska._nonces[flaska._noncesIndex])
assert.strictEqual(set.size, 6)
flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 3)
set.add(flaska._nonces[flaska._noncesIndex])
assert.strictEqual(set.size, 7)
flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 4)
set.add(flaska._nonces[flaska._noncesIndex])
assert.strictEqual(set.size, 8)
flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 4)
set.add(flaska._nonces[flaska._noncesIndex])
assert.strictEqual(set.size, 8)
})
t.test('after should not generate keys outside range', function() {
let flaska = new Flaska({
nonce: ['script-src'],
nonceCacheLength: 2,
}, faker)
let ctx = createCtx()
assert.strictEqual(flaska._after.length, 1)
//Check they're all unique
let set = new Set()
flaska._nonces.forEach(function(entry) {
set.add(entry)
})
assert.strictEqual(set.size, 2)
flaska._before[0](ctx)
flaska._before[0](ctx)
assert.strictEqual(flaska._noncesIndex, -1)
flaska._before[0](ctx)
assert.strictEqual(flaska._noncesIndex, -2)
set.add(ctx.state.nonce)
assert.strictEqual(set.size, 3)
flaska._before[0](ctx)
assert.strictEqual(flaska._noncesIndex, -3)
set.add(ctx.state.nonce)
assert.strictEqual(set.size, 4)
flaska._before[0](ctx)
assert.strictEqual(flaska._noncesIndex, -4)
set.add(ctx.state.nonce)
assert.strictEqual(set.size, 5)
assert.strictEqual(Object.keys(flaska._nonces).length, 2)
flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 0)
set.add(flaska._nonces[flaska._noncesIndex])
assert.strictEqual(set.size, 6)
assert.strictEqual(Object.keys(flaska._nonces).length, 2)
flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 1)
set.add(flaska._nonces[flaska._noncesIndex])
assert.strictEqual(set.size, 7)
assert.strictEqual(Object.keys(flaska._nonces).length, 2)
flaska._after[0](ctx)
assert.strictEqual(flaska._noncesIndex, 1)
set.add(flaska._nonces[flaska._noncesIndex])
assert.strictEqual(set.size, 7)
assert.strictEqual(Object.keys(flaska._nonces).length, 2)
})
})
t.describe('#log', function() {
@ -99,11 +366,6 @@ specialHandlers.forEach(function(type) {
})
t.describe('_on404', function() {
t.test('a valid function', function() {
let flaska = new Flaska({}, faker)
assert.strictEqual(typeof(flaska._on404), 'function')
})
t.test('default valid handling of context', function() {
let ctx = createCtx()
let flaska = new Flaska({}, faker)
@ -114,6 +376,28 @@ t.describe('_on404', function() {
message: 'Not Found',
})
})
t.test('should do nothing if body is not null', function() {
const assertBody = { a: 1 }
let ctx = createCtx()
let flaska = new Flaska({}, faker)
ctx.body = assertBody
flaska._on404(ctx)
assert.strictEqual(ctx.status, 200)
assert.strictEqual(ctx.body, assertBody)
})
t.test('should do nothing if body is null but status is 204', function() {
let ctx = createCtx()
let flaska = new Flaska({}, faker)
ctx.status = 204
ctx.body = null
flaska._on404(ctx)
assert.strictEqual(ctx.status, 204)
assert.strictEqual(ctx.body, null)
})
})
t.describe('_onerror', function() {
@ -194,8 +478,8 @@ t.describe('_onreqerror', function() {
let flaska = new Flaska({}, faker)
let ctx = createCtx()
flaska._onreqerror(assertError, ctx)
assert.strictEqual(ctx.log.info.callCount, 1)
assert.strictEqual(ctx.log.info.firstCall[0], assertError)
assert.strictEqual(ctx.log.info.callCount, 0)
assert.strictEqual(ctx.log.error.callCount, 0)
})
})
@ -308,8 +592,8 @@ t.describe('#before()', function() {
let flaska = new Flaska({}, faker)
assert.ok(flaska._before)
flaska.before(assertFunction)
assert.strictEqual(flaska._before.length, 1)
assert.strictEqual(flaska._before[0], assertFunction)
assert.strictEqual(flaska._before.length, 2)
assert.strictEqual(flaska._before[1], assertFunction)
})
})
@ -661,6 +945,7 @@ t.describe('#listenAsync()', function() {
checkIp = ip
cb()
})
let flaska = new Flaska({}, testFaker)
assert.ok(flaska.requestStart)
flaska.requestStart = function() {
@ -672,7 +957,7 @@ t.describe('#listenAsync()', function() {
assert.strictEqual(checkIp, assertIp)
})
t.test('call http correctly if only port specified', async function() {
t.test('call http and listen correctly if only port specified', async function() {
const assertPort = 325897235
let checkPort = null
let checkIp = null
@ -692,6 +977,55 @@ t.describe('#listenAsync()', function() {
assert.strictEqual(checkPort, assertPort)
assert.strictEqual(checkIp, '::')
})
t.test('call http and listenAsync correctly if supported', async function() {
const assertPort = 4632
const assertIp = 'asdf'
const assertReturns = { a: 1 }
const stubListenAsync = stub().resolves(assertReturns)
let flaska = new Flaska({}, {
createServer: function() {
return {
listenAsync: stubListenAsync,
on: stub(),
}
}
})
assert.ok(flaska.requestStart)
flaska.requestStart = function() {
checkInternalThis = this
checkIsTrue = true
}
let res = await flaska.listenAsync(assertPort, assertIp)
assert.strictEqual(res, assertReturns)
assert.strictEqual(stubListenAsync.firstCall[0], assertPort)
assert.strictEqual(stubListenAsync.firstCall[1], assertIp)
})
t.test('call http and listenAsync correctly if supported and ip is null', async function() {
const assertPort = 325897235
const assertReturns = { a: 1 }
const stubListenAsync = stub().resolves(assertReturns)
let flaska = new Flaska({}, {
createServer: function() {
return {
listenAsync: stubListenAsync,
on: stub(),
}
}
})
assert.ok(flaska.requestStart)
flaska.requestStart = function() {
checkInternalThis = this
checkIsTrue = true
}
let res = await flaska.listenAsync(assertPort)
assert.strictEqual(res, assertReturns)
assert.strictEqual(stubListenAsync.firstCall[0], assertPort)
assert.strictEqual(stubListenAsync.firstCall[1], '::')
})
t.test('register requestStart if no async', async function() {
let checkIsTrue = false

View File

@ -19,42 +19,32 @@ t.describe('#requestStart()', function() {
flaska.onreserror(onResError)
flaska.requestEnded = onEnded
flaska.requestEnd = function(err, ctx) {
try {
assert.ok(err)
assert.strictEqual(assertReq.on.callCount, 2)
assert.strictEqual(assertRes.on.callCount, 2)
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.ok(err)
assert.strictEqual(assertReq.on.callCount, 1)
assert.strictEqual(assertRes.on.callCount, 2)
assert.strictEqual(assertRes.on.firstCall[0], 'error')
assert.strictEqual(typeof(assertRes.on.firstCall[1]), 'function')
assertRes.on.firstCall[1](assertErrorTwo, ctx)
assert.strictEqual(onResError.callCount, 1)
assert.strictEqual(onResError.firstCall[0], assertErrorTwo)
assert.strictEqual(onResError.firstCall[1], ctx)
assert.strictEqual(assertRes.on.secondCall[0], 'finish')
assert.strictEqual(typeof(assertRes.on.secondCall[1]), 'function')
assert.strictEqual(onEnded.callCount, 0)
assertRes.on.secondCall[1]()
assert.strictEqual(onEnded.callCount, 1)
assert.strictEqual(onEnded.firstCall[0], ctx)
assert.strictEqual(assertRes.on.firstCall[0], 'error')
assert.strictEqual(typeof(assertRes.on.firstCall[1]), 'function')
assertRes.on.firstCall[1](assertErrorTwo, ctx)
assert.strictEqual(onResError.callCount, 1)
assert.strictEqual(onResError.firstCall[0], assertErrorTwo)
assert.strictEqual(onResError.firstCall[1], ctx)
assert.strictEqual(assertRes.on.secondCall[0], 'finish')
assert.strictEqual(typeof(assertRes.on.secondCall[1]), 'function')
assert.strictEqual(onEnded.callCount, 0)
assertRes.on.secondCall[1]()
assert.strictEqual(onEnded.callCount, 1)
assert.strictEqual(onEnded.firstCall[0], ctx)
assert.strictEqual(assertReq.on.firstCall[0], 'error')
assert.strictEqual(typeof(assertReq.on.firstCall[1]), 'function')
assertReq.on.firstCall[1](assertErrorOne, ctx)
assert.strictEqual(onReqError.callCount, 1)
assert.strictEqual(onReqError.firstCall[0], assertErrorOne)
assert.strictEqual(onReqError.firstCall[1], ctx)
assert.strictEqual(assertReq.on.secondCall[0], 'close')
assert.strictEqual(typeof(assertReq.on.secondCall[1]), 'function')
// Test abort and close
cb()
} catch (err) { cb(err) }
}
assert.strictEqual(assertReq.on.firstCall[0], 'error')
assert.strictEqual(typeof(assertReq.on.firstCall[1]), 'function')
assertReq.on.firstCall[1](assertErrorOne, ctx)
assert.strictEqual(onReqError.callCount, 1)
assert.strictEqual(onReqError.firstCall[0], assertErrorOne)
assert.strictEqual(onReqError.firstCall[1], ctx)
})
flaska._beforeCompiled = function(ctx) {
throw new Error()
}
@ -68,18 +58,13 @@ t.describe('#requestStart()', function() {
const assertRes = createRes({ b: 2 })
let flaska = new Flaska({}, faker)
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.strictEqual(err, assertError)
assert.deepStrictEqual(ctx.state, {})
assert.strictEqual(ctx.req, assertReq)
assert.strictEqual(ctx.res, assertRes)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.ok(err)
assert.strictEqual(err, assertError)
assert.deepStrictEqual(ctx.state, {})
assert.strictEqual(ctx.req, assertReq)
assert.strictEqual(ctx.res, assertRes)
})
flaska._beforeCompiled = function(ctx) {
assert.strictEqual(ctx.req, assertReq)
assert.strictEqual(ctx.res, assertRes)
@ -102,18 +87,13 @@ t.describe('#requestStart()', function() {
return Promise.resolve().then(function() { return Promise.reject(assertError) })
}
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.strictEqual(err, assertError)
assert.deepStrictEqual(ctx.state, {})
assert.strictEqual(ctx.req, assertReq)
assert.strictEqual(ctx.res, assertRes)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.ok(err)
assert.strictEqual(err, assertError)
assert.deepStrictEqual(ctx.state, {})
assert.strictEqual(ctx.req, assertReq)
assert.strictEqual(ctx.res, assertRes)
})
flaska.requestStart(assertReq, assertRes)
})
@ -135,27 +115,35 @@ t.describe('#requestStart()', function() {
}
}
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
flaska.requestEnd = cb.finish(function(err, ctx) {
try {
assert.ok(err)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx.url, assertPath)
assert.strictEqual(ctx.search, assertSearch)
assert.strictEqual(ctx.method, assertMethod)
assert.strictEqual(ctx.status, 200)
assert.strictEqual(ctx.body, null)
assert.strictEqual(ctx.type, null)
assert.strictEqual(ctx.length, null)
assert.strictEqual(ctx.log, assertLog)
assert.ok(ctx.query)
assert.ok(ctx.query.get)
assert.ok(ctx.query.set)
assert.ok(ctx.query.delete)
cb()
} catch (err) { cb(err) }
}
assert.ok(err)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx.url, assertPath)
assert.strictEqual(ctx.search, assertSearch)
assert.strictEqual(ctx.method, assertMethod)
assert.strictEqual(ctx.status, 200)
assert.strictEqual(ctx.body, null)
assert.strictEqual(ctx.type, null)
assert.strictEqual(ctx.length, null)
assert.strictEqual(ctx.log, assertLog)
assert.ok(ctx.query)
assert.ok(ctx.query.get)
assert.ok(ctx.query.set)
assert.ok(ctx.query.delete)
assert.deepEqual(
Object.keys(ctx.headers).sort(),
['Server','X-Content-Type-Options','Content-Security-Policy','Cross-Origin-Opener-Policy','Cross-Origin-Resource-Policy','Cross-Origin-Embedder-Policy','Date'].sort()
)
assert.strictEqual(ctx.headers['Server'], 'Flaska')
assert.strictEqual(ctx.headers['X-Content-Type-Options'], 'nosniff')
assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'`)
assert.strictEqual(ctx.headers['Cross-Origin-Opener-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Resource-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Embedder-Policy'], 'require-corp')
assert.ok(new Date(ctx.headers['Date']).getDate())
})
flaska.requestStart(createReq({
url: assertPath + assertSearch,
@ -163,14 +151,52 @@ t.describe('#requestStart()', function() {
}), createRes())
})
t.test('correctly adds default headers', function(cb) {
const assertError = new Error('test')
const defaultHeaders = {
'Test': 'Asdf',
'Herp': 'Derp',
}
let flaska = new Flaska({
defaultHeaders: defaultHeaders,
}, faker)
flaska.compile()
flaska.routers.test = {
match: function(path) {
throw assertError
}
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.ok(err)
assert.strictEqual(err, assertError)
let keys = Object.keys(defaultHeaders)
assert.strictEqual(Object.keys(ctx.headers).length, keys.length + 1)
for (let key of keys) {
assert.strictEqual(ctx.headers[key], defaultHeaders[key])
}
assert.ok(ctx.headers['Date'])
})
flaska.requestStart(createReq({
url: '/',
method: 'test',
}), createRes())
})
t.test('calls correct router with correct url and context if beforeAsync', function(cb) {
const assertError = new Error('test')
const assertMethod = 'test'
const assertPath = '/test/me'
const assertSearch = '?asdf=test'
let calledBefore = false
let flaska = new Flaska({}, faker)
flaska.compile()
flaska._beforeAsyncCompiled = function() { return Promise.resolve() }
flaska._beforeAsyncCompiled = function() {
calledBefore = true
return Promise.resolve()
}
flaska.routers.test = {
match: function(path) {
@ -179,18 +205,14 @@ t.describe('#requestStart()', function() {
}
}
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx.url, assertPath)
assert.strictEqual(ctx.search, assertSearch)
assert.strictEqual(ctx.method, assertMethod)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.ok(err)
assert.ok(calledBefore)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx.url, assertPath)
assert.strictEqual(ctx.search, assertSearch)
assert.strictEqual(ctx.method, assertMethod)
})
flaska.requestStart(createReq({
url: assertPath + assertSearch,
@ -221,18 +243,13 @@ t.describe('#requestStart()', function() {
throw assertError
}
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx, checkMiddleCtx)
assert.strictEqual(ctx.params, assertParams)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx, checkMiddleCtx)
assert.strictEqual(ctx.params, assertParams)
})
flaska.requestStart(createReq({
url: '',
@ -253,17 +270,12 @@ t.describe('#requestStart()', function() {
flaska.get('/:id', handler)
flaska.compile()
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx, checkHandlerCtx)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx, checkHandlerCtx)
})
flaska.requestStart(createReq({
url: '/test',
@ -283,16 +295,12 @@ t.describe('#requestStart()', function() {
flaska.get('/test', function() { throw new Error('should not be called') })
flaska.compile()
flaska.requestEnd = function(err, ctx) {
if (err) return cb(err)
try {
assert.ok(ctx)
assert.strictEqual(on404Error.callCount, 1)
assert.strictEqual(on404Error.firstCall[0], ctx)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.notOk(err)
assert.ok(ctx)
assert.strictEqual(on404Error.callCount, 1)
assert.strictEqual(on404Error.firstCall[0], ctx)
})
flaska.requestStart(createReq({
url: '/nope',
@ -313,17 +321,11 @@ t.describe('#requestStart()', function() {
flaska.get('/test', function() { throw new Error('should not be called') })
flaska.compile()
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(ctx, checkCtx)
assert.strictEqual(err, assertError)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.strictEqual(err, assertError)
assert.ok(ctx)
assert.strictEqual(ctx, checkCtx)
})
flaska.requestStart(createReq({
url: '/nope',
@ -343,17 +345,11 @@ t.describe('#requestStart()', function() {
flaska.get('/test', function() { throw new Error('should not be called') })
flaska.compile()
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(ctx, checkCtx)
assert.strictEqual(err, assertError)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.strictEqual(err, assertError)
assert.ok(ctx)
assert.strictEqual(ctx, checkCtx)
})
flaska.requestStart(createReq({
url: '/nope',
@ -373,17 +369,11 @@ t.describe('#requestStart()', function() {
flaska.get('/test', middles, function() { throw new Error('should not be called') })
flaska.compile()
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(ctx, checkCtx)
assert.strictEqual(err, assertError)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.strictEqual(err, assertError)
assert.ok(ctx)
assert.strictEqual(ctx, checkCtx)
})
flaska.requestStart(createReq({
url: '/test',
@ -405,14 +395,11 @@ t.describe('#requestStart()', function() {
throw new Error('should not be called')
}
flaska.requestEnd = function(err, ctx) {
try {
assert.notOk(err)
assert.ok(ctx)
assert.strictEqual(ctx.body, assertBody)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.notOk(err)
assert.ok(ctx)
assert.strictEqual(ctx.body, assertBody)
})
flaska.requestStart(createReq({
url: '/test/something/here',
@ -437,16 +424,11 @@ t.describe('#requestStart()', function() {
return Promise.resolve().then(function() { return Promise.reject(assertError) })
}
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
})
flaska.requestStart(createReq({
url: '',
@ -467,17 +449,12 @@ t.describe('#requestStart()', function() {
flaska.get('/:id', [function() { return Promise.resolve() }], handler)
flaska.compile()
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx, checkHandlerCtx)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx, checkHandlerCtx)
})
flaska.requestStart(createReq({
url: '/test',
@ -498,17 +475,12 @@ t.describe('#requestStart()', function() {
flaska.get('/:id', [function() { return Promise.resolve() }], handler)
flaska.compile()
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx, checkHandlerCtx)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx, checkHandlerCtx)
})
flaska.requestStart(createReq({
url: '/test',
@ -537,15 +509,12 @@ t.describe('#requestStart()', function() {
flaska.get('/::path', [middle], handler)
flaska.compile()
flaska.requestEnd = function(err, ctx) {
try {
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.notOk(err)
assert.ok(ctx)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.state, assertState)
cb()
} catch (err) { cb(err) }
}
})
flaska.requestStart(createReq({
url: '/test/something/here',
@ -568,17 +537,12 @@ t.describe('#requestStart()', function() {
flaska.get('/:id', handler)
flaska.compile()
flaska.requestEnd = function(err, ctx) {
if (err && err !== assertError) return cb(err)
try {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx, checkHandlerCtx)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.ok(err)
assert.ok(ctx)
assert.strictEqual(err, assertError)
assert.strictEqual(ctx, checkHandlerCtx)
})
flaska.requestStart(createReq({
url: '/test',
@ -603,14 +567,11 @@ t.describe('#requestStart()', function() {
throw new Error('should not be called')
}
flaska.requestEnd = function(err, ctx) {
try {
assert.notOk(err)
assert.ok(ctx)
assert.strictEqual(ctx.body, assertBody)
cb()
} catch (err) { cb(err) }
}
flaska.requestEnd = cb.finish(function(err, ctx) {
assert.notOk(err)
assert.ok(ctx)
assert.strictEqual(ctx.body, assertBody)
})
flaska.requestStart(createReq({
url: '/test/something/here',

View File

@ -1,5 +1,5 @@
import { Eltro as t, spy, assert, stub} from 'eltro'
import { Flaska, FlaskaRouter } from '../flaska.mjs'
import { FileResponse, Flaska, FlaskaRouter } from '../flaska.mjs'
import { fakeHttp, createCtx } from './helper.mjs'
const fakerHttp = fakeHttp()
@ -12,21 +12,17 @@ t.describe('#requestEnd()', function() {
const assertStatus = 501
// Calculated manually just in case
const assertBodyLength = 7
let onFinish = function(body) {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.res.setHeader.callCount, 2)
assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type')
assert.strictEqual(ctx.res.setHeader.firstCall[1], 'application/json; charset=utf-8')
assert.strictEqual(ctx.res.setHeader.secondCall[0], 'Content-Length')
assert.strictEqual(ctx.res.setHeader.secondCall[1], assertBodyLength)
assert.ok(body)
assert.strictEqual(body, '{"a":1}')
cb()
} catch (err) { cb(err) }
}
let onFinish = cb.finish(function(body) {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.headers['Content-Type'], 'application/json; charset=utf-8')
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
assert.ok(body)
assert.strictEqual(body, '{"a":1}')
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({}, onFinish)
let flaska = new Flaska({}, fakerHttp, fakeStream)
@ -44,14 +40,11 @@ t.describe('#requestEnd()', function() {
const assertErrorNotSeen = new Error('should not be seen')
const assertError = new Error('test')
let onFinish = function(body) {
try {
assert.strictEqual(ctx.status, 500)
assert.strictEqual(ctx.body.status, 500)
assert.strictEqual(ctx.body.message, 'Internal Server Error')
cb()
} catch (err) { cb(err) }
}
let onFinish = cb.finish(function(body) {
assert.strictEqual(ctx.status, 500)
assert.strictEqual(ctx.body.status, 500)
assert.strictEqual(ctx.body.message, 'Internal Server Error')
})
const ctx = createCtx({}, onFinish)
let flaska = new Flaska({}, fakerHttp, fakeStream)
@ -62,107 +55,364 @@ t.describe('#requestEnd()', function() {
flaska.requestEnd(assertErrorNotSeen, ctx)
})
t.test('call res and end correctly when dealing with objects', function(cb) {
const assertStatus = 202
// Calculated manually just in case
const assertBodyLength = 7
const assertBody = { a: 1 }
let onFinish = function(body) {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.res.setHeader.callCount, 2)
assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type')
assert.strictEqual(ctx.res.setHeader.firstCall[1], 'application/json; charset=utf-8')
assert.strictEqual(ctx.res.setHeader.secondCall[0], 'Content-Length')
assert.strictEqual(ctx.res.setHeader.secondCall[1], assertBodyLength)
assert.ok(body)
assert.strictEqual(body, '{"a":1}')
cb()
} catch (err) { cb(err) }
}
const ctx = createCtx({
status: assertStatus,
}, onFinish)
ctx.body = assertBody
let testMethods = ['GET', 'HEAD']
testMethods.forEach(function(method) {
t.describe(method, function() {
t.test('call res and end correctly when dealing with objects', function(cb) {
const assertStatus = 202
// Calculated manually just in case
const assertBodyLength = 7
const assertBody = { a: 1 }
let onFinish = cb.finish(function(body) {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.headers['Content-Type'], 'application/json; charset=utf-8')
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
if (method === 'GET') {
assert.ok(body)
assert.strictEqual(body, '{"a":1}')
} else {
assert.notOk(body)
}
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({
method: method,
status: assertStatus,
}, onFinish)
ctx.body = assertBody
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
})
t.test('call res and end correctly when dealing with buffers', function(cb) {
const assertStatus = 202
// Calculated manually just in case
const assertBodyLength = 11
const assertBody = Buffer.from('hello world')
let onFinish = cb.finish(function(body) {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.headers['Content-Type'], 'application/octet-stream')
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
if (method === 'GET') {
assert.ok(body)
assert.strictEqual(body, assertBody)
} else {
assert.notOk(body)
}
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({
method: method,
status: assertStatus,
}, onFinish)
ctx.body = assertBody
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
})
t.test('call res and end correctly when dealing with null and no status override', function(cb) {
// Calculated manually just in case
const assertBodyLength = 0
const assertBody = null
let onFinish = cb.finish(function(body) {
assert.strictEqual(ctx.status, 204)
assert.strictEqual(ctx.body, assertBody)
assert.notOk(ctx.headers['Content-Type'])
assert.notOk(ctx.headers['Content-Length'])
assert.strictEqual(body, undefined)
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({
method: method,
status: 200,
}, onFinish)
ctx.body = assertBody
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
})
t.test('call res and end correctly when dealing with null and status override', function(cb) {
const assertStatus = 202
// Calculated manually just in case
const assertBody = null
let onFinish = cb.finish(function(body) {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.notOk(ctx.headers['Content-Type'])
assert.strictEqual(ctx.headers['Content-Length'], 0)
assert.strictEqual(body, undefined)
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({
method: method,
status: assertStatus,
}, onFinish)
ctx.body = assertBody
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
})
t.test('call res and end correctly when dealing with strings', function(cb) {
const assertStatus = 206
// Calculated manually just in case
const assertBodyLength = 4
const assertBody = 'eða'
let onFinish = cb.finish(function(body) {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8')
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
if (method === 'GET') {
assert.ok(body)
assert.strictEqual(body, assertBody)
} else {
assert.notOk(body)
}
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({
method: method,
status: assertStatus,
}, onFinish)
ctx.body = assertBody
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
})
t.test('call res and end correctly when dealing with numbers', function(cb) {
const assertStatus = 211
// Calculated manually just in case
const assertBodyLength = 7
const assertBody = 4214124
let onFinish = cb.finish(function(body) {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8')
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
if (method === 'GET') {
assert.ok(body)
assert.strictEqual(body, assertBody.toString())
} else {
assert.notOk(body)
}
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({
method: method,
status: assertStatus,
}, onFinish)
ctx.body = assertBody
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
})
if (method === 'GET') {
t.test('call handleRequest if body contains it', function(cb) {
const assertContentType = 'test/test'
const assertContentLength = 1241241
const assertHandle = stub().returnWith(function(checkCtx, secondary) {
assert.notOk(secondary)
assert.strictEqual(checkCtx, ctx)
assert.notOk(ctx.res.writeHead.called)
ctx.type = assertContentType
ctx.headers['Content-Length'] = assertContentLength
return assertBody
})
let body = new FileResponse()
let assertBody = { pipe: function() {} }
let onFinish = cb.finish(function(source, target, callback) {
assert.ok(assertHandle.called)
assert.ok(ctx.res.writeHead)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(source, assertBody)
assert.strictEqual(target, ctx.res)
assert.strictEqual(typeof(callback), 'function')
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
assert.strictEqual(ctx.headers['Content-Type'], assertContentType)
assert.strictEqual(ctx.headers['Content-Length'], assertContentLength)
})
const ctx = createCtx({
})
ctx.body = body
fakeStream.pipeline = onFinish
body.handleRequest = assertHandle
let flaska = new Flaska({}, fakerHttp, fakeStream)
try {
flaska.requestEnd(null, ctx)
} catch (err) {
cb(err)
}
})
t.test('call pipeline correctly when dealing with pipe', function(cb) {
const assertStatus = 211
const assertType = 'herp/derp'
const assertBody = { pipe: function() {} }
let onFinish = cb.finish(function(source, target, callback) {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.headers['Content-Type'], assertType)
assert.strictEqual(source, assertBody)
assert.strictEqual(target, ctx.res)
assert.strictEqual(typeof(callback), 'function')
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({
status: assertStatus,
})
fakeStream.pipeline = onFinish
ctx.body = assertBody
ctx.type = assertType
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
})
} else {
t.test('call handleRequest if body contains it', function(cb) {
const assertContentType = 'test/test'
const assertContentLength = 1241241
const assertHandle = stub().returnWith(function(checkCtx, secondary) {
assert.notOk(secondary)
assert.strictEqual(checkCtx, ctx)
assert.notOk(ctx.res.writeHead.called)
ctx.type = assertContentType
ctx.headers['Content-Length'] = assertContentLength
return null
})
let body = new FileResponse()
let onFinish = cb.finish(function(body) {
assert.ok(assertHandle.called)
assert.ok(ctx.res.writeHead)
assert.strictEqual(ctx.body, null)
assert.notOk(body)
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
assert.strictEqual(ctx.headers['Content-Type'], assertContentType)
assert.strictEqual(ctx.headers['Content-Length'], assertContentLength)
})
const ctx = createCtx({
method: method,
}, onFinish)
ctx.body = body
body.handleRequest = assertHandle
let flaska = new Flaska({}, fakerHttp, fakeStream)
try {
flaska.requestEnd(null, ctx)
} catch (err) {
cb(err)
}
})
t.test('call pipeline correctly when dealing with pipe', function(cb) {
const assertStatus = 211
const assertType = 'herp/derp'
const assertBody = { pipe: function() {}, destroy: stub() }
let onFinish = cb.finish(function(body) {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.headers['Content-Type'], assertType)
assert.notOk(ctx.headers['Content-Length'])
assert.notOk(body)
assert.ok(assertBody.destroy.called)
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({
method: method,
status: assertStatus,
}, onFinish)
ctx.body = assertBody
ctx.type = assertType
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
})
}
})
})
t.test('call res and end correctly when dealing with strings', function(cb) {
const assertStatus = 206
// Calculated manually just in case
const assertBodyLength = 4
const assertBody = 'eða'
let onFinish = function(body) {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.res.setHeader.callCount, 2)
assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type')
assert.strictEqual(ctx.res.setHeader.firstCall[1], 'text/plain; charset=utf-8')
assert.strictEqual(ctx.res.setHeader.secondCall[0], 'Content-Length')
assert.strictEqual(ctx.res.setHeader.secondCall[1], assertBodyLength)
assert.ok(body)
assert.strictEqual(body, assertBody)
cb()
} catch (err) { cb(err) }
}
t.test('call _onerror with error if handleRequest throws error', function() {
let body = new FileResponse()
const assertError = new Error('Secrets of the Goddess')
let assertBody = { a : 1 }
const ctx = createCtx({
status: assertStatus,
}, onFinish)
ctx.body = assertBody
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
})
t.test('call res and end correctly when dealing with numbers', function(cb) {
const assertStatus = 211
// Calculated manually just in case
const assertBodyLength = 7
const assertBody = 4214124
let onFinish = function(body) {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.res.setHeader.callCount, 2)
assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type')
assert.strictEqual(ctx.res.setHeader.firstCall[1], 'text/plain; charset=utf-8')
assert.strictEqual(ctx.res.setHeader.secondCall[0], 'Content-Length')
assert.strictEqual(ctx.res.setHeader.secondCall[1], assertBodyLength)
assert.ok(body)
assert.strictEqual(body, assertBody.toString())
cb()
} catch (err) { cb(err) }
}
const ctx = createCtx({
status: assertStatus,
}, onFinish)
ctx.body = assertBody
status: 204,
})
ctx.body = body
const assertHandle = stub().throws(assertError)
body.handleRequest = assertHandle
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska._onerror = stub().returnWith(function(err, checkCtx) {
assert.strictEqual(err, assertError)
checkCtx.body = assertBody
})
flaska.requestEnd(null, ctx)
assert.ok(assertHandle.called)
assert.ok(flaska._onerror.called)
assert.strictEqual(flaska._onerror.firstCall[0], assertError)
assert.ok(ctx.res.writeHead)
assert.strictEqual(ctx.body, assertBody)
})
t.test('call res and end correctly when dealing with custom type', function(cb) {
const assertStatus = 209
const assertBody = 'test'
const assertType = 'something/else'
let onFinish = function(body) {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type')
assert.strictEqual(ctx.res.setHeader.firstCall[1], assertType)
assert.strictEqual(body, assertBody)
cb()
} catch (err) { cb(err) }
}
let onFinish = cb.finish(function(body) {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.headers['Content-Type'], assertType)
assert.strictEqual(body, assertBody)
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({
status: assertStatus,
body: assertBody,
@ -173,45 +423,16 @@ t.describe('#requestEnd()', function() {
flaska.requestEnd(null, ctx)
})
t.test('call pipeline correctly when dealing with pipe', function(cb) {
const assertStatus = 211
const assertType = 'herp/derp'
const assertBody = { pipe: function() {} }
let onFinish = function(source, target, callback) {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type')
assert.strictEqual(ctx.res.setHeader.firstCall[1], assertType)
assert.strictEqual(source, assertBody)
assert.strictEqual(target, ctx.res)
assert.strictEqual(typeof(callback), 'function')
cb()
} catch (err) { cb(err) }
}
const ctx = createCtx({
status: assertStatus,
})
fakeStream.pipeline = onFinish
ctx.body = assertBody
ctx.type = assertType
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
})
t.test('call pipe should have default type', function(cb) {
let onFinish = function(source, target) {
try {
assert.strictEqual(ctx.res.statusCode, 200)
assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type')
assert.strictEqual(ctx.res.setHeader.firstCall[1], 'application/octet-stream')
assert.strictEqual(source, ctx.body)
assert.strictEqual(target, ctx.res)
cb()
} catch (err) { cb(err) }
}
let onFinish = cb.finish(function(source, target) {
assert.strictEqual(ctx.status, 200)
assert.strictEqual(ctx.headers['Content-Type'], 'application/octet-stream')
assert.strictEqual(source, ctx.body)
assert.strictEqual(target, ctx.res)
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({})
ctx.body = { pipe: function() {} }
fakeStream.pipeline = onFinish
@ -229,16 +450,15 @@ t.describe('#requestEnd()', function() {
tests.forEach(function(test) {
t.test(`call pipe with file extension ${test[0]} should mimetype ${test[1]}`, function(cb) {
let onFinish = function(source, target) {
try {
assert.strictEqual(ctx.res.statusCode, 200)
assert.strictEqual(ctx.res.setHeader.firstCall[0], 'Content-Type')
assert.strictEqual(ctx.res.setHeader.firstCall[1], test[1])
assert.strictEqual(source, ctx.body)
assert.strictEqual(target, ctx.res)
cb()
} catch (err) { cb(err) }
}
let onFinish = cb.finish(function(source, target) {
assert.strictEqual(ctx.status, 200)
assert.strictEqual(ctx.headers['Content-Type'], test[1])
assert.strictEqual(source, ctx.body)
assert.strictEqual(target, ctx.res)
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
})
const ctx = createCtx({})
ctx.body = { pipe: function() {}, path: './bla/test/temp.' + test[0] }
fakeStream.pipeline = onFinish
@ -266,15 +486,14 @@ t.describe('#requestEnd()', function() {
const assertStatus = status
const assertNotBody = 'test'
const assertNotType = 'something/else'
let onFinish = function(body) {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.res.setHeader.callCount, 0)
assert.notOk(body)
cb()
} catch (err) { cb(err) }
}
let onFinish = cb.finish(function(body) {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.res.setHeader.callCount, 0)
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers)
assert.notOk(body)
})
const ctx = createCtx({
status: assertStatus,
body: assertNotBody,

View File

@ -6,7 +6,7 @@ const indexMap = [
'thirdCall',
]
export function fakeHttp(inj1, inj2) {
export function fakeHttp(inj1, inj2, inj3) {
let intermediate = {
createServer: function(cb) {
if (inj1) inj1(cb)
@ -15,6 +15,9 @@ export function fakeHttp(inj1, inj2) {
listen: function(port, ip, cb) {
if (inj2) inj2(port, ip, cb)
else if (cb) cb()
},
on: function(name, handler) {
if (inj3) inj3(name, handler)
}
}
}
@ -24,6 +27,7 @@ export function fakeHttp(inj1, inj2) {
export function createReq(def) {
return defaults(def, {
headers: {},
on: spy(),
})
}
@ -40,7 +44,7 @@ export function createRes(def) {
})
}
export function createCtx(def, endHandler) {
export function createCtx(def = {}, endHandler = null) {
return defaults(def, {
req: createReq(),
res: createRes({ end: endHandler || spy() }),
@ -53,6 +57,7 @@ export function createCtx(def, endHandler) {
body: null,
type: null,
length: null,
headers: {},
log: {
error: spy(),
info: spy(),

View File

@ -1,9 +1,10 @@
import fsSync from 'fs'
import http from 'http'
import fs from 'fs/promises'
import formidable from 'formidable'
import { Eltro as t, assert, stub } from 'eltro'
import { setTimeout } from 'timers/promises'
import { Flaska, JsonHandler, FormidableHandler } from '../flaska.mjs'
import { Flaska, JsonHandler, FormidableHandler, FileResponse } from '../flaska.mjs'
import Client from './client.mjs'
const port = 51024
@ -23,6 +24,11 @@ let reqBody = null
let file = null
let uploaded = []
flaska.after(function(ctx) {
if (ctx.aborted) return
ctx.log.info(ctx.status)
})
flaska.get('/', function(ctx) {
ctx.body = { status: true }
})
@ -30,6 +36,12 @@ flaska.post('/json', JsonHandler(), function(ctx) {
ctx.body = { success: true }
reqBody = ctx.req.body
})
flaska.post('/json/slow', JsonHandler(), async function(ctx) {
await setTimeout(300)
ctx.body = { success: true }
ctx.status = 201
reqBody = ctx.req.body
})
flaska.get('/timeout', function(ctx) {
return new Promise(function() {})
})
@ -37,12 +49,24 @@ flaska.get('/file', function(ctx) {
file = fsSync.createReadStream('./test/test.png')
ctx.body = file
})
flaska.get('/filehandle', function(ctx) {
return fs.stat('./test/test.txt').then(function(stat) {
ctx.body = new FileResponse('./test/test.txt', stat)
})
})
flaska.post('/file/upload', FormidableHandler(formidable, {
uploadDir: './test/upload'
}), function(ctx) {
uploaded.push(ctx.req.file)
ctx.body = ctx.req.file
})
flaska.post('/file/upload/many', FormidableHandler(formidable, {
uploadDir: './test/upload',
}), function(ctx) {
uploaded.push(ctx.req.files.herp)
uploaded.push(ctx.req.files.derp)
ctx.body = ctx.req.files
})
flaska.get('/file/leak', function(ctx) {
file = fsSync.createReadStream('./test/test.png')
ctx.body = file
@ -73,6 +97,9 @@ t.describe('/', function() {
})
t.describe('/json', function() {
t.beforeEach(function() {
log.info.reset()
})
t.test('should return success and store body', async function() {
const assertBody = { a: '' }
for (let i = 0; i < 1010; i++) {
@ -82,6 +109,21 @@ t.describe('/json', function() {
let body = await client.post('/json', assertBody)
assert.deepEqual(body, { success: true })
assert.deepStrictEqual(reqBody, assertBody)
assert.strictEqual(log.info.callCount, 1)
assert.strictEqual(log.info.firstCall[0], 200)
})
t.test('should return and log correctly', async function() {
const assertBody = { a: '' }
for (let i = 0; i < 1010; i++) {
assertBody.a += 'aaaaaaaaaa'
}
reqBody = null
let body = await client.post('/json/slow', assertBody)
assert.deepEqual(body, { success: true })
assert.deepStrictEqual(reqBody, assertBody)
assert.strictEqual(log.info.callCount, 1)
assert.strictEqual(log.info.firstCall[0], 201)
})
t.test('should fail if body is too big', async function() {
@ -103,15 +145,15 @@ t.describe('/json', function() {
t.test('should fail if not a valid json', async function() {
reset()
let err = await assert.isRejected(client.customRequest('POST', '/json', 'aaaa'))
let err = await assert.isRejected(client.customRequest('POST', '/json', 'XXXXX'))
assert.strictEqual(err.body.status, 400)
assert.match(err.body.message, /invalid json/i)
assert.match(err.body.message, /token a/i)
assert.strictEqual(err.body.request, 'aaaa')
assert.match(err.body.message, /token[^X]+X/i)
assert.strictEqual(err.body.request, 'XXXXX')
assert.strictEqual(log.error.callCount, 1)
assert.match(log.error.firstCall[0].message, /invalid json/i)
assert.match(log.error.firstCall[0].message, /token a/i)
assert.match(log.error.firstCall[0].message, /token[^X]+X/i)
})
t.test('should handle incomplete requests correctly', async function() {
@ -128,8 +170,8 @@ t.describe('/json', function() {
await setTimeout(20)
assert.strictEqual(log.error.callCount, 0)
assert.strictEqual(log.info.callCount, 1)
assert.strictEqual(log.info.firstCall[0].message, 'aborted')
assert.strictEqual(log.info.callCount, 0)
// assert.strictEqual(log.info.firstCall[0].message, 'aborted')
})
})
@ -139,12 +181,9 @@ t.describe('/timeout', function() {
let err = await assert.isRejected(client.customRequest('GET', '/timeout', JSON.stringify({}), { timeout: 20 }))
await setTimeout(20)
assert.match(err.message, /timed out/)
assert.notOk(log.error.called)
assert.ok(log.info.called)
assert.strictEqual(log.info.firstCall[0].message, 'aborted')
assert.notOk(log.info.called)
})
})
@ -156,7 +195,9 @@ t.describe('/file', function() {
await client.customRequest('GET', '/file', null, { toPipe: target })
await setTimeout(20)
while (!target.closed && !file.closed) {
await setTimeout(10)
}
assert.ok(target.closed)
assert.ok(file.closed)
@ -186,20 +227,80 @@ t.describe('/file', function() {
req.destroy()
await setTimeout(20)
while (!file.closed) {
await setTimeout(10)
}
assert.strictEqual(log.error.callCount, 0)
assert.strictEqual(log.info.callCount, 1)
assert.strictEqual(log.info.firstCall[0].message, 'aborted')
assert.strictEqual(log.info.callCount, 0)
assert.ok(file.closed)
})
})
t.describe('/filehandle', function() {
const agent = new http.Agent({
keepAlive: true,
maxSockets: 1,
keepAliveMsecs: 3000,
})
t.test('server should send correctly', async function() {
log.error.reset()
let res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent })
assert.strictEqual(res.status, 200)
assert.strictEqual(res.headers['content-length'], '11')
assert.strictEqual(res.headers['content-type'], 'text/plain')
assert.match(res.headers['etag'], /\"\d+-11-\d+"/)
assert.ok(res.headers['last-modified'])
let d = new Date(Date.parse(res.headers['last-modified']))
let etag = res.headers['etag']
assert.ok(d.getTime())
res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'If-Modified-Since': d.toUTCString()
},
})
assert.strictEqual(res.status, 304)
assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['etag'], etag)
res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'If-None-Match': etag
},
})
assert.strictEqual(res.status, 304)
assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['etag'], etag)
res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'Range': 'bytes=2-5'
},
})
assert.strictEqual(res.status, 206)
assert.strictEqual(res.data, 'llo ')
assert.strictEqual(res.headers['content-length'], '4')
res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'Range': 'bytes=0-0'
},
})
assert.strictEqual(res.status, 206)
assert.strictEqual(res.data, 'H')
assert.strictEqual(res.headers['content-length'], '1')
})
t.after(function() {
agent.destroy()
})
})
t.describe('/file/upload', function() {
t.test('server should upload file', async function() {
let res = await client.upload('/file/upload', './test/test.png')
@ -211,6 +312,195 @@ t.describe('/file/upload', function() {
assert.strictEqual(statSource.size, statTarget.size)
assert.strictEqual(statSource.size, res.size)
assert.strictEqual(res.type, 'image/png')
})
t.test('server should have correct type', async function() {
let res = await client.upload('/file/upload', './test/test.jpg')
let [statSource, statTarget] = await Promise.all([
fs.stat('./test/test.jpg'),
fs.stat(res.path),
])
assert.strictEqual(statSource.size, statTarget.size)
assert.strictEqual(statSource.size, res.size)
assert.strictEqual(res.type, 'image/jpeg')
})
t.test('server should use type from user', async function() {
const assertType = 'some/test/here'
let res = await client.upload('/file/upload', './test/test.jpg', 'POST', {}, assertType)
let [statSource, statTarget] = await Promise.all([
fs.stat('./test/test.jpg'),
fs.stat(res.path),
])
assert.strictEqual(statSource.size, statTarget.size)
assert.strictEqual(statSource.size, res.size)
assert.strictEqual(res.type, assertType)
})
t.test('server should attempt to correct type if type is application/octet-stream', async function() {
const assertNotType = 'application/octet-stream'
let res = await client.upload('/file/upload', './test/test.jpg', 'POST', {}, assertNotType)
let [statSource, statTarget] = await Promise.all([
fs.stat('./test/test.jpg'),
fs.stat(res.path),
])
assert.strictEqual(statSource.size, statTarget.size)
assert.strictEqual(statSource.size, res.size)
assert.notStrictEqual(res.type, assertNotType)
assert.strictEqual(res.type, 'image/jpeg')
})
t.test('server fall back to type application/octet-stream if unknown', async function() {
let res = await client.upload('/file/upload', './test/test.gibberish')
let [statSource, statTarget] = await Promise.all([
fs.stat('./test/test.gibberish'),
fs.stat(res.path),
])
assert.strictEqual(statSource.size, statTarget.size)
assert.strictEqual(statSource.size, res.size)
assert.strictEqual(res.type, 'application/octet-stream')
res = await client.upload('/file/upload', './test/test.gibberish', 'POST', {}, 'application/octet-stream')
assert.strictEqual(res.type, 'application/octet-stream')
})
})
t.describe('/file/upload/many', function() {
t.test('server should upload file', async function() {
let res = await client.upload('/file/upload/many', {
herp: './test/test.jpg',
derp: './test/test.png',
})
let [statSourcePng, statSourceJpg, statTargetHerp, statTargetDerp] = await Promise.all([
fs.stat('./test/test.png'),
fs.stat('./test/test.jpg'),
fs.stat(res.herp.path),
fs.stat(res.derp.path),
])
assert.strictEqual(statSourceJpg.size, statTargetHerp.size)
assert.strictEqual(statSourceJpg.size, res.herp.size)
assert.strictEqual(statSourcePng.size, statTargetDerp.size)
assert.strictEqual(statSourcePng.size, res.derp.size)
})
})
t.describe('HEAD', function() {
const agent = new http.Agent({
keepAlive: true,
maxSockets: 1,
keepAliveMsecs: 3000,
})
t.describe('/file', function() {
t.test('server return HEAD for pipes', async function() {
log.error.reset()
let res = await client.customRequest('HEAD', '/file', null, { getRaw: true, agent: agent })
while (!file.closed) {
await setTimeout(10)
}
assert.ok(file.closed)
let statSource = await fs.stat('./test/test.png')
assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['content-type'], 'image/png')
assert.notOk(res.headers['content-length'])
})
t.test('server should autoclose body file handles on errors', async function() {
reset()
file = null
let req = await client.customRequest('HEAD', '/file/leak', null, { returnRequest: true })
req.end()
while (!file) {
await setTimeout(10)
}
assert.ok(file)
assert.notOk(file.closed)
req.destroy()
while (!file.closed) {
await setTimeout(10)
}
assert.strictEqual(log.error.callCount, 0)
assert.strictEqual(log.info.callCount, 0)
assert.ok(file.closed)
})
})
t.describe('/filehandle', function() {
t.test('server should send correctly', async function() {
log.error.reset()
let res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent })
assert.strictEqual(res.status, 200)
assert.strictEqual(res.headers['content-length'], '11')
assert.strictEqual(res.headers['content-type'], 'text/plain')
assert.match(res.headers['etag'], /\"\d+-11-\d+"/)
assert.ok(res.headers['last-modified'])
let d = new Date(Date.parse(res.headers['last-modified']))
let etag = res.headers['etag']
assert.ok(d.getTime())
assert.strictEqual(res.data, '')
res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'If-Modified-Since': d.toUTCString()
},
})
assert.strictEqual(res.status, 304)
assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['etag'], etag)
res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'If-None-Match': etag
},
})
assert.strictEqual(res.status, 304)
assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['etag'], etag)
res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'Range': 'bytes=2-5'
},
})
assert.strictEqual(res.status, 206)
assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['content-length'], '4')
res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'Range': 'bytes=0-0'
},
})
assert.strictEqual(res.status, 206)
assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['content-length'], '1')
})
t.after(function() {
agent.destroy()
})
})
})

View File

@ -2,7 +2,7 @@ import os from 'os'
import path from 'path'
import { Buffer } from 'buffer'
import { Eltro as t, assert, stub} from 'eltro'
import { QueryHandler, JsonHandler, FormidableHandler, HttpError } from '../flaska.mjs'
import { QueryHandler, JsonHandler, FormidableHandler, HttpError, CorsHandler } from '../flaska.mjs'
import { createCtx } from './helper.mjs'
import { finished } from 'stream'
import { setTimeout } from 'timers/promises'
@ -29,6 +29,409 @@ t.describe('#QueryHandler()', function() {
})
})
t.describe('#CorsHandler()', function() {
let corsHandler
let ctx
t.test('should return a handler', function() {
corsHandler = CorsHandler()
assert.strictEqual(typeof(corsHandler), 'function')
})
t.describe('OPTIONS', function() {
t.beforeEach(function() {
ctx = createCtx()
ctx.method = 'OPTIONS'
})
t.test('should set status and headers', function() {
const assertOrigin = 'http://my.site.here'
const assertRequestHeaders = 'asdf,foobar'
corsHandler = CorsHandler({
allowedOrigins: [assertOrigin],
})
ctx.req.headers['origin'] = assertOrigin
ctx.req.headers['access-control-request-method'] = 'GET'
ctx.req.headers['access-control-request-headers'] = assertRequestHeaders
assert.notOk(ctx.headers['Vary'])
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin)
assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH')
assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders)
assert.notOk(ctx.headers['Access-Control-Allow-Credentials'])
assert.notOk(ctx.headers['Access-Control-Max-Age'])
assert.strictEqual(ctx.status, 204)
})
t.test('should set Allow-Credentials if credentials is specified', function() {
const assertOrigin = 'http://my.site.here'
const assertRequestHeaders = 'asdf,foobar'
corsHandler = CorsHandler({
allowedOrigins: [assertOrigin],
credentials: true,
})
ctx.req.headers['origin'] = assertOrigin
ctx.req.headers['access-control-request-method'] = 'GET'
ctx.req.headers['access-control-request-headers'] = assertRequestHeaders
assert.notOk(ctx.headers['Vary'])
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.strictEqual(ctx.headers['Access-Control-Allow-Credentials'], 'true')
assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin)
assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH')
assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders)
assert.notOk(ctx.headers['Access-Control-Max-Age'])
assert.strictEqual(ctx.status, 204)
})
t.test('should set Max-Age if maxAge is specified', function() {
const assertOrigin = 'http://my.site.here'
const assertRequestHeaders = 'asdf,foobar'
const assertMaxAge = '600'
corsHandler = CorsHandler({
allowedOrigins: [assertOrigin],
maxAge: assertMaxAge,
})
ctx.req.headers['origin'] = assertOrigin
ctx.req.headers['access-control-request-method'] = 'GET'
ctx.req.headers['access-control-request-headers'] = assertRequestHeaders
assert.notOk(ctx.headers['Vary'])
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin)
assert.strictEqual(ctx.headers['Access-Control-Max-Age'], assertMaxAge)
assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH')
assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders)
assert.notOk(ctx.headers['Access-Control-Allow-Credentials'])
assert.strictEqual(ctx.status, 204)
})
t.test('should support custom allowed methods and headers', function() {
const assertOrigin = 'http://my.site.here'
const assertAllowedMethods = 'GET,HEAD'
const assertAllowedHeaders = 'test1,test2'
corsHandler = CorsHandler({
allowedOrigins: [assertOrigin],
allowedMethods: assertAllowedMethods,
allowedHeaders: assertAllowedHeaders,
})
ctx.req.headers['origin'] = assertOrigin
ctx.req.headers['access-control-request-method'] = 'GET'
ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad'
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.notOk(ctx.headers['Access-Control-Allow-Credentials'])
assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin)
assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods)
assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertAllowedHeaders)
assert.strictEqual(ctx.status, 204)
})
t.test('should not set any allowed headers if allowedHeaders is explicitly false', function() {
const assertOrigin = 'http://my.site.here'
const assertAllowedMethods = 'GET,HEAD'
corsHandler = CorsHandler({
allowedOrigins: [assertOrigin],
allowedMethods: assertAllowedMethods,
allowedHeaders: false,
})
ctx.req.headers['origin'] = assertOrigin
ctx.req.headers['access-control-request-method'] = 'GET'
ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad'
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.notOk(ctx.headers['Access-Control-Allow-Credentials'])
assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin)
assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods)
assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], undefined)
assert.strictEqual(ctx.status, 204)
})
t.test('should not add any headers if origin missing', function() {
corsHandler = CorsHandler({
allowedOrigins: ['https://test.com'],
})
ctx.req.headers['origin'] = null
ctx.req.headers['access-control-request-method'] = 'GET'
ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad'
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
assert.strictEqual(ctx.status, 204)
})
t.test('should not add any headers if origin not found', function() {
const assertOrigin = 'http://my.site.here'
const assertAllowedMethods = 'GET,HEAD'
corsHandler = CorsHandler({
allowedOrigins: ['https://my.site.here'],
allowedMethods: assertAllowedMethods,
allowedHeaders: false,
})
ctx.req.headers['origin'] = assertOrigin
ctx.req.headers['access-control-request-method'] = 'GET'
ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad'
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
assert.strictEqual(ctx.status, 204)
})
t.test('should not add any headers if request-method is missing', function() {
const assertOrigin = 'http://my.site.here'
corsHandler = CorsHandler({
allowedOrigins: ['http://my.site.here'],
})
ctx.req.headers['origin'] = assertOrigin
delete ctx.req.headers['access-control-request-method']
ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad'
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
assert.strictEqual(ctx.status, 204)
})
t.test('should set headers if allowedOrigins has a *', function() {
const assertOrigin = 'http://my.site.here'
corsHandler = CorsHandler({
allowedOrigins: ['*'],
})
ctx.req.headers['origin'] = assertOrigin
ctx.req.headers['access-control-request-method'] = 'GET'
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin)
assert.ok(ctx.headers['Access-Control-Allow-Methods'])
assert.strictEqual(ctx.status, 204)
})
})
t.describe('GET/POST/DELETE/PATCH/PUT', function() {
let testMethods = ['GET', 'POST', 'DELETE', 'PATCH', 'PUT']
t.test('should set header but no status', function() {
testMethods.forEach(function(method) {
ctx = createCtx()
ctx.method = method
ctx.status = method
const assertOrigin = 'http://my.site.here'
corsHandler = CorsHandler({
allowedOrigins: [assertOrigin],
})
ctx.req.headers['origin'] = assertOrigin
ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad'
assert.notOk(ctx.headers['Vary'])
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin)
assert.notOk(ctx.headers['Access-Control-Allow-Credentials'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
assert.notOk(ctx.headers['Access-Control-Expose-Headers'])
assert.strictEqual(ctx.status, method)
})
})
t.test('should set credential header if specifed', function() {
testMethods.forEach(function(method) {
ctx = createCtx()
ctx.method = method
ctx.status = method
const assertOrigin = 'http://my.site.here'
corsHandler = CorsHandler({
allowedOrigins: [assertOrigin],
credentials: true,
})
ctx.req.headers['origin'] = assertOrigin
assert.notOk(ctx.headers['Vary'])
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.strictEqual(ctx.headers['Access-Control-Allow-Credentials'], 'true')
assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin)
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
assert.notOk(ctx.headers['Access-Control-Expose-Headers'])
assert.strictEqual(ctx.status, method)
})
})
t.test('should set expose headers if specifed', function() {
testMethods.forEach(function(method) {
const assertExposeHeaders = 'Some, Test, Here'
ctx = createCtx()
ctx.method = method
ctx.status = method
const assertOrigin = 'http://my.site.here'
corsHandler = CorsHandler({
allowedOrigins: [assertOrigin],
exposeHeaders: assertExposeHeaders,
})
ctx.req.headers['origin'] = assertOrigin
assert.notOk(ctx.headers['Vary'])
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin)
assert.notOk(ctx.headers['Access-Control-Allow-Credentials'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
assert.strictEqual(ctx.headers['Access-Control-Expose-Headers'], assertExposeHeaders)
assert.strictEqual(ctx.status, method)
})
})
t.test('should not add any headers if origin missing', function() {
testMethods.forEach(function(method) {
ctx = createCtx()
ctx.method = method
ctx.status = method
corsHandler = CorsHandler({
allowedOrigins: ['https://test.com'],
})
ctx.req.headers['origin'] = null
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
assert.notOk(ctx.headers['Access-Control-Expose-Headers'])
assert.strictEqual(ctx.status, method)
})
})
t.test('should not add any headers if origin not found', function() {
testMethods.forEach(function(method) {
ctx = createCtx()
ctx.method = method
ctx.status = method
const assertOrigin = 'http://my.site.here'
const assertAllowedMethods = 'GET,HEAD'
corsHandler = CorsHandler({
allowedOrigins: ['https://my.site.here'],
allowedMethods: assertAllowedMethods,
allowedHeaders: false,
})
ctx.req.headers['origin'] = assertOrigin
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
corsHandler(ctx)
assert.strictEqual(ctx.headers['Vary'], 'Origin')
assert.notOk(ctx.headers['Access-Control-Allow-Origin'])
assert.notOk(ctx.headers['Access-Control-Allow-Methods'])
assert.notOk(ctx.headers['Access-Control-Allow-Headers'])
assert.notOk(ctx.headers['Access-Control-Expose-Headers'])
assert.strictEqual(ctx.status, method)
})
})
})
})
t.describe('#JsonHandler()', function() {
let jsonHandler = JsonHandler()
let ctx
@ -116,8 +519,8 @@ t.describe('#JsonHandler()', function() {
finished = true
})
ctx.req.on.firstCall[1](Buffer.alloc(10, 'a'))
ctx.req.on.firstCall[1](Buffer.alloc(10, 'a'))
ctx.req.on.firstCall[1](Buffer.alloc(10, 'X'))
ctx.req.on.firstCall[1](Buffer.alloc(10, 'X'))
ctx.req.on.secondCall[1]()
await setTimeout(10)
@ -128,11 +531,11 @@ t.describe('#JsonHandler()', function() {
assert.ok(err instanceof HttpError)
assert.strictEqual(err.status, 400)
assert.match(err.message, /JSON/)
assert.match(err.message, /Unexpected token a in/i)
assert.match(err.message, /Unexpected token[^X]+X/i)
assert.strictEqual(err.body.status, 400)
assert.match(err.body.message, /Invalid JSON/i)
assert.match(err.body.message, /Unexpected token a in/i)
assert.strictEqual(err.body.request, 'aaaaaaaaaaaaaaaaaaaa')
assert.match(err.body.message, /Unexpected token[^X]+X/i)
assert.strictEqual(err.body.request, 'XXXXXXXXXXXXXXXXXXXX')
})
t.test('should not throw if body is empty', async function() {
@ -233,7 +636,10 @@ t.describe('#FormidableHandler()', function() {
let err = await assert.isRejected(handler(ctx))
assert.strictEqual(err, assertError)
assert.notStrictEqual(err, assertError)
assert.ok(err instanceof HttpError)
assert.strictEqual(err.message, assertError.message)
assert.strictEqual(err.status, 400)
})
t.test('should throw rename error if rename fails', async function() {

BIN
test/test.gibberish Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
test/test.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

1
test/test.txt Normal file
View File

@ -0,0 +1 @@
Hello World