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(), ]) })