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, FormidableHandler, FileResponse } from '../flaska.mjs' import Client from './client.mjs' const port = 51025 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 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.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('/timeout', function() { t.test('server should handle timeout', async function() { reset() let err = await assert.isRejected(client.customRequest('GET', '/timeout', null, { 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(), ]) })