FileResponse: Completely finished all HTTP/1.1 support for file handling including range, preconditions, modified and so much more.
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
Flaska: Refactored how headers and status are sent.
This commit is contained in:
parent
cfd56f83ae
commit
499cfa8ce0
10 changed files with 673 additions and 56 deletions
95
flaska.mjs
95
flaska.mjs
|
@ -4,6 +4,7 @@ import path from 'path'
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
import stream from 'stream'
|
import stream from 'stream'
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
|
import fsSync from 'fs'
|
||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
import { Buffer } from 'buffer'
|
import { Buffer } from 'buffer'
|
||||||
import zlib from 'zlib'
|
import zlib from 'zlib'
|
||||||
|
@ -294,6 +295,88 @@ export class FileResponse {
|
||||||
this.filepath = filepath
|
this.filepath = filepath
|
||||||
this.stat = stat
|
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 = ctx.req.headers['range'].match(/bytes=(\d+)-(\d+)?/)
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = useFs.createReadStream(this.filepath, readOptions)
|
||||||
|
ctx.headers['Last-Modified'] = lastModified
|
||||||
|
ctx.headers['Content-Length'] = size
|
||||||
|
return stream
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FlaskaRouter {
|
export class FlaskaRouter {
|
||||||
|
@ -579,6 +662,7 @@ ctx.state.nonce = nonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructFunction += 'ctx.headers = {'
|
constructFunction += 'ctx.headers = {'
|
||||||
|
constructFunction += `'Date': new Date().toUTCString(),`
|
||||||
for (let key of headerKeys) {
|
for (let key of headerKeys) {
|
||||||
if (key === 'Content-Security-Policy' && options.nonce.length) {
|
if (key === 'Content-Security-Policy' && options.nonce.length) {
|
||||||
let groups = options.defaultHeaders[key].split(';')
|
let groups = options.defaultHeaders[key].split(';')
|
||||||
|
@ -822,7 +906,15 @@ ctx.state.nonce = nonce;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestEnd(err, ctx) {
|
requestEnd(orgErr, ctx) {
|
||||||
|
let err = orgErr
|
||||||
|
if (ctx.body && ctx.body.handleRequest) {
|
||||||
|
try {
|
||||||
|
ctx.body = ctx.body.handleRequest(ctx)
|
||||||
|
} catch (newErr) {
|
||||||
|
err = newErr
|
||||||
|
}
|
||||||
|
}
|
||||||
if (err) {
|
if (err) {
|
||||||
try {
|
try {
|
||||||
this._onerror(err, ctx)
|
this._onerror(err, ctx)
|
||||||
|
@ -835,7 +927,6 @@ ctx.state.nonce = nonce;
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.res.statusCode = ctx.status
|
|
||||||
if (statuses.empty[ctx.status]) {
|
if (statuses.empty[ctx.status]) {
|
||||||
ctx.res.writeHead(ctx.status, ctx.headers)
|
ctx.res.writeHead(ctx.status, ctx.headers)
|
||||||
return ctx.res.end()
|
return ctx.res.end()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "flaska",
|
"name": "flaska",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"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.",
|
"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",
|
"main": "flaska.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -29,6 +29,12 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options =
|
||||||
headers: {},
|
headers: {},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
if (options.agent) {
|
||||||
|
opts.agent = options.agent
|
||||||
|
}
|
||||||
|
|
||||||
|
// opts.agent = agent
|
||||||
|
|
||||||
const req = http.request(opts)
|
const req = http.request(opts)
|
||||||
|
|
||||||
req.on('error', (err) => {
|
req.on('error', (err) => {
|
||||||
|
@ -56,13 +62,21 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options =
|
||||||
}
|
}
|
||||||
|
|
||||||
res.on('end', function () {
|
res.on('end', function () {
|
||||||
|
if (options.getRaw) {
|
||||||
|
output = {
|
||||||
|
status: res.statusCode,
|
||||||
|
data: output,
|
||||||
|
headers: res.headers,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (!output) return resolve(null)
|
if (!output) return resolve(null)
|
||||||
try {
|
try {
|
||||||
output = JSON.parse(output)
|
output = JSON.parse(output)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return reject(new Error(`${e.message} while decoding: ${output}`))
|
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}`)
|
let err = new Error(`Request failed [${output.status}]: ${output.message}`)
|
||||||
err.body = output
|
err.body = output
|
||||||
return reject(err)
|
return reject(err)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Eltro as t, assert, stub } from 'eltro'
|
import { Eltro as t, assert, stub } from 'eltro'
|
||||||
import { FileResponse } from '../flaska.mjs'
|
import { FileResponse } from '../flaska.mjs'
|
||||||
|
import { createCtx } from './helper.mjs'
|
||||||
|
|
||||||
t.test('should add path and stat', function() {
|
t.test('should add path and stat', function() {
|
||||||
const assertPath = 'some/path/here'
|
const assertPath = 'some/path/here'
|
||||||
|
@ -12,8 +13,389 @@ t.test('should add path and stat', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.describe('CreateReader()', function() {
|
t.describe('CreateReader()', function() {
|
||||||
t.test('should return fileReader for file with correct seeking', function() {
|
const assertPath = '/some/path/here.png'
|
||||||
const stubCreateReadStream = stub()
|
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('return fileReader ' + 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('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
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -43,21 +43,25 @@ t.describe('#constructor', function() {
|
||||||
|
|
||||||
flaska._before[0](ctx)
|
flaska._before[0](ctx)
|
||||||
|
|
||||||
assert.deepStrictEqual(ctx.headers, {
|
assert.deepEqual(
|
||||||
'Server': 'Flaska',
|
Object.keys(ctx.headers).sort(),
|
||||||
'X-Content-Type-Options': 'nosniff',
|
['Server','X-Content-Type-Options','Content-Security-Policy','Cross-Origin-Opener-Policy','Cross-Origin-Resource-Policy','Cross-Origin-Embedder-Policy','Date'].sort()
|
||||||
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'`,
|
)
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
||||||
'Cross-Origin-Resource-Policy': 'same-origin',
|
assert.strictEqual(ctx.headers['Server'], 'Flaska')
|
||||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
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:; 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)
|
assert.strictEqual(flaska._after.length, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should have before ready setting headers on context if defaultHeaders is specified', function() {
|
t.test('should have before ready setting headers on context if defaultHeaders is specified', function() {
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
'Server': 'nginx/1.16.1',
|
'Server': 'nginx/1.16.1',
|
||||||
'Date': 'Mon, 21 Mar\' 2022 07:26:01 GMT',
|
|
||||||
'Content-Type': 'applicat"ion/json; charset=utf-8',
|
'Content-Type': 'applicat"ion/json; charset=utf-8',
|
||||||
'Content-Length': '1646',
|
'Content-Length': '1646',
|
||||||
'Connection': 'keep-alive',
|
'Connection': 'keep-alive',
|
||||||
|
@ -75,8 +79,15 @@ t.describe('#constructor', function() {
|
||||||
|
|
||||||
flaska._before[0](ctx)
|
flaska._before[0](ctx)
|
||||||
|
|
||||||
assert.notStrictEqual(ctx.headers, defaultHeaders)
|
let keys = Object.keys(defaultHeaders)
|
||||||
assert.deepStrictEqual(ctx.headers, defaultHeaders)
|
console.log(Object.keys(ctx.headers).sort())
|
||||||
|
console.log(keys.sort())
|
||||||
|
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)
|
assert.strictEqual(flaska._after.length, 0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -111,14 +122,14 @@ t.describe('#_nonce', function() {
|
||||||
assert.strictEqual(flaska._noncesIndex, oldIndex - 1)
|
assert.strictEqual(flaska._noncesIndex, oldIndex - 1)
|
||||||
assert.strictEqual(flaska._nonces[oldIndex], ctx.state.nonce)
|
assert.strictEqual(flaska._nonces[oldIndex], ctx.state.nonce)
|
||||||
|
|
||||||
assert.deepStrictEqual(ctx.headers, {
|
|
||||||
'Server': 'Flaska',
|
assert.strictEqual(ctx.headers['Server'], 'Flaska')
|
||||||
'X-Content-Type-Options': 'nosniff',
|
assert.strictEqual(ctx.headers['X-Content-Type-Options'], 'nosniff')
|
||||||
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline' 'nonce-${ctx.state.nonce}'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`,
|
assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline' 'nonce-${ctx.state.nonce}'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`)
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
assert.strictEqual(ctx.headers['Cross-Origin-Opener-Policy'], 'same-origin')
|
||||||
'Cross-Origin-Resource-Policy': 'same-origin',
|
assert.strictEqual(ctx.headers['Cross-Origin-Resource-Policy'], 'same-origin')
|
||||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
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() {
|
t.test('should always return nonce values even if it runs out in cache', function() {
|
||||||
|
|
|
@ -153,14 +153,18 @@ t.describe('#requestStart()', function() {
|
||||||
assert.ok(ctx.query.get)
|
assert.ok(ctx.query.get)
|
||||||
assert.ok(ctx.query.set)
|
assert.ok(ctx.query.set)
|
||||||
assert.ok(ctx.query.delete)
|
assert.ok(ctx.query.delete)
|
||||||
assert.deepStrictEqual(ctx.headers, {
|
assert.deepEqual(
|
||||||
'Server': 'Flaska',
|
Object.keys(ctx.headers).sort(),
|
||||||
'X-Content-Type-Options': 'nosniff',
|
['Server','X-Content-Type-Options','Content-Security-Policy','Cross-Origin-Opener-Policy','Cross-Origin-Resource-Policy','Cross-Origin-Embedder-Policy','Date'].sort()
|
||||||
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'`,
|
)
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
||||||
'Cross-Origin-Resource-Policy': 'same-origin',
|
assert.strictEqual(ctx.headers['Server'], 'Flaska')
|
||||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
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:; 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())
|
||||||
cb()
|
cb()
|
||||||
} catch (err) { cb(err) }
|
} catch (err) { cb(err) }
|
||||||
}
|
}
|
||||||
|
@ -194,8 +198,12 @@ t.describe('#requestStart()', function() {
|
||||||
try {
|
try {
|
||||||
assert.ok(err)
|
assert.ok(err)
|
||||||
assert.strictEqual(err, assertError)
|
assert.strictEqual(err, assertError)
|
||||||
assert.notStrictEqual(ctx.headers, defaultHeaders)
|
let keys = Object.keys(defaultHeaders)
|
||||||
assert.deepStrictEqual(ctx.headers, 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'])
|
||||||
cb()
|
cb()
|
||||||
} catch (err) { cb(err) }
|
} catch (err) { cb(err) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Eltro as t, spy, assert, stub} from 'eltro'
|
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'
|
import { fakeHttp, createCtx } from './helper.mjs'
|
||||||
|
|
||||||
const fakerHttp = fakeHttp()
|
const fakerHttp = fakeHttp()
|
||||||
|
@ -16,7 +16,6 @@ t.describe('#requestEnd()', function() {
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(ctx.status, assertStatus)
|
assert.strictEqual(ctx.status, assertStatus)
|
||||||
assert.strictEqual(ctx.body, assertBody)
|
assert.strictEqual(ctx.body, assertBody)
|
||||||
assert.strictEqual(ctx.res.statusCode, assertStatus)
|
|
||||||
assert.strictEqual(ctx.headers['Content-Type'], 'application/json; charset=utf-8')
|
assert.strictEqual(ctx.headers['Content-Type'], 'application/json; charset=utf-8')
|
||||||
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
|
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
|
||||||
assert.ok(body)
|
assert.ok(body)
|
||||||
|
@ -71,7 +70,6 @@ t.describe('#requestEnd()', function() {
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(ctx.status, assertStatus)
|
assert.strictEqual(ctx.status, assertStatus)
|
||||||
assert.strictEqual(ctx.body, assertBody)
|
assert.strictEqual(ctx.body, assertBody)
|
||||||
assert.strictEqual(ctx.res.statusCode, assertStatus)
|
|
||||||
assert.strictEqual(ctx.headers['Content-Type'], 'application/json; charset=utf-8')
|
assert.strictEqual(ctx.headers['Content-Type'], 'application/json; charset=utf-8')
|
||||||
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
|
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
|
||||||
assert.ok(body)
|
assert.ok(body)
|
||||||
|
@ -100,7 +98,6 @@ t.describe('#requestEnd()', function() {
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(ctx.status, assertStatus)
|
assert.strictEqual(ctx.status, assertStatus)
|
||||||
assert.strictEqual(ctx.body, assertBody)
|
assert.strictEqual(ctx.body, assertBody)
|
||||||
assert.strictEqual(ctx.res.statusCode, assertStatus)
|
|
||||||
assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8')
|
assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8')
|
||||||
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
|
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
|
||||||
assert.ok(body)
|
assert.ok(body)
|
||||||
|
@ -129,7 +126,6 @@ t.describe('#requestEnd()', function() {
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(ctx.status, assertStatus)
|
assert.strictEqual(ctx.status, assertStatus)
|
||||||
assert.strictEqual(ctx.body, assertBody)
|
assert.strictEqual(ctx.body, assertBody)
|
||||||
assert.strictEqual(ctx.res.statusCode, assertStatus)
|
|
||||||
assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8')
|
assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8')
|
||||||
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
|
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
|
||||||
assert.ok(body)
|
assert.ok(body)
|
||||||
|
@ -149,6 +145,52 @@ t.describe('#requestEnd()', function() {
|
||||||
flaska.requestEnd(null, ctx)
|
flaska.requestEnd(null, ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.test('call handleRequest if body contains it', function() {
|
||||||
|
let body = new FileResponse()
|
||||||
|
let assertBody = { a : 1 }
|
||||||
|
const ctx = createCtx({
|
||||||
|
status: 204,
|
||||||
|
})
|
||||||
|
ctx.body = body
|
||||||
|
const assertHandle = stub().returnWith(function(checkCtx, secondary) {
|
||||||
|
assert.notOk(secondary)
|
||||||
|
assert.strictEqual(checkCtx, ctx)
|
||||||
|
assert.notOk(ctx.res.writeHead.called)
|
||||||
|
return assertBody
|
||||||
|
})
|
||||||
|
body.handleRequest = assertHandle
|
||||||
|
|
||||||
|
let flaska = new Flaska({}, fakerHttp, fakeStream)
|
||||||
|
flaska.requestEnd(null, ctx)
|
||||||
|
assert.ok(assertHandle.called)
|
||||||
|
assert.ok(ctx.res.writeHead)
|
||||||
|
assert.strictEqual(ctx.body, assertBody)
|
||||||
|
})
|
||||||
|
|
||||||
|
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: 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) {
|
t.test('call res and end correctly when dealing with custom type', function(cb) {
|
||||||
const assertStatus = 209
|
const assertStatus = 209
|
||||||
const assertBody = 'test'
|
const assertBody = 'test'
|
||||||
|
@ -156,7 +198,6 @@ t.describe('#requestEnd()', function() {
|
||||||
let onFinish = function(body) {
|
let onFinish = function(body) {
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(ctx.status, assertStatus)
|
assert.strictEqual(ctx.status, assertStatus)
|
||||||
assert.strictEqual(ctx.res.statusCode, assertStatus)
|
|
||||||
assert.strictEqual(ctx.headers['Content-Type'], assertType)
|
assert.strictEqual(ctx.headers['Content-Type'], assertType)
|
||||||
assert.strictEqual(body, assertBody)
|
assert.strictEqual(body, assertBody)
|
||||||
assert.ok(ctx.res.writeHead.called)
|
assert.ok(ctx.res.writeHead.called)
|
||||||
|
@ -183,7 +224,6 @@ t.describe('#requestEnd()', function() {
|
||||||
let onFinish = function(source, target, callback) {
|
let onFinish = function(source, target, callback) {
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(ctx.status, assertStatus)
|
assert.strictEqual(ctx.status, assertStatus)
|
||||||
assert.strictEqual(ctx.res.statusCode, assertStatus)
|
|
||||||
assert.strictEqual(ctx.headers['Content-Type'], assertType)
|
assert.strictEqual(ctx.headers['Content-Type'], assertType)
|
||||||
assert.strictEqual(source, assertBody)
|
assert.strictEqual(source, assertBody)
|
||||||
assert.strictEqual(target, ctx.res)
|
assert.strictEqual(target, ctx.res)
|
||||||
|
@ -208,7 +248,7 @@ t.describe('#requestEnd()', function() {
|
||||||
t.test('call pipe should have default type', function(cb) {
|
t.test('call pipe should have default type', function(cb) {
|
||||||
let onFinish = function(source, target) {
|
let onFinish = function(source, target) {
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(ctx.res.statusCode, 200)
|
assert.strictEqual(ctx.status, 200)
|
||||||
assert.strictEqual(ctx.headers['Content-Type'], 'application/octet-stream')
|
assert.strictEqual(ctx.headers['Content-Type'], 'application/octet-stream')
|
||||||
assert.strictEqual(source, ctx.body)
|
assert.strictEqual(source, ctx.body)
|
||||||
assert.strictEqual(target, ctx.res)
|
assert.strictEqual(target, ctx.res)
|
||||||
|
@ -237,7 +277,7 @@ t.describe('#requestEnd()', function() {
|
||||||
t.test(`call pipe with file extension ${test[0]} should mimetype ${test[1]}`, function(cb) {
|
t.test(`call pipe with file extension ${test[0]} should mimetype ${test[1]}`, function(cb) {
|
||||||
let onFinish = function(source, target) {
|
let onFinish = function(source, target) {
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(ctx.res.statusCode, 200)
|
assert.strictEqual(ctx.status, 200)
|
||||||
assert.strictEqual(ctx.headers['Content-Type'], test[1])
|
assert.strictEqual(ctx.headers['Content-Type'], test[1])
|
||||||
assert.strictEqual(source, ctx.body)
|
assert.strictEqual(source, ctx.body)
|
||||||
assert.strictEqual(target, ctx.res)
|
assert.strictEqual(target, ctx.res)
|
||||||
|
@ -277,7 +317,6 @@ t.describe('#requestEnd()', function() {
|
||||||
let onFinish = function(body) {
|
let onFinish = function(body) {
|
||||||
try {
|
try {
|
||||||
assert.strictEqual(ctx.status, assertStatus)
|
assert.strictEqual(ctx.status, assertStatus)
|
||||||
assert.strictEqual(ctx.res.statusCode, assertStatus)
|
|
||||||
assert.strictEqual(ctx.res.setHeader.callCount, 0)
|
assert.strictEqual(ctx.res.setHeader.callCount, 0)
|
||||||
assert.ok(ctx.res.writeHead.called)
|
assert.ok(ctx.res.writeHead.called)
|
||||||
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
|
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)
|
||||||
|
|
|
@ -41,7 +41,7 @@ export function createRes(def) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCtx(def, endHandler) {
|
export function createCtx(def = {}, endHandler = null) {
|
||||||
return defaults(def, {
|
return defaults(def, {
|
||||||
req: createReq(),
|
req: createReq(),
|
||||||
res: createRes({ end: endHandler || spy() }),
|
res: createRes({ end: endHandler || spy() }),
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import fsSync from 'fs'
|
import fsSync from 'fs'
|
||||||
|
import http from 'http'
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import formidable from 'formidable'
|
import formidable from 'formidable'
|
||||||
import { Eltro as t, assert, stub } from 'eltro'
|
import { Eltro as t, assert, stub } from 'eltro'
|
||||||
import { setTimeout } from 'timers/promises'
|
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'
|
import Client from './client.mjs'
|
||||||
|
|
||||||
const port = 51024
|
const port = 51024
|
||||||
|
@ -37,6 +38,11 @@ flaska.get('/file', function(ctx) {
|
||||||
file = fsSync.createReadStream('./test/test.png')
|
file = fsSync.createReadStream('./test/test.png')
|
||||||
ctx.body = file
|
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, {
|
flaska.post('/file/upload', FormidableHandler(formidable, {
|
||||||
uploadDir: './test/upload'
|
uploadDir: './test/upload'
|
||||||
}), function(ctx) {
|
}), function(ctx) {
|
||||||
|
@ -139,7 +145,9 @@ t.describe('/timeout', function() {
|
||||||
|
|
||||||
let err = await assert.isRejected(client.customRequest('GET', '/timeout', JSON.stringify({}), { timeout: 20 }))
|
let err = await assert.isRejected(client.customRequest('GET', '/timeout', JSON.stringify({}), { timeout: 20 }))
|
||||||
|
|
||||||
await setTimeout(20)
|
while (!log.info.called) {
|
||||||
|
await setTimeout(10)
|
||||||
|
}
|
||||||
|
|
||||||
assert.match(err.message, /timed out/)
|
assert.match(err.message, /timed out/)
|
||||||
assert.notOk(log.error.called)
|
assert.notOk(log.error.called)
|
||||||
|
@ -156,7 +164,9 @@ t.describe('/file', function() {
|
||||||
|
|
||||||
await client.customRequest('GET', '/file', null, { toPipe: target })
|
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(target.closed)
|
||||||
assert.ok(file.closed)
|
assert.ok(file.closed)
|
||||||
|
@ -186,8 +196,6 @@ t.describe('/file', function() {
|
||||||
|
|
||||||
req.destroy()
|
req.destroy()
|
||||||
|
|
||||||
await setTimeout(20)
|
|
||||||
|
|
||||||
while (!file.closed) {
|
while (!file.closed) {
|
||||||
await setTimeout(10)
|
await setTimeout(10)
|
||||||
}
|
}
|
||||||
|
@ -200,6 +208,69 @@ t.describe('/file', function() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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.describe('/file/upload', function() {
|
||||||
t.test('server should upload file', async function() {
|
t.test('server should upload file', async function() {
|
||||||
let res = await client.upload('/file/upload', './test/test.png')
|
let res = await client.upload('/file/upload', './test/test.png')
|
||||||
|
|
1
test/test.txt
Normal file
1
test/test.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Hello World
|
Loading…
Reference in a new issue