import os from 'os' import path from 'path' import { Buffer } from 'buffer' import { Eltro as t, assert, stub} from 'eltro' import { QueryHandler, JsonHandler, FormidableHandler, HttpError, CorsHandler } from '../flaska.mjs' import { createCtx } from './helper.mjs' import { finished } from 'stream' import { setTimeout } from 'timers/promises' t.describe('#QueryHandler()', function() { let queryHandler = QueryHandler() t.test('should return a handler', function() { assert.strictEqual(typeof(queryHandler), 'function') }) t.test('should support separating query from request url', function() { const assertItem1 = 'safdsfdsag' const assertItem2 = 'hello%20world' const ctx = { req: { url: `/some/path?item1=${assertItem1}&ITEM2=${assertItem2}` } } queryHandler(ctx) assert.strictEqual(ctx.query.get('item1'), assertItem1) assert.strictEqual(ctx.query.get('ITEM2'), 'hello world') }) }) t.describe('#CorsHandler()', function() { let corsHandler let ctx t.test('should return a handler', function() { corsHandler = CorsHandler() assert.strictEqual(typeof(corsHandler), 'function') }) t.describe('OPTIONS', function() { t.beforeEach(function() { ctx = createCtx() ctx.method = 'OPTIONS' }) t.test('should set status and headers', function() { const assertOrigin = 'http://my.site.here' const assertRequestHeaders = 'asdf,foobar' corsHandler = CorsHandler({ allowedOrigins: [assertOrigin], }) ctx.req.headers['origin'] = assertOrigin ctx.req.headers['access-control-request-method'] = 'GET' ctx.req.headers['access-control-request-headers'] = assertRequestHeaders assert.notOk(ctx.headers['Vary']) assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH') assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders) assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) assert.notOk(ctx.headers['Access-Control-Max-Age']) assert.strictEqual(ctx.status, 204) }) t.test('should set Allow-Credentials if credentials is specified', function() { const assertOrigin = 'http://my.site.here' const assertRequestHeaders = 'asdf,foobar' corsHandler = CorsHandler({ allowedOrigins: [assertOrigin], credentials: true, }) ctx.req.headers['origin'] = assertOrigin ctx.req.headers['access-control-request-method'] = 'GET' ctx.req.headers['access-control-request-headers'] = assertRequestHeaders assert.notOk(ctx.headers['Vary']) assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.strictEqual(ctx.headers['Access-Control-Allow-Credentials'], 'true') assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH') assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders) assert.notOk(ctx.headers['Access-Control-Max-Age']) assert.strictEqual(ctx.status, 204) }) t.test('should set Max-Age if maxAge is specified', function() { const assertOrigin = 'http://my.site.here' const assertRequestHeaders = 'asdf,foobar' const assertMaxAge = '600' corsHandler = CorsHandler({ allowedOrigins: [assertOrigin], maxAge: assertMaxAge, }) ctx.req.headers['origin'] = assertOrigin ctx.req.headers['access-control-request-method'] = 'GET' ctx.req.headers['access-control-request-headers'] = assertRequestHeaders assert.notOk(ctx.headers['Vary']) assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) assert.strictEqual(ctx.headers['Access-Control-Max-Age'], assertMaxAge) assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH') assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders) assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) assert.strictEqual(ctx.status, 204) }) t.test('should support custom allowed methods and headers', function() { const assertOrigin = 'http://my.site.here' const assertAllowedMethods = 'GET,HEAD' const assertAllowedHeaders = 'test1,test2' corsHandler = CorsHandler({ allowedOrigins: [assertOrigin], allowedMethods: assertAllowedMethods, allowedHeaders: assertAllowedHeaders, }) ctx.req.headers['origin'] = assertOrigin ctx.req.headers['access-control-request-method'] = 'GET' ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods) assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertAllowedHeaders) assert.strictEqual(ctx.status, 204) }) t.test('should not set any allowed headers if allowedHeaders is explicitly false', function() { const assertOrigin = 'http://my.site.here' const assertAllowedMethods = 'GET,HEAD' corsHandler = CorsHandler({ allowedOrigins: [assertOrigin], allowedMethods: assertAllowedMethods, allowedHeaders: false, }) ctx.req.headers['origin'] = assertOrigin ctx.req.headers['access-control-request-method'] = 'GET' ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods) assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], undefined) assert.strictEqual(ctx.status, 204) }) t.test('should not add any headers if origin missing', function() { corsHandler = CorsHandler({ allowedOrigins: ['https://test.com'], }) ctx.req.headers['origin'] = null ctx.req.headers['access-control-request-method'] = 'GET' ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) assert.strictEqual(ctx.status, 204) }) t.test('should not add any headers if origin not found', function() { const assertOrigin = 'http://my.site.here' const assertAllowedMethods = 'GET,HEAD' corsHandler = CorsHandler({ allowedOrigins: ['https://my.site.here'], allowedMethods: assertAllowedMethods, allowedHeaders: false, }) ctx.req.headers['origin'] = assertOrigin ctx.req.headers['access-control-request-method'] = 'GET' ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) assert.strictEqual(ctx.status, 204) }) t.test('should not add any headers if request-method is missing', function() { const assertOrigin = 'http://my.site.here' corsHandler = CorsHandler({ allowedOrigins: ['http://my.site.here'], }) ctx.req.headers['origin'] = assertOrigin delete ctx.req.headers['access-control-request-method'] ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) assert.strictEqual(ctx.status, 204) }) }) t.describe('GET/POST/DELETE/PATCH/PUT', function() { let testMethods = ['GET', 'POST', 'DELETE', 'PATCH', 'PUT'] t.test('should set header but no status', function() { testMethods.forEach(function(method) { ctx = createCtx() ctx.method = method ctx.status = method const assertOrigin = 'http://my.site.here' corsHandler = CorsHandler({ allowedOrigins: [assertOrigin], }) ctx.req.headers['origin'] = assertOrigin ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' assert.notOk(ctx.headers['Vary']) assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) assert.notOk(ctx.headers['Access-Control-Expose-Headers']) assert.strictEqual(ctx.status, method) }) }) t.test('should set credential header if specifed', function() { testMethods.forEach(function(method) { ctx = createCtx() ctx.method = method ctx.status = method const assertOrigin = 'http://my.site.here' corsHandler = CorsHandler({ allowedOrigins: [assertOrigin], credentials: true, }) ctx.req.headers['origin'] = assertOrigin assert.notOk(ctx.headers['Vary']) assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.strictEqual(ctx.headers['Access-Control-Allow-Credentials'], 'true') assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) assert.notOk(ctx.headers['Access-Control-Expose-Headers']) assert.strictEqual(ctx.status, method) }) }) t.test('should set expose headers if specifed', function() { testMethods.forEach(function(method) { const assertExposeHeaders = 'Some, Test, Here' ctx = createCtx() ctx.method = method ctx.status = method const assertOrigin = 'http://my.site.here' corsHandler = CorsHandler({ allowedOrigins: [assertOrigin], exposeHeaders: assertExposeHeaders, }) ctx.req.headers['origin'] = assertOrigin assert.notOk(ctx.headers['Vary']) assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) assert.notOk(ctx.headers['Access-Control-Allow-Credentials']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) assert.strictEqual(ctx.headers['Access-Control-Expose-Headers'], assertExposeHeaders) assert.strictEqual(ctx.status, method) }) }) t.test('should not add any headers if origin missing', function() { testMethods.forEach(function(method) { ctx = createCtx() ctx.method = method ctx.status = method corsHandler = CorsHandler({ allowedOrigins: ['https://test.com'], }) ctx.req.headers['origin'] = null assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) assert.notOk(ctx.headers['Access-Control-Expose-Headers']) assert.strictEqual(ctx.status, method) }) }) t.test('should not add any headers if origin not found', function() { testMethods.forEach(function(method) { ctx = createCtx() ctx.method = method ctx.status = method const assertOrigin = 'http://my.site.here' const assertAllowedMethods = 'GET,HEAD' corsHandler = CorsHandler({ allowedOrigins: ['https://my.site.here'], allowedMethods: assertAllowedMethods, allowedHeaders: false, }) ctx.req.headers['origin'] = assertOrigin assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) corsHandler(ctx) assert.strictEqual(ctx.headers['Vary'], 'Origin') assert.notOk(ctx.headers['Access-Control-Allow-Origin']) assert.notOk(ctx.headers['Access-Control-Allow-Methods']) assert.notOk(ctx.headers['Access-Control-Allow-Headers']) assert.notOk(ctx.headers['Access-Control-Expose-Headers']) assert.strictEqual(ctx.status, method) }) }) }) }) t.describe('#JsonHandler()', function() { let jsonHandler = JsonHandler() let ctx t.beforeEach(function() { ctx = createCtx() }) t.test('should return a handler', function() { assert.strictEqual(typeof(jsonHandler), 'function') }) t.test('should support fetching body from request', async function() { const assertBody = { a: 1, temp: 'test', hello: 'world'} let parsed = JSON.stringify(assertBody) let finished = false let err = null jsonHandler(ctx).catch(function(error) { err = error }).then(function() { finished = true }) assert.ok(ctx.req.on.called) assert.strictEqual(ctx.req.on.firstCall[0], 'data') assert.strictEqual(ctx.req.on.secondCall[0], 'end') assert.strictEqual(finished, false) ctx.req.on.firstCall[1](Buffer.from(parsed.slice(0, parsed.length / 2))) assert.strictEqual(finished, false) ctx.req.on.firstCall[1](Buffer.from(parsed.slice(parsed.length / 2))) assert.strictEqual(finished, false) ctx.req.on.secondCall[1]() await setTimeout(10) assert.strictEqual(finished, true) assert.strictEqual(err, null) assert.notStrictEqual(ctx.req.body, assertBody) assert.deepStrictEqual(ctx.req.body, assertBody) }) t.test('should throw if buffer grows too large', async function() { let defaultLimit = 10 * 1024 let segmentSize = 100 let finished = false let err = null jsonHandler(ctx).catch(function(error) { err = error }).then(function() { finished = true }) for (let i = 0; i < defaultLimit; i += segmentSize) { ctx.req.on.firstCall[1](Buffer.alloc(segmentSize, 'a')) } await setTimeout(10) assert.strictEqual(finished, true) assert.notStrictEqual(err, null) assert.ok(err instanceof HttpError) assert.strictEqual(err.status, 413) assert.match(err.message, new RegExp(100 * 103)) assert.match(err.message, new RegExp(defaultLimit)) }) t.test('should throw if buffer is not valid json', async function() { let finished = false let err = null jsonHandler(ctx).catch(function(error) { err = error }).then(function() { finished = true }) ctx.req.on.firstCall[1](Buffer.alloc(10, 'a')) ctx.req.on.firstCall[1](Buffer.alloc(10, 'a')) ctx.req.on.secondCall[1]() await setTimeout(10) assert.strictEqual(finished, true) assert.notStrictEqual(err, null) assert.ok(err instanceof HttpError) assert.strictEqual(err.status, 400) assert.match(err.message, /JSON/) assert.match(err.message, /Unexpected token a in/i) assert.strictEqual(err.body.status, 400) assert.match(err.body.message, /Invalid JSON/i) assert.match(err.body.message, /Unexpected token a in/i) assert.strictEqual(err.body.request, 'aaaaaaaaaaaaaaaaaaaa') }) t.test('should not throw if body is empty', async function() { let finished = false let err = null jsonHandler(ctx).catch(function(error) { err = error }).then(function() { finished = true }) assert.ok(ctx.req.on.called) assert.strictEqual(ctx.req.on.firstCall[0], 'data') assert.strictEqual(ctx.req.on.secondCall[0], 'end') assert.strictEqual(finished, false) ctx.req.on.secondCall[1]() await setTimeout(10) assert.strictEqual(finished, true) assert.strictEqual(err, null) assert.deepStrictEqual(ctx.req.body, {}) }) }) t.describe('#FormidableHandler()', function() { let formidable let incomingForm let ctx t.beforeEach(function() { ctx = createCtx() formidable = { IncomingForm: stub(), fsRename: stub().resolves(), } incomingForm = { parse: stub().returnWith(function(req, cb) { cb(null, {}, { file: { name: 'asdf.png' } }) }) } formidable.IncomingForm.returns(incomingForm) }) t.test('should call formidable with correct defaults', async function() { let handler = FormidableHandler(formidable) await handler(ctx) assert.strictEqual(incomingForm.uploadDir, os.tmpdir()) assert.strictEqual(incomingForm.maxFileSize, 8 * 1024 * 1024) assert.strictEqual(incomingForm.maxFieldsSize, 10 * 1024) assert.strictEqual(incomingForm.maxFields, 50) assert.strictEqual(incomingForm.parse.firstCall[0], ctx.req) }) t.test('should apply fields and rename file before returning', async function() { const assertFilename = 'Lets love.png' const assertOriginalPath = 'Hitoshi/Fujima/Yuigi.png' const assertFile = { a: 1, name: assertFilename, path: assertOriginalPath } const assertFields = { b: 2, c: 3 } let handler = FormidableHandler(formidable) incomingForm.parse.returnWith(function(req, cb) { cb(null, assertFields, { file: assertFile }) }) assert.notOk(ctx.req.body) assert.notOk(ctx.req.file) let prefix = new Date().toISOString().replace(/-/g, '').replace('T', '_').replace(/:/g, '').split('.')[0] await handler(ctx) assert.strictEqual(ctx.req.body, assertFields) assert.strictEqual(ctx.req.file, assertFile) assert.strictEqual(ctx.req.file.path, path.join(os.tmpdir(), ctx.req.file.filename)) assert.match(ctx.req.file.filename, new RegExp(prefix)) assert.match(ctx.req.file.filename, new RegExp(assertFilename)) assert.ok(formidable.fsRename.called) assert.strictEqual(formidable.fsRename.firstCall[0], assertOriginalPath) assert.strictEqual(formidable.fsRename.firstCall[1], ctx.req.file.path) }) t.test('should throw parse error if parse fails', async function() { const assertError = new Error('Aozora') let handler = FormidableHandler(formidable) incomingForm.parse.returnWith(function(req, cb) { cb(assertError) }) let err = await assert.isRejected(handler(ctx)) assert.notStrictEqual(err, assertError) assert.ok(err instanceof HttpError) assert.strictEqual(err.message, assertError.message) assert.strictEqual(err.status, 400) }) t.test('should throw rename error if rename fails', async function() { const assertError = new Error('Aozora') formidable.fsRename.rejects(assertError) let handler = FormidableHandler(formidable) let err = await assert.isRejected(handler(ctx)) assert.strictEqual(err, assertError) }) t.test('should not call rename if no file is present', async function() { const assertNotError = new Error('Aozora') const assertFields = { a: 1 } formidable.fsRename.rejects(assertNotError) let handler = FormidableHandler(formidable) incomingForm.parse.returnWith(function(req, cb) { cb(null, assertFields, null) }) await handler(ctx) assert.strictEqual(ctx.req.body, assertFields) assert.strictEqual(ctx.req.file, null) incomingForm.parse.returnWith(function(req, cb) { cb(null, assertFields, { file: null }) }) await handler(ctx) assert.strictEqual(ctx.req.body, assertFields) assert.strictEqual(ctx.req.file, null) }) t.test('should throw filename error if filename fails', async function() { const assertError = new Error('Dallaglio Piano') let handler = FormidableHandler(formidable, { filename: function() { throw assertError } }) let err = await assert.isRejected(handler(ctx)) assert.strictEqual(err, assertError) }) t.test('should default to original name of filename returns null', async function() { const assertFilename = 'Herrscher.png' let handler = FormidableHandler(formidable, { filename: function() { return null } }) incomingForm.parse.returnWith(function(req, cb) { cb(null, { }, { file: { name: assertFilename } }) }) await handler(ctx) assert.strictEqual(ctx.req.file.filename, assertFilename) }) t.test('should support multiple filename calls in same second', async function() { const assertFilename = 'Herrscher.png' let handler = FormidableHandler(formidable) await handler(ctx) let file1 = ctx.req.file await handler(ctx) let file2 = ctx.req.file assert.notStrictEqual(file1, file2) assert.notStrictEqual(file1.filename, file2.filename) }) t.test('should support parsing fields as json', async function() { const assertFields = { b: '2', c: '3', e: 'asdf', f: '{"a": 1}' } let handler = FormidableHandler(formidable, { parseFields: true, }) incomingForm.parse.returnWith(function(req, cb) { cb(null, assertFields, { file: { name: 'test.png' } }) }) await handler(ctx) assert.strictEqual(ctx.req.body, assertFields) assert.strictEqual(ctx.req.body.b, 2) assert.strictEqual(ctx.req.body.c, 3) assert.strictEqual(ctx.req.body.e, 'asdf') assert.deepStrictEqual(ctx.req.body.f, {a: 1}) }) })