flaska/test/http.test.mjs

514 lines
15 KiB
JavaScript

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, FileResponse } from '../flaska.mjs'
import Client from './client.mjs'
const port = 51024
const log = {
fatal: stub(),
error: stub(),
warn: stub(),
info: stub(),
debug: stub(),
trace: stub(),
log: stub(),
}
const flaska = new Flaska({ log })
const client = new Client(port)
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 }
})
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() {})
})
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
return new Promise(function() {})
})
function reset() {
log.fatal.reset()
log.error.reset()
log.warn.reset()
log.info.reset()
log.debug.reset()
log.trace.reset()
log.log.reset()
}
t.before(function() {
return flaska.listenAsync(port)
})
t.describe('/', function() {
t.test('should return status true', function() {
return client.get('/').then(function(body) {
assert.deepEqual(body, { status: true })
})
})
})
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++) {
assertBody.a += 'aaaaaaaaaa'
}
reqBody = null
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() {
reset()
const assertBody = { a: '' }
for (let i = 0; i < 10300; i++) {
assertBody.a += 'aaaaaaaaaa'
}
let err = await assert.isRejected(client.post('/json', assertBody))
assert.strictEqual(err.body.status, 413)
assert.ok(log.error.called)
assert.match(log.error.firstCall[0].message, /10240/)
assert.strictEqual(log.error.firstCall[0].status, 413)
})
t.test('should fail if not a valid json', async function() {
reset()
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[^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[^X]+X/i)
})
t.test('should handle incomplete requests correctly', async function() {
reset()
let req = await client.customRequest('POST', '/json', 'aaaa', { returnRequest: true })
req.write('part1')
await setTimeout(20)
req.destroy()
await setTimeout(20)
assert.strictEqual(log.error.callCount, 0)
assert.strictEqual(log.info.callCount, 0)
// assert.strictEqual(log.info.firstCall[0].message, 'aborted')
})
})
t.describe('/timeout', function() {
t.test('server should handle timeout', async function() {
reset()
let err = await assert.isRejected(client.customRequest('GET', '/timeout', JSON.stringify({}), { timeout: 20 }))
assert.match(err.message, /timed out/)
assert.notOk(log.error.called)
assert.notOk(log.info.called)
})
})
t.describe('/file', function() {
t.test('server should pipe', async function() {
log.error.reset()
let target = fsSync.createWriteStream('./test_tmp.png')
await client.customRequest('GET', '/file', null, { toPipe: target })
while (!target.closed && !file.closed) {
await setTimeout(10)
}
assert.ok(target.closed)
assert.ok(file.closed)
let [statSource, statTarget] = await Promise.all([
fs.stat('./test/test.png'),
fs.stat('./test_tmp.png'),
])
assert.strictEqual(statSource.size, statTarget.size)
})
t.test('server should autoclose body file handles on errors', async function() {
reset()
file = null
let req = await client.customRequest('GET', '/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() {
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')
let [statSource, statTarget] = await Promise.all([
fs.stat('./test/test.png'),
fs.stat(res.path),
])
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()
})
})
})
t.after(function() {
return Promise.all([
fs.rm('./test_tmp.png', { force: true }),
Promise.all(uploaded.map(file => fs.rm(file.path, { force: true }))),
flaska.closeAsync(),
])
})