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 stream from 'stream'
|
||||
import fs from 'fs/promises'
|
||||
import fsSync from 'fs'
|
||||
import { URL } from 'url'
|
||||
import { Buffer } from 'buffer'
|
||||
import zlib from 'zlib'
|
||||
|
@ -294,6 +295,88 @@ export class FileResponse {
|
|||
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 = 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 {
|
||||
|
@ -579,6 +662,7 @@ 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(';')
|
||||
|
@ -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) {
|
||||
try {
|
||||
this._onerror(err, ctx)
|
||||
|
@ -835,7 +927,6 @@ ctx.state.nonce = nonce;
|
|||
return
|
||||
}
|
||||
|
||||
ctx.res.statusCode = ctx.status
|
||||
if (statuses.empty[ctx.status]) {
|
||||
ctx.res.writeHead(ctx.status, ctx.headers)
|
||||
return ctx.res.end()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"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.",
|
||||
"main": "flaska.mjs",
|
||||
"scripts": {
|
||||
|
|
|
@ -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 (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)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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'
|
||||
|
@ -12,8 +13,389 @@ t.test('should add path and stat', function() {
|
|||
})
|
||||
|
||||
t.describe('CreateReader()', function() {
|
||||
t.test('should return fileReader for file with correct seeking', function() {
|
||||
const stubCreateReadStream = stub()
|
||||
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('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)
|
||||
|
||||
assert.deepStrictEqual(ctx.headers, {
|
||||
'Server': 'Flaska',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'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',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
})
|
||||
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:; 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',
|
||||
'Date': 'Mon, 21 Mar\' 2022 07:26:01 GMT',
|
||||
'Content-Type': 'applicat"ion/json; charset=utf-8',
|
||||
'Content-Length': '1646',
|
||||
'Connection': 'keep-alive',
|
||||
|
@ -75,8 +79,15 @@ t.describe('#constructor', function() {
|
|||
|
||||
flaska._before[0](ctx)
|
||||
|
||||
assert.notStrictEqual(ctx.headers, defaultHeaders)
|
||||
assert.deepStrictEqual(ctx.headers, defaultHeaders)
|
||||
let keys = Object.keys(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)
|
||||
})
|
||||
})
|
||||
|
@ -111,14 +122,14 @@ t.describe('#_nonce', function() {
|
|||
assert.strictEqual(flaska._noncesIndex, oldIndex - 1)
|
||||
assert.strictEqual(flaska._nonces[oldIndex], ctx.state.nonce)
|
||||
|
||||
assert.deepStrictEqual(ctx.headers, {
|
||||
'Server': 'Flaska',
|
||||
'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}'`,
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Resource-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
})
|
||||
|
||||
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:; 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() {
|
||||
|
|
|
@ -153,14 +153,18 @@ t.describe('#requestStart()', function() {
|
|||
assert.ok(ctx.query.get)
|
||||
assert.ok(ctx.query.set)
|
||||
assert.ok(ctx.query.delete)
|
||||
assert.deepStrictEqual(ctx.headers, {
|
||||
'Server': 'Flaska',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'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',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
})
|
||||
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:; 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()
|
||||
} catch (err) { cb(err) }
|
||||
}
|
||||
|
@ -194,8 +198,12 @@ t.describe('#requestStart()', function() {
|
|||
try {
|
||||
assert.ok(err)
|
||||
assert.strictEqual(err, assertError)
|
||||
assert.notStrictEqual(ctx.headers, defaultHeaders)
|
||||
assert.deepStrictEqual(ctx.headers, defaultHeaders)
|
||||
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'])
|
||||
cb()
|
||||
} catch (err) { cb(err) }
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
@ -16,7 +16,6 @@ t.describe('#requestEnd()', function() {
|
|||
try {
|
||||
assert.strictEqual(ctx.status, assertStatus)
|
||||
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-Length'], assertBodyLength)
|
||||
assert.ok(body)
|
||||
|
@ -71,7 +70,6 @@ t.describe('#requestEnd()', function() {
|
|||
try {
|
||||
assert.strictEqual(ctx.status, assertStatus)
|
||||
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-Length'], assertBodyLength)
|
||||
assert.ok(body)
|
||||
|
@ -100,7 +98,6 @@ t.describe('#requestEnd()', function() {
|
|||
try {
|
||||
assert.strictEqual(ctx.status, assertStatus)
|
||||
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-Length'], assertBodyLength)
|
||||
assert.ok(body)
|
||||
|
@ -129,7 +126,6 @@ t.describe('#requestEnd()', function() {
|
|||
try {
|
||||
assert.strictEqual(ctx.status, assertStatus)
|
||||
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-Length'], assertBodyLength)
|
||||
assert.ok(body)
|
||||
|
@ -149,6 +145,52 @@ t.describe('#requestEnd()', function() {
|
|||
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) {
|
||||
const assertStatus = 209
|
||||
const assertBody = 'test'
|
||||
|
@ -156,7 +198,6 @@ t.describe('#requestEnd()', function() {
|
|||
let onFinish = function(body) {
|
||||
try {
|
||||
assert.strictEqual(ctx.status, assertStatus)
|
||||
assert.strictEqual(ctx.res.statusCode, assertStatus)
|
||||
assert.strictEqual(ctx.headers['Content-Type'], assertType)
|
||||
assert.strictEqual(body, assertBody)
|
||||
assert.ok(ctx.res.writeHead.called)
|
||||
|
@ -183,7 +224,6 @@ t.describe('#requestEnd()', function() {
|
|||
let onFinish = function(source, target, callback) {
|
||||
try {
|
||||
assert.strictEqual(ctx.status, assertStatus)
|
||||
assert.strictEqual(ctx.res.statusCode, assertStatus)
|
||||
assert.strictEqual(ctx.headers['Content-Type'], assertType)
|
||||
assert.strictEqual(source, assertBody)
|
||||
assert.strictEqual(target, ctx.res)
|
||||
|
@ -208,7 +248,7 @@ t.describe('#requestEnd()', function() {
|
|||
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.status, 200)
|
||||
assert.strictEqual(ctx.headers['Content-Type'], 'application/octet-stream')
|
||||
assert.strictEqual(source, ctx.body)
|
||||
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) {
|
||||
let onFinish = function(source, target) {
|
||||
try {
|
||||
assert.strictEqual(ctx.res.statusCode, 200)
|
||||
assert.strictEqual(ctx.status, 200)
|
||||
assert.strictEqual(ctx.headers['Content-Type'], test[1])
|
||||
assert.strictEqual(source, ctx.body)
|
||||
assert.strictEqual(target, ctx.res)
|
||||
|
@ -277,7 +317,6 @@ t.describe('#requestEnd()', function() {
|
|||
let onFinish = function(body) {
|
||||
try {
|
||||
assert.strictEqual(ctx.status, assertStatus)
|
||||
assert.strictEqual(ctx.res.statusCode, assertStatus)
|
||||
assert.strictEqual(ctx.res.setHeader.callCount, 0)
|
||||
assert.ok(ctx.res.writeHead.called)
|
||||
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, {
|
||||
req: createReq(),
|
||||
res: createRes({ end: endHandler || spy() }),
|
||||
|
|
|
@ -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
|
||||
|
@ -37,6 +38,11 @@ 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) {
|
||||
|
@ -139,7 +145,9 @@ t.describe('/timeout', function() {
|
|||
|
||||
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.notOk(log.error.called)
|
||||
|
@ -156,7 +164,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,8 +196,6 @@ t.describe('/file', function() {
|
|||
|
||||
req.destroy()
|
||||
|
||||
await setTimeout(20)
|
||||
|
||||
while (!file.closed) {
|
||||
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.test('server should upload file', async function() {
|
||||
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