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.get('/', function(ctx) { ctx.body = { status: true } }) flaska.post('/json', JsonHandler(), function(ctx) { ctx.body = { success: true } 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.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.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) }) 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', 'aaaa')) assert.strictEqual(err.body.status, 400) assert.match(err.body.message, /invalid json/i) assert.match(err.body.message, /token a/i) assert.strictEqual(err.body.request, 'aaaa') assert.strictEqual(log.error.callCount, 1) assert.match(log.error.firstCall[0].message, /invalid json/i) assert.match(log.error.firstCall[0].message, /token a/i) }) 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, 1) 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 })) while (!log.info.called) { await setTimeout(10) } assert.match(err.message, /timed out/) assert.notOk(log.error.called) assert.ok(log.info.called) assert.strictEqual(log.info.firstCall[0].message, 'aborted') }) }) 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, 1) assert.strictEqual(log.info.firstCall[0].message, 'aborted') 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) }) }) 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(), ]) })