From 8f8c219251b7180a55038fec86f9cd3f0b341239 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Tue, 27 Mar 2018 19:25:51 -0400 Subject: [PATCH 01/35] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e8bbdf9..41b0c6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-api", - "version": "0.3.1", + "version": "0.4.0", "description": "Lightweight web framework for your serverless applications", "main": "index.js", "scripts": { From 4e0063ec72044fb1805033287ee0d0a2938afee3 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Tue, 27 Mar 2018 19:29:27 -0400 Subject: [PATCH 02/35] update lambda api image inclusion method for npm fix --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 9fcaf67..da17958 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ - - +[![Lambda API](https://www.jeremydaly.com/wp-content/uploads/2018/03/lambda-api-logo.svg)](https://serverless-api.com/) [![Build Status](https://travis-ci.org/jeremydaly/lambda-api.svg?branch=master)](https://travis-ci.org/jeremydaly/lambda-api) [![npm](https://img.shields.io/npm/v/lambda-api.svg)](https://www.npmjs.com/package/lambda-api) From bc2a00b3b64711a0705797f1a99723e8b03d7f9f Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Tue, 27 Mar 2018 21:52:29 -0400 Subject: [PATCH 03/35] add register function to close #11, rework base property --- index.js | 49 ++++++++++++++++++++++++++++++++----------------- request.js | 8 ++++---- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 0ecf62f..81f22a6 100644 --- a/index.js +++ b/index.js @@ -19,9 +19,12 @@ class API { // Set the version and base paths this._version = props && props.version ? props.version : 'v1' - this._base = props && props.base ? props.base.trim() : '' + this._base = props && props.base && typeof props.base === 'string' ? props.base.trim() : '' this._callbackName = props && props.callback ? props.callback.trim() : 'callback' + // Prefix stack w/ base + this._prefix = this.parseRoute(this._base) + // Stores timers for debugging this._timers = {} this._procTimes = {} @@ -56,6 +59,7 @@ class API { // Testing flag this._test = false + } // end constructor // GET: convenience method @@ -86,8 +90,11 @@ class API { // METHOD: Adds method and handler to routes METHOD(method, path, handler) { + // Parse the path + let parsedPath = this.parseRoute(path) + // Split the route and clean it up - let route = path.trim().replace(/^\/(.*?)(\/)*$/,'$1').split('/') + let route = this._prefix.concat(parsedPath) // Keep track of path variables let pathVars = {} @@ -106,7 +113,7 @@ class API { // Add the route to the global _routes this.setRoute( this._routes, - (i === route.length-1 ? { ['__'+method.toUpperCase()]: { vars: pathVars, handler: handler, route: path } } : {}), + (i === route.length-1 ? { ['__'+method.toUpperCase()]: { vars: pathVars, handler: handler, route: '/'+parsedPath.join('/') } } : {}), route.slice(0,i+1) ); @@ -298,20 +305,11 @@ class API { // UTILITY FUNCTIONS //-------------------------------------------------------------------------// - deepFind(obj, path) { - let paths = path//.split('.'), - let current = obj - - for (let i = 0; i < paths.length; ++i) { - if (current[paths[i]] == undefined) { - return undefined - } else { - current = current[paths[i]] - } - } - return current + parseRoute(path) { + return path.trim().replace(/^\/(.*?)(\/)*$/,'$1').split('/').filter(x => x.trim() !== '') } + setRoute(obj, value, path) { if (typeof path === "string") { let path = path.split('.') @@ -354,9 +352,26 @@ class API { return this._app } + + // Register routes with options + register(fn,options) { + + // Extract Prefix + let prefix = options.prefix && options.prefix.toString().trim() !== '' ? + this.parseRoute(options.prefix) : [] + + // Concat to existing prefix + this._prefix = this._prefix.concat(prefix) + + // Execute the routing function + fn(this,options) + + // Remove the last prefix + this._prefix = this._prefix.slice(0,-(prefix.length)) + + } // end register + } // end API class // Export the API class module.exports = opts => new API(opts) - -// console.error('DEPRECATED: constructor method. Use require(\'lambda-api\')({ version: \'v1.0\', base: \'v1\' }) to initialize the framework instead') diff --git a/request.js b/request.js index 339c0f0..23aa919 100644 --- a/request.js +++ b/request.js @@ -67,10 +67,10 @@ class REQUEST { // Extract path from event (strip querystring just in case) let path = app._event.path.trim().split('?')[0].replace(/^\/(.*?)(\/)*$/,'$1').split('/') - // Remove base if it exists - if (app._base && app._base === path[0]) { - path.shift() - } // end remove base + // // Remove base if it exists + // if (app._base && app._base === path[0]) { + // path.shift() + // } // end remove base // Init the route this.route = null From bfa26be103b2087dc7d5fa8db23778cb35ac421d Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Tue, 27 Mar 2018 21:53:23 -0400 Subject: [PATCH 04/35] update tests with new base config and add register tests --- test/_testRoutes-v1.js | 25 ++++++ test/_testRoutes-v2.js | 21 +++++ test/basePath.js | 75 ++++++++++++++++ test/cookies.js | 2 +- test/errorHandling.js | 2 +- test/finally.js | 2 +- test/headers.js | 2 +- test/middleware.js | 2 +- test/modules.js | 2 +- test/namespaces.js | 2 +- test/register.js | 199 +++++++++++++++++++++++++++++++++++++++++ test/responses.js | 4 +- test/routes.js | 2 +- 13 files changed, 330 insertions(+), 10 deletions(-) create mode 100644 test/_testRoutes-v1.js create mode 100644 test/_testRoutes-v2.js create mode 100644 test/basePath.js create mode 100644 test/register.js diff --git a/test/_testRoutes-v1.js b/test/_testRoutes-v1.js new file mode 100644 index 0000000..8416354 --- /dev/null +++ b/test/_testRoutes-v1.js @@ -0,0 +1,25 @@ +'use strict' + +module.exports = function(app, opts) { + + app.get('/test-register', function(req,res) { + res.json({ path: req.path, route: req.route, method: req.method }) + }) + + app.get('/test-register/sub1', function(req,res) { + res.json({ path: req.path, route: req.route, method: req.method }) + }) + + app.get('test-register/:param1/test/', function(req,res) { + res.json({ path: req.path, route: req.route, method: req.method, params: req.params }) + }) + + if (opts.prefix === '/vX/vY') { + app.register(require('./_testRoutes-v1'), { prefix: '/vZ' }) + } + + app.get('/test-register/sub2/', function(req,res) { + res.json({ path: req.path, route: req.route, method: req.method }) + }) + +} // end diff --git a/test/_testRoutes-v2.js b/test/_testRoutes-v2.js new file mode 100644 index 0000000..726c2ca --- /dev/null +++ b/test/_testRoutes-v2.js @@ -0,0 +1,21 @@ +'use strict' + +module.exports = function(app, opts) { + + app.get('/test-register', function(req,res) { + res.json({ path: req.path, route: req.route, method: req.method }) + }) + + app.get('/test-register/sub1', function(req,res) { + res.json({ path: req.path, route: req.route, method: req.method }) + }) + + app.get('/test-register/sub2', function(req,res) { + res.json({ path: req.path, route: req.route, method: req.method }) + }) + + app.get('/test-register/:param1/test/', function(req,res) { + res.json({ path: req.path, route: req.route, method: req.method, params: req.params }) + }) + +} // end diff --git a/test/basePath.js b/test/basePath.js new file mode 100644 index 0000000..71f38f1 --- /dev/null +++ b/test/basePath.js @@ -0,0 +1,75 @@ +'use strict'; + +const Promise = require('bluebird') // Promise library +const expect = require('chai').expect // Assertion library + +// Init API instance +const api = require('../index')({ version: 'v1.0', base: '/v1' }) + +// NOTE: Set test to true +api._test = true; + +let event = { + httpMethod: 'get', + path: '/test', + body: {}, + headers: { + 'Content-Type': 'application/json' + } +} + +/******************************************************************************/ +/*** DEFINE TEST ROUTES ***/ +/******************************************************************************/ +api.get('/test', function(req,res) { + res.status(200).json({ method: 'get', status: 'ok' }) +}) + +api.get('/test/:test', function(req,res) { + // console.log(req) + res.status(200).json({ method: 'get', status: 'ok', param: req.params.test }) +}) + +api.get('/test/test2/test3', function(req,res) { + res.status(200).json({ path: req.path, method: 'get', status: 'ok' }) +}) + + +/******************************************************************************/ +/*** BEGIN TESTS ***/ +/******************************************************************************/ + +describe('Base Path Tests:', function() { + + it('Simple path with base: /v1/test', function() { + let _event = Object.assign({},event,{ path: '/v1/test' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok"}' }) + }) + }) // end it + + it('Path with base and parameter: /v1/test/123', function() { + let _event = Object.assign({},event,{ path: '/v1/test/123' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123"}' }) + }) + }) // end it + + it('Nested path with base: /v1/test/test2/test3', function() { + let _event = Object.assign({},event,{ path: '/v1/test/test2/test3' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + // console.log(JSON.stringify(JSON.parse(result.body),null,2)); + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test/test2/test3","method":"get","status":"ok"}' }) + }) + }) // end it + +}) // end BASEPATH tests diff --git a/test/cookies.js b/test/cookies.js index 8250309..a80018a 100644 --- a/test/cookies.js +++ b/test/cookies.js @@ -4,7 +4,7 @@ const Promise = require('bluebird') // Promise library const expect = require('chai').expect // Assertion library // Init API instance -const api = require('../index')({ version: 'v1.0', base: 'v1' }) +const api = require('../index')({ version: 'v1.0' }) // NOTE: Set test to true api._test = true; diff --git a/test/errorHandling.js b/test/errorHandling.js index 526470c..91aa0a7 100644 --- a/test/errorHandling.js +++ b/test/errorHandling.js @@ -4,7 +4,7 @@ const Promise = require('bluebird') // Promise library const expect = require('chai').expect // Assertion library // Init API instance -const api = require('../index')({ version: 'v1.0', base: 'v1' }) +const api = require('../index')({ version: 'v1.0' }) // NOTE: Set test to true api._test = true; diff --git a/test/finally.js b/test/finally.js index 1e7cf69..fd01cf2 100644 --- a/test/finally.js +++ b/test/finally.js @@ -4,7 +4,7 @@ const Promise = require('bluebird') // Promise library const expect = require('chai').expect // Assertion library // Init API instance -const api = require('../index')({ version: 'v1.0', base: 'v1' }) +const api = require('../index')({ version: 'v1.0' }) // Simulate database module const fakeDatabase = { connected: true } diff --git a/test/headers.js b/test/headers.js index 60e9443..1c40617 100644 --- a/test/headers.js +++ b/test/headers.js @@ -4,7 +4,7 @@ const Promise = require('bluebird') // Promise library const expect = require('chai').expect // Assertion library // Init API instance -const api = require('../index')({ version: 'v1.0', base: 'v1' }) +const api = require('../index')({ version: 'v1.0' }) // NOTE: Set test to true api._test = true; diff --git a/test/middleware.js b/test/middleware.js index e5ae829..0a0726b 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -4,7 +4,7 @@ const Promise = require('bluebird') // Promise library const expect = require('chai').expect // Assertion library // Init API instance -const api = require('../index')({ version: 'v1.0', base: 'v1' }) +const api = require('../index')({ version: 'v1.0' }) // NOTE: Set test to true api._test = true; diff --git a/test/modules.js b/test/modules.js index 692a2b2..155975a 100644 --- a/test/modules.js +++ b/test/modules.js @@ -4,7 +4,7 @@ const Promise = require('bluebird') // Promise library const expect = require('chai').expect // Assertion library // Init API instance -const api = require('../index')({ version: 'v1.0', base: 'v1' }) +const api = require('../index')({ version: 'v1.0' }) const appTest = require('./_testApp') diff --git a/test/namespaces.js b/test/namespaces.js index d734126..4d1edc3 100644 --- a/test/namespaces.js +++ b/test/namespaces.js @@ -4,7 +4,7 @@ const Promise = require('bluebird') // Promise library const expect = require('chai').expect // Assertion library // Init API instance -const api = require('../index')({ version: 'v1.0', base: 'v1' }) +const api = require('../index')({ version: 'v1.0' }) // Add the data namespace api.app('data',require('./_testData')) diff --git a/test/register.js b/test/register.js new file mode 100644 index 0000000..8147c80 --- /dev/null +++ b/test/register.js @@ -0,0 +1,199 @@ +'use strict'; + +const Promise = require('bluebird') // Promise library +const expect = require('chai').expect // Assertion library + +// Init API instance +const api = require('../index')({ version: 'v1.0' }) + +// NOTE: Set test to true +api._test = true; + +let event = { + httpMethod: 'get', + path: '/test', + body: {}, + headers: { + 'Content-Type': 'application/json' + } +} + +/******************************************************************************/ +/*** REGISTER ROUTES ***/ +/******************************************************************************/ + +api.register(require('./_testRoutes-v1'), { prefix: '/v1' }) +api.register(require('./_testRoutes-v1'), { prefix: '/vX/vY' }) +api.register(require('./_testRoutes-v1'), { prefix: '' }) +api.register(require('./_testRoutes-v2'), { prefix: '/v2' }) + +/******************************************************************************/ +/*** BEGIN TESTS ***/ +/******************************************************************************/ + +describe('Register Tests:', function() { + + it('No prefix', function() { + let _event = Object.assign({},event,{ path: '/test-register' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/test-register","route":"/test-register","method":"GET"}' }) + }) + }) // end it + + it('No prefix (nested)', function() { + let _event = Object.assign({},event,{ path: '/test-register/sub1' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/test-register/sub1","route":"/test-register/sub1","method":"GET"}' }) + }) + }) // end it + + it('No prefix (nested w/ param)', function() { + let _event = Object.assign({},event,{ path: '/test-register/TEST/test' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}' }) + }) + }) // end it + + + it('With prefix', function() { + let _event = Object.assign({},event,{ path: '/v1/test-register' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test-register","route":"/test-register","method":"GET"}' }) + }) + }) // end it + + it('With prefix (nested)', function() { + let _event = Object.assign({},event,{ path: '/v1/test-register/sub1' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test-register/sub1","route":"/test-register/sub1","method":"GET"}' }) + }) + }) // end it + + it('With prefix (nested w/ param)', function() { + let _event = Object.assign({},event,{ path: '/v1/test-register/TEST/test' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}' }) + }) + }) // end it + + + it('With double prefix', function() { + let _event = Object.assign({},event,{ path: '/vX/vY/test-register' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register","route":"/test-register","method":"GET"}' }) + }) + }) // end it + + it('With double prefix (nested)', function() { + let _event = Object.assign({},event,{ path: '/vX/vY/test-register/sub1' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register/sub1","route":"/test-register/sub1","method":"GET"}' }) + }) + }) // end it + + it('With double prefix (nested w/ param)', function() { + let _event = Object.assign({},event,{ path: '/vX/vY/test-register/TEST/test' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}' }) + }) + }) // end it + + + it('With recursive prefix', function() { + let _event = Object.assign({},event,{ path: '/vX/vY/vZ/test-register' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register","route":"/test-register","method":"GET"}' }) + }) + }) // end it + + it('With recursive prefix (nested)', function() { + let _event = Object.assign({},event,{ path: '/vX/vY/vZ/test-register/sub1' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register/sub1","route":"/test-register/sub1","method":"GET"}' }) + }) + }) // end it + + it('With recursive prefix (nested w/ param)', function() { + let _event = Object.assign({},event,{ path: '/vX/vY/vZ/test-register/TEST/test' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}' }) + }) + }) // end it + + it('After recursive interation', function() { + let _event = Object.assign({},event,{ path: '/vX/vY/test-register/sub2' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register/sub2","route":"/test-register/sub2","method":"GET"}' }) + }) + }) // end it + + it('New prefix', function() { + let _event = Object.assign({},event,{ path: '/v2/test-register' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v2/test-register","route":"/test-register","method":"GET"}' }) + }) + }) // end it + + it('New prefix (nested)', function() { + let _event = Object.assign({},event,{ path: '/v2/test-register/sub1' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v2/test-register/sub1","route":"/test-register/sub1","method":"GET"}' }) + }) + }) // end it + + it('New prefix (nested w/ param)', function() { + let _event = Object.assign({},event,{ path: '/v2/test-register/TEST/test' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v2/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}' }) + }) + }) // end it + +}) // end ROUTE tests diff --git a/test/responses.js b/test/responses.js index 5d579e8..c19ad67 100644 --- a/test/responses.js +++ b/test/responses.js @@ -4,9 +4,9 @@ const Promise = require('bluebird') // Promise library const expect = require('chai').expect // Assertion library // Init API instance -const api = require('../index')({ version: 'v1.0', base: 'v1' }) +const api = require('../index')({ version: 'v1.0' }) // Init secondary API for JSONP callback testing -const api2 = require('../index')({ version: 'v1.0', base: 'v1', callback: 'cb' }) +const api2 = require('../index')({ version: 'v1.0', callback: 'cb' }) // NOTE: Set test to true api._test = true; diff --git a/test/routes.js b/test/routes.js index f7f5dcd..cf27007 100644 --- a/test/routes.js +++ b/test/routes.js @@ -4,7 +4,7 @@ const Promise = require('bluebird') // Promise library const expect = require('chai').expect // Assertion library // Init API instance -const api = require('../index')({ version: 'v1.0', base: 'v1' }) +const api = require('../index')({ version: 'v1.0' }) // NOTE: Set test to true api._test = true; From 75284c5b846aa4ff6a475e4b42a5bfbfe782fac1 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Wed, 28 Mar 2018 16:50:53 -0400 Subject: [PATCH 05/35] add base64 body parsing if isBase64Encoded header is available --- request.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/request.js b/request.js index 23aa919..768fe0e 100644 --- a/request.js +++ b/request.js @@ -55,13 +55,19 @@ class REQUEST { // Set the requestContext this.requestContext = app._event.requestContext + // Capture the raw body + this.rawBody = app._event.body + + // Set the body (decode it if base64 encoded) + this.body = app._event.isBase64Encoded ? Buffer.from(app._event.body, 'base64').toString() : app._event.body + // Set the body if (this.headers['content-type'] && this.headers['content-type'].includes("application/x-www-form-urlencoded")) { - this.body = QS.parse(app._event.body) - } else if (typeof app._event.body === 'object') { - this.body = app._event.body + this.body = QS.parse(this.body) + } else if (typeof this.body === 'object') { + this.body = this.body } else { - this.body = parseBody(app._event.body) + this.body = parseBody(this.body) } // Extract path from event (strip querystring just in case) From 49476ab69d39b93f496bf3ea21fe1eaa55e2a36a Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Wed, 28 Mar 2018 17:16:22 -0400 Subject: [PATCH 06/35] add support for root paths --- index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.js b/index.js index 81f22a6..8986fed 100644 --- a/index.js +++ b/index.js @@ -96,6 +96,9 @@ class API { // Split the route and clean it up let route = this._prefix.concat(parsedPath) + // For root path support + if (route.length === 0) { route.push('')} + // Keep track of path variables let pathVars = {} From 074ee79a538bd8db94cb9293b24dbfc6b7baf8ef Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Wed, 28 Mar 2018 17:16:58 -0400 Subject: [PATCH 07/35] add notes for root paths and binary support --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index da17958..16ccc39 100644 --- a/README.md +++ b/README.md @@ -454,3 +454,9 @@ Simply create one `{proxy+}` route that uses the `ANY` method and all requests w ## Contributions Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/lambda-api/issues) for suggestions and bugs reports. + + +## NOTES +In order for "root" path mapping to work, you need to also create an `ANY` route for `/` in addition to your `{proxy+}` route. + +To enable binary support, you need to add `*/*` under Binary Media Types in API Gateway > APIs > [your api] -> Settings. From 0a6de4766ab7714201bc931fcbe0be3468614a31 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Wed, 28 Mar 2018 17:22:09 -0400 Subject: [PATCH 08/35] additional notes about binary support --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16ccc39..15a7cd6 100644 --- a/README.md +++ b/README.md @@ -459,4 +459,4 @@ Contributions, ideas and bug reports are welcome and greatly appreciated. Please ## NOTES In order for "root" path mapping to work, you need to also create an `ANY` route for `/` in addition to your `{proxy+}` route. -To enable binary support, you need to add `*/*` under Binary Media Types in API Gateway > APIs > [your api] -> Settings. +To enable binary support, you need to add `*/*` under Binary Media Types in API Gateway > APIs > [your api] -> Settings. This will also base64 encode all body content, but Lambda API will automatically decode it for you. From 6b0d2d8f43771166fa74ad66a359576260933ae6 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 11:34:03 -0400 Subject: [PATCH 09/35] add mimeLookup and mimemap --- mimemap.js | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ utils.js | 21 ++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 mimemap.js diff --git a/mimemap.js b/mimemap.js new file mode 100644 index 0000000..6de8570 --- /dev/null +++ b/mimemap.js @@ -0,0 +1,73 @@ +'use strict' + +/** + * Lightweight web framework for your serverless applications + * @author Jeremy Daly + * @license MIT + */ + +// Minimal mime map for common file types + +module.exports = { + // images + gif: 'image/gif', + ico: 'image/x-icon', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + svg: 'image/svg+xml', + svgz: 'image/svg+xml', + + // text + atom: 'application/atom+xml', + css: 'text/css', + csv: 'text/csv', + html: 'text/html', + htm: 'text/html', + js: 'application/javascript', + json: 'application/json', + map: 'application/json', + rdf: 'application/rdf+xml', + rss: 'application/rss+xml', + webmanifest: 'application/manifest+json', + xml: 'application/xml', + xls: 'application/xml', + + // other binary + gz: 'application/gzip', + pdf: 'application/pdf', + zip: 'application/zip', + + // fonts + woff: 'application/font-woff', + + // MS file Types + doc: 'application/msword', + dot: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + dotx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + docm: 'application/vnd.ms-word.document.macroEnabled.12', + dotm: 'application/vnd.ms-word.template.macroEnabled.12', + xls: 'application/vnd.ms-excel', + xlt: 'application/vnd.ms-excel', + xla: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + xltx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + xlsm: 'application/vnd.ms-excel.sheet.macroEnabled.12', + xltm: 'application/vnd.ms-excel.template.macroEnabled.12', + xlam: 'application/vnd.ms-excel.addin.macroEnabled.12', + xlsb: 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + ppt: 'application/vnd.ms-powerpoint', + pot: 'application/vnd.ms-powerpoint', + pps: 'application/vnd.ms-powerpoint', + ppa: 'application/vnd.ms-powerpoint', + pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + potx: 'application/vnd.openxmlformats-officedocument.presentationml.template', + ppsx: 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + ppam: 'application/vnd.ms-powerpoint.addin.macroEnabled.12', + pptm: 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + potm: 'application/vnd.ms-powerpoint.template.macroEnabled.12', + ppsm: 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', + mdb: 'application/vnd.ms-access' + +} diff --git a/utils.js b/utils.js index 0eba78d..b79625d 100644 --- a/utils.js +++ b/utils.js @@ -26,9 +26,13 @@ module.exports.encodeUrl = url => String(url) .replace(UNMATCHED_SURROGATE_PAIR_REGEXP, UNMATCHED_SURROGATE_PAIR_REPLACE) .replace(ENCODE_CHARS_REGEXP, encodeURI) + + module.exports.encodeBody = body => typeof body === 'object' ? JSON.stringify(body) : (body && typeof body !== 'string' ? body.toString() : (body ? body : '')) + + module.exports.parseBody = body => { try { return JSON.parse(body) @@ -36,3 +40,20 @@ module.exports.parseBody = body => { return body; } } + + + +const mimeMap = require('./mimemap.js') // MIME Map + +module.exports.mimeLookup = (input,custom={}) => { + let type = input.trim().replace(/^\./,'') + + // If it contains a slash, return unmodified + if (/.*\/.*/.test(type)) { + return input.trim() + } else { + // Lookup mime type + let mime = Object.assign(mimeMap,custom)[type] + return mime ? mime : false + } +} From 7926f216ac1e5a90cf0852887b7a90acbda53a6f Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 11:35:01 -0400 Subject: [PATCH 10/35] add mimeLookup tests --- test/utils.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/utils.js b/test/utils.js index c1d62f9..c24c053 100644 --- a/test/utils.js +++ b/test/utils.js @@ -84,4 +84,50 @@ describe('Utility Function Tests:', function() { }) // end encodeBody tests + describe('mimeLookup:', function() { + + it('.pdf', function() { + expect(utils.mimeLookup('.pdf')).to.equal('application/pdf') + }) // end it + + it('application/pdf', function() { + expect(utils.mimeLookup('application/pdf')).to.equal('application/pdf') + }) // end it + + it('application-x/pdf (non-standard w/ slash)', function() { + expect(utils.mimeLookup('application-x/pdf')).to.equal('application-x/pdf') + }) // end it + + it('xml', function() { + expect(utils.mimeLookup('xml')).to.equal('application/xml') + }) // end it + + it('.html', function() { + expect(utils.mimeLookup('.html')).to.equal('text/html') + }) // end it + + it('css', function() { + expect(utils.mimeLookup('css')).to.equal('text/css') + }) // end it + + it('jpg', function() { + expect(utils.mimeLookup('jpg')).to.equal('image/jpeg') + }) // end it + + it('.svg', function() { + expect(utils.mimeLookup('.svg')).to.equal('image/svg+xml') + }) // end it + + it('docx', function() { + expect(utils.mimeLookup('docx')).to.equal('application/vnd.openxmlformats-officedocument.wordprocessingml.document') + }) // end it + + it('Custom', function() { + expect(utils.mimeLookup('.mpeg', { mpeg: 'video/mpeg' })).to.equal('video/mpeg') + }) // end it + + + }) // end encodeBody tests + + }) // end UTILITY tests From 96a64d7ce4b39337f54283d7654d9dcc29350b34 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 11:36:23 -0400 Subject: [PATCH 11/35] add basic attachment() method tests --- test/attachments.js | 102 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 test/attachments.js diff --git a/test/attachments.js b/test/attachments.js new file mode 100644 index 0000000..06bce0c --- /dev/null +++ b/test/attachments.js @@ -0,0 +1,102 @@ +'use strict'; + +const Promise = require('bluebird') // Promise library +const expect = require('chai').expect // Assertion library + +// Init API instance +const api = require('../index')({ version: 'v1.0', mimeTypes: { test: 'text/test' } }) + +// NOTE: Set test to true +api._test = true; + +let event = { + httpMethod: 'get', + path: '/', + body: {}, + headers: { + 'Content-Type': 'application/json' + } +} + +/******************************************************************************/ +/*** DEFINE TEST ROUTES ***/ +/******************************************************************************/ + +api.get('/attachment', function(req,res) { + res.attachment().send({ status: 'ok' }) +}) + +api.get('/attachment/pdf', function(req,res) { + res.attachment('/test/foo.pdf').send('filedata') +}) + +api.get('/attachment/png', function(req,res) { + res.attachment('/test/foo.png').send('filedata') +}) + +api.get('/attachment/csv', function(req,res) { + res.attachment('/test/path/foo.csv').send('filedata') +}) + +api.get('/attachment/custom', function(req,res) { + res.attachment('/test/path/foo.test').send('filedata') +}) + + +/******************************************************************************/ +/*** BEGIN TESTS ***/ +/******************************************************************************/ + +describe('Attachment Tests:', function() { + + it('Simple attachment', function() { + let _event = Object.assign({},event,{ path: '/attachment' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Disposition': 'attachment', 'Content-Type': 'application/json' }, statusCode: 200, body: '{"status":"ok"}', isBase64Encoded: false }) + }) + }) // end it + + it('PDF attachment w/ path', function() { + let _event = Object.assign({},event,{ path: '/attachment/pdf' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Disposition': 'attachment; filename=\"foo.pdf\"', 'Content-Type': 'application/pdf' }, statusCode: 200, body: 'filedata', isBase64Encoded: false }) + }) + }) // end it + + it('PNG attachment w/ path', function() { + let _event = Object.assign({},event,{ path: '/attachment/png' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Disposition': 'attachment; filename=\"foo.png\"', 'Content-Type': 'image/png' }, statusCode: 200, body: 'filedata', isBase64Encoded: false }) + }) + }) // end it + + it('CSV attachment w/ path', function() { + let _event = Object.assign({},event,{ path: '/attachment/csv' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Disposition': 'attachment; filename=\"foo.csv\"', 'Content-Type': 'text/csv' }, statusCode: 200, body: 'filedata', isBase64Encoded: false }) + }) + }) // end it + + it('Custom MIME type attachment w/ path', function() { + let _event = Object.assign({},event,{ path: '/attachment/custom' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Disposition': 'attachment; filename=\"foo.test\"', 'Content-Type': 'text/test' }, statusCode: 200, body: 'filedata', isBase64Encoded: false }) + }) + }) // end it + +}) // end HEADER tests From f62eb1291da2d8de2c7bdaabfa9ebe0622d6415e Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 11:36:57 -0400 Subject: [PATCH 12/35] update tests to support default isBase64Encoded flag --- test/basePath.js | 6 +-- test/cookies.js | 36 +++++++-------- test/errorHandling.js | 10 ++--- test/finally.js | 4 +- test/headers.js | 6 +-- test/middleware.js | 6 +-- test/modules.js | 8 ++-- test/namespaces.js | 4 +- test/register.js | 32 ++++++------- test/responses.js | 28 ++++++------ test/routes.js | 101 ++++++++++++++++++++++++------------------ 11 files changed, 128 insertions(+), 113 deletions(-) diff --git a/test/basePath.js b/test/basePath.js index 71f38f1..095343c 100644 --- a/test/basePath.js +++ b/test/basePath.js @@ -47,7 +47,7 @@ describe('Base Path Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false }) }) }) // end it @@ -57,7 +57,7 @@ describe('Base Path Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123"}', isBase64Encoded: false }) }) }) // end it @@ -68,7 +68,7 @@ describe('Base Path Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(JSON.stringify(JSON.parse(result.body),null,2)); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test/test2/test3","method":"get","status":"ok"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test/test2/test3","method":"get","status":"ok"}', isBase64Encoded: false }) }) }) // end it diff --git a/test/cookies.js b/test/cookies.js index a80018a..df66ed1 100644 --- a/test/cookies.js +++ b/test/cookies.js @@ -131,7 +131,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=value; Path=/' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -146,7 +146,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=http%3A%2F%2F%20%5B%5D%20foo%3Bbar; Path=/' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -162,7 +162,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=%7B%22foo%22%3A%22bar%22%7D; Path=/' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -178,7 +178,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': '123=value; Path=/' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -194,7 +194,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=value; Expires=Tue, 01 Jan 2019 00:00:00 GMT; Path=/' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -209,7 +209,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=value; MaxAge=3600; Expires='+ new Date(Date.now()+3600000).toUTCString() + '; Path=/' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -224,7 +224,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=value; Domain=test.com; Expires=Tue, 01 Jan 2019 00:00:00 GMT; Path=/' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -239,7 +239,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=value; Domain=test.com; Expires=Tue, 01 Jan 2019 00:00:00 GMT; HttpOnly; Path=/' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -254,7 +254,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=value; Domain=test.com; Expires=Tue, 01 Jan 2019 00:00:00 GMT; Path=/; Secure' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -269,7 +269,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=value; Domain=test.com; Expires=Tue, 01 Jan 2019 00:00:00 GMT; Path=/test; Secure' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -284,7 +284,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=value; Domain=test.com; Expires=Tue, 01 Jan 2019 00:00:00 GMT; Path=/; SameSite=Strict' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -299,7 +299,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=value; Domain=test.com; Expires=Tue, 01 Jan 2019 00:00:00 GMT; Path=/; SameSite=Lax' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -314,7 +314,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=value; Domain=test.com; Expires=Tue, 01 Jan 2019 00:00:00 GMT; Path=/; SameSite=Test' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -338,7 +338,7 @@ describe('Cookie Tests:', function() { expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json', - }, statusCode: 200, body: '{"cookies":{"test":"some value"}}' + }, statusCode: 200, body: '{"cookies":{"test":"some value"}}', isBase64Encoded: false }) }) }) // end it @@ -357,7 +357,7 @@ describe('Cookie Tests:', function() { expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json', - }, statusCode: 200, body: '{\"cookies\":{\"test\":\"some value\",\"test2\":{\"foo\":\"bar\"}}}' + }, statusCode: 200, body: '{\"cookies\":{\"test\":\"some value\",\"test2\":{\"foo\":\"bar\"}}}', isBase64Encoded: false }) }) }) // end it @@ -377,7 +377,7 @@ describe('Cookie Tests:', function() { expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json', - }, statusCode: 200, body: '{\"cookies\":{\"test\":\"some value\",\"test2\":{\"foo\":\"bar\"},\"test3\":\"domain\"}}' + }, statusCode: 200, body: '{\"cookies\":{\"test\":\"some value\",\"test2\":{\"foo\":\"bar\"},\"test3\":\"domain\"}}', isBase64Encoded: false }) }) }) // end it @@ -398,7 +398,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; MaxAge=-1; Path=/' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it @@ -415,7 +415,7 @@ describe('Cookie Tests:', function() { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'test=; Domain=test.com; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; MaxAge=-1; Path=/; Secure' - }, statusCode: 200, body: '{}' + }, statusCode: 200, body: '{}', isBase64Encoded: false }) }) }) // end it diff --git a/test/errorHandling.js b/test/errorHandling.js index 91aa0a7..ee01303 100644 --- a/test/errorHandling.js +++ b/test/errorHandling.js @@ -100,7 +100,7 @@ describe('Error Handling Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a test error message"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a test error message"}', isBase64Encoded: false }) }) }) // end it @@ -110,7 +110,7 @@ describe('Error Handling Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a test thrown error"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a test thrown error"}', isBase64Encoded: false }) }) }) // end it @@ -120,7 +120,7 @@ describe('Error Handling Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 405, body: '{"error":"This is a simulated error"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 405, body: '{"error":"This is a simulated error"}', isBase64Encoded: false }) }) }) // end it @@ -130,7 +130,7 @@ describe('Error Handling Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/plain' }, statusCode: 500, body: 'This is a test error message: 123/456' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/plain' }, statusCode: 500, body: 'This is a test error message: 123/456', isBase64Encoded: false }) }) }) // end it @@ -140,7 +140,7 @@ describe('Error Handling Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/plain' }, statusCode: 500, body: 'This is a test error message: 123/456' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/plain' }, statusCode: 500, body: 'This is a test error message: 123/456', isBase64Encoded: false }) }) }) // end it diff --git a/test/finally.js b/test/finally.js index fd01cf2..e9902cb 100644 --- a/test/finally.js +++ b/test/finally.js @@ -51,7 +51,7 @@ describe('Finally Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","connected":true}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","connected":true}', isBase64Encoded: false }) expect(fakeDatabase).to.deep.equal({ connected: true }) }) }) // end it @@ -62,7 +62,7 @@ describe('Finally Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","connected":false}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","connected":false}', isBase64Encoded: false }) }) }) // end it diff --git a/test/headers.js b/test/headers.js index 1c40617..28a2bb7 100644 --- a/test/headers.js +++ b/test/headers.js @@ -52,7 +52,7 @@ describe('Header Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json', 'test': 'testVal' }, statusCode: 200, body: '{"method":"get","status":"ok"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json', 'test': 'testVal' }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false }) }) }) // end it @@ -62,7 +62,7 @@ describe('Header Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html' }, statusCode: 200, body: '
testHTML
' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html' }, statusCode: 200, body: '
testHTML
', isBase64Encoded: false }) }) }) // end it @@ -72,7 +72,7 @@ describe('Header Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html' }, statusCode: 200, body: '
testHTML
' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html' }, statusCode: 200, body: '
testHTML
', isBase64Encoded: false }) }) }) // end it diff --git a/test/middleware.js b/test/middleware.js index 0a0726b..3c8d8fd 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -84,7 +84,7 @@ describe('Middleware Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddleware":"123","testMiddleware2":"456"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddleware":"123","testMiddleware2":"456"}', isBase64Encoded: false }) }) }) // end it @@ -94,7 +94,7 @@ describe('Middleware Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddleware3":"123","testMiddleware4":"456","testMiddleware5":"789"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddleware3":"123","testMiddleware4":"456","testMiddleware5":"789"}', isBase64Encoded: false }) }) }) // end it @@ -105,7 +105,7 @@ describe('Middleware Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddlewarePromise":"test"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","testMiddlewarePromise":"test"}', isBase64Encoded: false }) }) }) // end it diff --git a/test/modules.js b/test/modules.js index 155975a..4b71e2a 100644 --- a/test/modules.js +++ b/test/modules.js @@ -55,7 +55,7 @@ describe('Module Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","app":"app1"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","app":"app1"}', isBase64Encoded: false }) }) }) // end it @@ -65,7 +65,7 @@ describe('Module Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","app":"app2"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","app":"app2"}', isBase64Encoded: false }) }) }) // end it @@ -75,7 +75,7 @@ describe('Module Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a called module error"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a called module error"}', isBase64Encoded: false }) }) }) // end it @@ -85,7 +85,7 @@ describe('Module Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a thrown module error"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"This is a thrown module error"}', isBase64Encoded: false }) }) }) // end it diff --git a/test/namespaces.js b/test/namespaces.js index 4d1edc3..f3748d0 100644 --- a/test/namespaces.js +++ b/test/namespaces.js @@ -64,7 +64,7 @@ describe('Namespace Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log("RESULTS:",result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","data":{"foo":"sample data","bar":"additional sample data"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","data":{"foo":"sample data","bar":"additional sample data"}}', isBase64Encoded: false }) }) }) // end it @@ -75,7 +75,7 @@ describe('Namespace Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log("RESULTS:",result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","data":{"foo":"sample data","bar":"additional sample data"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","data":{"foo":"sample data","bar":"additional sample data"}}', isBase64Encoded: false }) }) }) // end it diff --git a/test/register.js b/test/register.js index 8147c80..9eec36c 100644 --- a/test/register.js +++ b/test/register.js @@ -39,7 +39,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/test-register","route":"/test-register","method":"GET"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/test-register","route":"/test-register","method":"GET"}', isBase64Encoded: false }) }) }) // end it @@ -49,7 +49,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/test-register/sub1","route":"/test-register/sub1","method":"GET"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/test-register/sub1","route":"/test-register/sub1","method":"GET"}', isBase64Encoded: false }) }) }) // end it @@ -59,7 +59,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', isBase64Encoded: false }) }) }) // end it @@ -70,7 +70,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test-register","route":"/test-register","method":"GET"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test-register","route":"/test-register","method":"GET"}', isBase64Encoded: false }) }) }) // end it @@ -80,7 +80,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test-register/sub1","route":"/test-register/sub1","method":"GET"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test-register/sub1","route":"/test-register/sub1","method":"GET"}', isBase64Encoded: false }) }) }) // end it @@ -90,7 +90,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', isBase64Encoded: false }) }) }) // end it @@ -101,7 +101,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register","route":"/test-register","method":"GET"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register","route":"/test-register","method":"GET"}', isBase64Encoded: false }) }) }) // end it @@ -111,7 +111,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register/sub1","route":"/test-register/sub1","method":"GET"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register/sub1","route":"/test-register/sub1","method":"GET"}', isBase64Encoded: false }) }) }) // end it @@ -121,7 +121,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', isBase64Encoded: false }) }) }) // end it @@ -132,7 +132,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register","route":"/test-register","method":"GET"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register","route":"/test-register","method":"GET"}', isBase64Encoded: false }) }) }) // end it @@ -142,7 +142,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register/sub1","route":"/test-register/sub1","method":"GET"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register/sub1","route":"/test-register/sub1","method":"GET"}', isBase64Encoded: false }) }) }) // end it @@ -152,7 +152,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', isBase64Encoded: false }) }) }) // end it @@ -162,7 +162,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register/sub2","route":"/test-register/sub2","method":"GET"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/vX/vY/test-register/sub2","route":"/test-register/sub2","method":"GET"}', isBase64Encoded: false }) }) }) // end it @@ -172,7 +172,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v2/test-register","route":"/test-register","method":"GET"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v2/test-register","route":"/test-register","method":"GET"}', isBase64Encoded: false }) }) }) // end it @@ -182,7 +182,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v2/test-register/sub1","route":"/test-register/sub1","method":"GET"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v2/test-register/sub1","route":"/test-register/sub1","method":"GET"}', isBase64Encoded: false }) }) }) // end it @@ -192,7 +192,7 @@ describe('Register Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v2/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"path":"/v2/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', isBase64Encoded: false }) }) }) // end it diff --git a/test/responses.js b/test/responses.js index c19ad67..0a55965 100644 --- a/test/responses.js +++ b/test/responses.js @@ -86,7 +86,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"object":true}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"object":true}', isBase64Encoded: false }) }) }) // end it @@ -96,7 +96,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '123' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '123', isBase64Encoded: false }) }) }) // end it @@ -106,7 +106,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '[1,2,3]' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '[1,2,3]', isBase64Encoded: false }) }) }) // end it @@ -116,7 +116,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'this is a string' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'this is a string', isBase64Encoded: false }) }) }) // end it @@ -126,7 +126,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '', isBase64Encoded: false }) }) }) // end it @@ -136,7 +136,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'callback({"foo":"bar"})' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'callback({"foo":"bar"})', isBase64Encoded: false }) }) }) // end it @@ -146,7 +146,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'foo({"foo":"bar"})' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'foo({"foo":"bar"})', isBase64Encoded: false }) }) }) // end it @@ -157,7 +157,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api2.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'bar({"foo":"bar"})' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'bar({"foo":"bar"})', isBase64Encoded: false }) }) }) // end it @@ -167,7 +167,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'foo_bar({"foo":"bar"})' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: 'foo_bar({"foo":"bar"})', isBase64Encoded: false }) }) }) // end it @@ -177,7 +177,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com' }, statusCode: 200, body: 'Location header set' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com' }, statusCode: 200, body: 'Location header set', isBase64Encoded: false }) }) }) // end it @@ -187,7 +187,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com?foo=bar%20with%20space' }, statusCode: 200, body: 'Location header set' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com?foo=bar%20with%20space' }, statusCode: 200, body: 'Location header set', isBase64Encoded: false }) }) }) // end it @@ -197,7 +197,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com' }, statusCode: 302, body: '

302 Redirecting to http://www.github.com

' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com' }, statusCode: 302, body: '

302 Redirecting to http://www.github.com

', isBase64Encoded: false }) }) }) // end it @@ -207,7 +207,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com' }, statusCode: 301, body: '

301 Redirecting to http://www.github.com

' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com' }, statusCode: 301, body: '

301 Redirecting to http://www.github.com

', isBase64Encoded: false }) }) }) // end it @@ -217,7 +217,7 @@ describe('Response Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com?foo=bar&bat=baz%3Cscript%3Ealert(\'not%20good\')%3C/script%3E' }, statusCode: 302, body: '

302 Redirecting to http://www.github.com?foo=bar&bat=baz<script>alert('not good')</script>

' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'text/html', 'Location': 'http://www.github.com?foo=bar&bat=baz%3Cscript%3Ealert(\'not%20good\')%3C/script%3E' }, statusCode: 302, body: '

302 Redirecting to http://www.github.com?foo=bar&bat=baz<script>alert('not good')</script>

', isBase64Encoded: false }) }) }) // end it diff --git a/test/routes.js b/test/routes.js index cf27007..07332e2 100644 --- a/test/routes.js +++ b/test/routes.js @@ -21,6 +21,11 @@ let event = { /******************************************************************************/ /*** DEFINE TEST ROUTES ***/ /******************************************************************************/ + +api.get('/', function(req,res) { + res.status(200).json({ method: 'get', status: 'ok' }) +}) + api.get('/test', function(req,res) { res.status(200).json({ method: 'get', status: 'ok' }) }) @@ -146,13 +151,23 @@ describe('Route Tests:', function() { describe('GET', function() { + it('Base path: /', function() { + let _event = Object.assign({},event,{ path: '/' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false }) + }) + }) // end it + it('Simple path: /test', function() { let _event = Object.assign({},event,{}) return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false }) }) }) // end it @@ -162,7 +177,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false }) }) }) // end it @@ -172,7 +187,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123"}', isBase64Encoded: false }) }) }) // end it @@ -183,7 +198,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { //console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123","query":"321"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123","query":"321"}', isBase64Encoded: false }) }) }) // end it @@ -194,7 +209,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', isBase64Encoded: false }) }) }) // end it @@ -206,7 +221,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { //console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123","query":"321"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123","query":"321"}', isBase64Encoded: false }) }) }) // end it @@ -217,7 +232,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { //console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123","query":"321"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123","query":"321"}', isBase64Encoded: false }) }) }) // end it @@ -229,7 +244,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"Route not found"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) }) }) // end it @@ -248,7 +263,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok"}', isBase64Encoded: false }) }) }) // end it @@ -258,7 +273,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok"}', isBase64Encoded: false }) }) }) // end it @@ -268,7 +283,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","param":"123"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","param":"123"}', isBase64Encoded: false }) }) }) // end it @@ -279,7 +294,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { //console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","param":"123","query":"321"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","param":"123","query":"321"}', isBase64Encoded: false }) }) }) // end it @@ -290,7 +305,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', isBase64Encoded: false }) }) }) // end it @@ -301,7 +316,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123"}}', isBase64Encoded: false }) }) }) // end it @@ -312,7 +327,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123"}}', isBase64Encoded: false }) }) }) // end it @@ -323,7 +338,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) }) }) // end it @@ -334,7 +349,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) }) }) // end it @@ -345,7 +360,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) }) }) // end it @@ -356,7 +371,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) }) }) // end it @@ -367,7 +382,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"Route not found"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) }) }) // end it @@ -386,7 +401,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok"}', isBase64Encoded: false }) }) }) // end it @@ -396,7 +411,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok"}', isBase64Encoded: false }) }) }) // end it @@ -406,7 +421,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","param":"123"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","param":"123"}', isBase64Encoded: false }) }) }) // end it @@ -417,7 +432,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { //console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","param":"123","query":"321"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","param":"123","query":"321"}', isBase64Encoded: false }) }) }) // end it @@ -428,7 +443,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', isBase64Encoded: false }) }) }) // end it @@ -440,7 +455,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123"}}', isBase64Encoded: false }) }) }) // end it @@ -451,7 +466,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123"}}', isBase64Encoded: false }) }) }) // end it @@ -462,7 +477,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) }) }) // end it @@ -473,7 +488,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) }) }) // end it @@ -484,7 +499,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) }) }) // end it @@ -495,7 +510,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) }) }) // end it @@ -507,7 +522,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"Route not found"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) }) }) // end it @@ -525,7 +540,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"delete","status":"ok","param":"123"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"delete","status":"ok","param":"123"}', isBase64Encoded: false }) }) }) // end it @@ -536,7 +551,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"delete","status":"ok","params":{"test":"123","test2":"456"}}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"delete","status":"ok","params":{"test":"123","test2":"456"}}', isBase64Encoded: false }) }) }) // end it @@ -548,7 +563,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"Route not found"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) }) }) // end it @@ -567,7 +582,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok"}', isBase64Encoded: false }) }) }) // end it @@ -577,7 +592,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok"}', isBase64Encoded: false }) }) }) // end it @@ -587,7 +602,7 @@ describe('Route Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok","param":"123"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok","param":"123"}', isBase64Encoded: false }) }) }) // end it @@ -598,7 +613,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { //console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok","param":"123","query":"321"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok","param":"123","query":"321"}', isBase64Encoded: false }) }) }) // end it @@ -609,7 +624,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', isBase64Encoded: false }) }) }) // end it @@ -620,7 +635,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok","path":"/*"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok","path":"/*"}', isBase64Encoded: false }) }) }) // end it @@ -631,7 +646,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok","path":"/*"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 200, body: '{"method":"options","status":"ok","path":"/*"}', isBase64Encoded: false }) }) }) // end it @@ -653,7 +668,7 @@ describe('Route Tests:', function() { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { // console.log(result); - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"Route not found"}' }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) }) }) // end it From e8f6686ccee3329e2778fb5a37b14a77077acbda Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 11:37:24 -0400 Subject: [PATCH 13/35] add custom mimeTypes config option --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 8986fed..c55fbd7 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,7 @@ class API { this._version = props && props.version ? props.version : 'v1' this._base = props && props.base && typeof props.base === 'string' ? props.base.trim() : '' this._callbackName = props && props.callback ? props.callback.trim() : 'callback' + this._mimeTypes = props && props.mimeTypes && typeof props.mimeTypes === 'object' ? props.mimeTypes : {} // Prefix stack w/ base this._prefix = this.parseRoute(this._base) From 9a73b68d3dd7f5fff9624587d454eff219b165ce Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 11:38:32 -0400 Subject: [PATCH 14/35] add attachment() method and basic sendFile --- response.js | 51 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/response.js b/response.js index 96e7e3e..0d4da2f 100644 --- a/response.js +++ b/response.js @@ -9,6 +9,10 @@ const escapeHtml = require('./utils.js').escapeHtml const encodeUrl = require('./utils.js').encodeUrl const encodeBody = require('./utils.js').encodeBody +const mimeLookup = require('./utils.js').mimeLookup + +const fs = require('fs') // Require Node.js file system +const path = require('path') // Require Node.js path class RESPONSE { @@ -27,6 +31,9 @@ class RESPONSE { "Content-Type": "application/json" //charset=UTF-8 } + // base64 encoding flag + this._isBase64 = false + // Default callback function this._callback = 'callback' } @@ -138,11 +145,44 @@ class RESPONSE { return this.cookie(name,'',options) } - // TODO: attachement - // TODO: download - // TODO: sendFile - // TODO: sendStatus + + // Set content-disposition header and content type + attachment() { + // Check for supplied filename/path + let filename = arguments.length > 0 ? path.parse(arguments[0]) : undefined + this.header('Content-Disposition','attachment' + (filename ? '; filename="' + filename.base + '"' : '')) + + // If filename exits, attempt to set the type + if (filename) { this.type(filename.ext) } + return this + } + + + // TODO: use attachment() to set headers + download(file) { + // file, filename, options, fn + } + + + sendFile(file,opts={}) { + let data = fs.readFileSync(file) + this._isBase64 = true + this.send(data.toString('base64')) + } + + // TODO: type + type(type) { + let mimeType = mimeLookup(type,this.app._mimeTypes) + if (mimeType) { + this.header('Content-Type',mimeType) + } + return this + } + + + // TODO: sendStatus + // Sends the request to the main callback @@ -152,7 +192,8 @@ class RESPONSE { const response = { headers: this._headers, statusCode: this._statusCode, - body: encodeBody(body) + body: encodeBody(body), + isBase64Encoded: this._isBase64 } // Trigger the callback function From 928c7e3433680e76af29506b3894f4e068f2e9f8 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 18:00:29 -0400 Subject: [PATCH 15/35] add txt to mimemap --- mimemap.js | 1 + 1 file changed, 1 insertion(+) diff --git a/mimemap.js b/mimemap.js index 6de8570..ecd8624 100644 --- a/mimemap.js +++ b/mimemap.js @@ -29,6 +29,7 @@ module.exports = { map: 'application/json', rdf: 'application/rdf+xml', rss: 'application/rss+xml', + txt: 'text/plain', webmanifest: 'application/manifest+json', xml: 'application/xml', xls: 'application/xml', From 91eea99af1e15d403d18f59ff6af7e09a3dfff15 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 18:01:19 -0400 Subject: [PATCH 16/35] add auto reset of isBase64 on error --- index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.js b/index.js index c55fbd7..ec3a17b 100644 --- a/index.js +++ b/index.js @@ -152,6 +152,9 @@ class API { }).catch((e) => { + // Error messages are never base64 encoded + response._isBase64 = false + let message; if (e instanceof Error) { From a0f8a78b6a12c99adf20259cf8d836801b6f97df Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 18:02:07 -0400 Subject: [PATCH 17/35] add initial sendFile() functionality --- response.js | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/response.js b/response.js index 0d4da2f..53505fe 100644 --- a/response.js +++ b/response.js @@ -14,6 +14,9 @@ const mimeLookup = require('./utils.js').mimeLookup const fs = require('fs') // Require Node.js file system const path = require('path') // Require Node.js path +const Promise = require('bluebird') // Promise library + + class RESPONSE { // Create the constructor function. @@ -164,10 +167,74 @@ class RESPONSE { } - sendFile(file,opts={}) { - let data = fs.readFileSync(file) - this._isBase64 = true - this.send(data.toString('base64')) + sendFile(file) { + + let buffer, modified + + let opts = arguments.length > 1 && typeof arguments[1] === 'object' ? arguments[1] : {} + let fn = arguments.length > 1 && typeof arguments[1] === 'function' ? arguments[1] : + (arguments.length > 2 && typeof arguments[2] === 'function' ? arguments[2] : + e => { if(e) this.error(e) } ) + + // Begin a promise chain + Promise.try(() => { + + // Create buffer based on input + if (typeof file === 'string') { + if (/^s3:\/\//i.test(file)) { + console.log('S3'); + buffer = 'empty' + } else { + buffer = fs.readFileSync((opts.root ? opts.root : '') + file) + modified = fs.statSync((opts.root ? opts.root : '') + file).mtime // only if last-modified? + this.type(path.extname(file)) + } + } else if (Buffer.isBuffer(file)) { + buffer = file + } else { + throw new Error('Invalid file') + } + + // Add headers from options + if (typeof opts.headers === 'object') { + Object.keys(opts.headers).map(header => { + this.header(header,opts.headers[header]) + }) + } + + // Add cache-control headers + if (opts.cacheControl !== false) { + if (opts.cacheControl !== true && opts.cacheControl !== undefined) { + this.header('Cache-Control', opts.cacheControl) + } else { + let maxAge = opts.maxAge && !isNaN(opts.maxAge) ? (opts.maxAge/1000|0) : 0 + this.header('Cache-Control', (opts.private === true ? 'private, ' : '') + 'max-age=' + maxAge) + this.header('Expires',new Date(Date.now() + maxAge).toUTCString()) + } + } + + // Add last-modified headers + if (opts.lastModified !== false) { + let lastModified = opts.lastModified && typeof opts.lastModified.toUTCString === 'function' ? opts.lastModified : (modified ? modified : new Date()) + this.header('Last-Modified', lastModified.toUTCString()) + } + + }).then(() => { + // Execute callback + return Promise.resolve(fn()) + }).then(() => { + // Set base64 encoding flag + this._isBase64 = true + // Convert buffer to base64 string + this.send(buffer.toString('base64')) + }).catch(e => { + // Execute callback with caught error + return Promise.resolve(fn(e)) + }).catch(e => { + // Catch any final error + this.error(e) + }) + } From e27c672edd1378ba50de0f49f40d61a3ffb473f6 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 18:02:36 -0400 Subject: [PATCH 18/35] add tests for sendFile and test.txt file --- test/sendFile.js | 298 +++++++++++++++++++++++++++++++++++++++++++++++ test/test.txt | 1 + 2 files changed, 299 insertions(+) create mode 100644 test/sendFile.js create mode 100644 test/test.txt diff --git a/test/sendFile.js b/test/sendFile.js new file mode 100644 index 0000000..24db35a --- /dev/null +++ b/test/sendFile.js @@ -0,0 +1,298 @@ +'use strict'; + +const Promise = require('bluebird') // Promise library +const expect = require('chai').expect // Assertion library + +const fs = require('fs') // Require Node.js file system + +// Init API instance +const api = require('../index')({ version: 'v1.0', mimeTypes: { test: 'text/test' } }) + +// NOTE: Set test to true +api._test = true; + +let event = { + httpMethod: 'get', + path: '/', + body: {}, + headers: { + 'Content-Type': 'application/json' + } +} + +/******************************************************************************/ +/*** DEFINE TEST ROUTES ***/ +/******************************************************************************/ + +api.get('/sendfile/badpath', function(req,res) { + res.sendFile() +}) + +api.get('/sendfile', function(req,res) { + res.sendFile('./test-missing.txt') +}) + +api.get('/sendfile/err', function(req,res) { + res.sendFile('./test-missing.txt', err => { + if (err) { + res.status(404).error('There was an error accessing the requested file') + } + }) +}) + +api.get('/sendfile/test', function(req,res) { + res.sendFile('test/test.txt' + (req.query.test ? req.query.test : ''), err => { + + if (err) { + return Promise.try(() => { + for(let i = 0; i<40000000; i++) {} + return true + }).then((x) => { + res.status(501).error('Custom File Error') + // throw('test error') + }) + } else { + return Promise.try(() => { + for(let i = 0; i<40000000; i++) {} + return true + }).then((x) => { + res.status(201) + }) + } + + }) +}) + +api.get('/sendfile/buffer', function(req,res) { + res.sendFile(fs.readFileSync('test/test.txt')) +}) + +api.get('/sendfile/headers', function(req,res) { + res.sendFile('test/test.txt', { + headers: { 'x-test': 'test', 'x-timestamp': 1 } + }) +}) + +api.get('/sendfile/headers-private', function(req,res) { + res.sendFile('test/test.txt', { + headers: { 'x-test': 'test', 'x-timestamp': 1 }, + private: true + }) +}) + +api.get('/sendfile/last-modified', function(req,res) { + res.sendFile('test/test.txt', { + lastModified: new Date('Fri, 1 Jan 2018 00:00:00 GMT') + }) +}) + +api.get('/sendfile/no-last-modified', function(req,res) { + res.sendFile('test/test.txt', { + lastModified: false + }) +}) + +api.get('/sendfile/no-cache-control', function(req,res) { + res.sendFile('test/test.txt', { + cacheControl: false + }) +}) + +api.get('/sendfile/custom-cache-control', function(req,res) { + res.sendFile('test/test.txt', { + cacheControl: 'no-cache, no-store' + }) +}) + +// Error Middleware +api.use(function(err,req,res,next) { + next() +}) + +/******************************************************************************/ +/*** BEGIN TESTS ***/ +/******************************************************************************/ + +describe('SendFile Tests:', function() { + + it('Bad path', function() { + let _event = Object.assign({},event,{ path: '/sendfile/badpath' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"Invalid file"}', isBase64Encoded: false }) + }) + }) // end it + + it('Missing file', function() { + let _event = Object.assign({},event,{ path: '/sendfile' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"ENOENT: no such file or directory, open \'./test-missing.txt\'"}', isBase64Encoded: false }) + }) + }) // end it + + + + it('Missing file with custom catch', function() { + let _event = Object.assign({},event,{ path: '/sendfile/err' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"There was an error accessing the requested file"}', isBase64Encoded: false }) + }) + }) // end it + + + it('Text file w/ callback override (promise)', function() { + let _event = Object.assign({},event,{ path: '/sendfile/test' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'Cache-Control': 'max-age=0', + 'Expires': result.headers.Expires, + 'Last-Modified': result.headers['Last-Modified'] + }, + statusCode: 201, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + + it('Text file error w/ callback override (promise)', function() { + let _event = Object.assign({},event,{ path: '/sendfile/test', queryStringParameters: { test: 'x' } }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 501, body: '{"error":"Custom File Error"}', isBase64Encoded: false }) + }) + }) // end it + + + + it('Buffer Input', function() { + let _event = Object.assign({},event,{ path: '/sendfile/buffer' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'max-age=0', + 'Expires': result.headers.Expires, + 'Last-Modified': result.headers['Last-Modified'] + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + + it('Text file w/ headers', function() { + let _event = Object.assign({},event,{ path: '/sendfile/headers' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'x-test': 'test', + 'x-timestamp': 1, + 'Cache-Control': 'max-age=0', + 'Expires': result.headers.Expires, + 'Last-Modified': result.headers['Last-Modified'] + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + it('Text file w/ headers (private cache)', function() { + let _event = Object.assign({},event,{ path: '/sendfile/headers-private' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'x-test': 'test', + 'x-timestamp': 1, + 'Cache-Control': 'private, max-age=0', + 'Expires': result.headers.Expires, + 'Last-Modified': result.headers['Last-Modified'] + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + it('Text file custom Last-Modified', function() { + let _event = Object.assign({},event,{ path: '/sendfile/last-modified' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'Cache-Control': 'max-age=0', + 'Expires': result.headers.Expires, + 'Last-Modified': 'Mon, 01 Jan 2018 00:00:00 GMT' + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + + it('Text file no Last-Modified', function() { + let _event = Object.assign({},event,{ path: '/sendfile/no-last-modified' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'Cache-Control': 'max-age=0', + 'Expires': result.headers.Expires + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + + it('Text file no Cache-Control', function() { + let _event = Object.assign({},event,{ path: '/sendfile/no-cache-control' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'Last-Modified': result.headers['Last-Modified'] + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + + it('Text file custom Cache-Control', function() { + let _event = Object.assign({},event,{ path: '/sendfile/custom-cache-control' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'Cache-Control': 'no-cache, no-store', + 'Last-Modified': result.headers['Last-Modified'] + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + +}) // end HEADER tests diff --git a/test/test.txt b/test/test.txt new file mode 100644 index 0000000..e2c973b --- /dev/null +++ b/test/test.txt @@ -0,0 +1 @@ +Test file for sendFile From 229b93f071591a090dbd16abea89408b2bf612cc Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 20:07:09 -0400 Subject: [PATCH 19/35] initial documentation for sendFile and new config options --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 15a7cd6..4fb40bf 100644 --- a/README.md +++ b/README.md @@ -124,13 +124,22 @@ npm i lambda-api --save ## Configuration -Require the `lambda-api` module into your Lambda handler script and instantiate it. You can initialize the API with an optional `version` which can be accessed via the `REQUEST` object and a `base` path. +Require the `lambda-api` module into your Lambda handler script and instantiate it. You can initialize the API with the following options: + +| Property | Type | Description | +| -------- | ---- | ----------- | +| version | `String` | Version number accessible via the `REQUEST` object | +| base | `String` | Base path for all routes, e.g. `base: 'v1'` would prefix all routes with `/v1` | +| callbackName | `String` | Override the default callback query parameter name for JSONP calls | +| mimeTypes | `Object` | Name/value pairs of additional MIME types to be supported by the `type()`. The key should be the file extension (without the `.`) and the value should be the expected MIME type, e.g. `application/json` | ```javascript // Require the framework and instantiate it with optional version and base parameters const api = require('lambda-api')({ version: 'v1.0', base: 'v1' }); ``` +Supported `options`: version, base, callbackName, mimeTypes + ## Routes and HTTP Methods Routes are defined by using convenience methods or the `METHOD` method. There are currently five convenience route methods: `get()`, `post()`, `put()`, `delete()` and `options()`. Convenience route methods require two parameters, a *route* and a function that accepts two arguments. A *route* is simply a path such as `/users`. The second parameter must be a function that accepts a `REQUEST` and a `RESPONSE` argument. These arguments can be named whatever you like, but convention dictates `req` and `res`. Examples using convenience route methods: @@ -251,6 +260,21 @@ api.get('/users', function(req,res) { }) ``` +### type +Sets the `Content-Type` header for you based on a single `String` input. There are thousands of MIME types, many of which are likely never to be used by your application. Lambda API stores a list of the most popular file types and will automatically set the correct `Content-Type` based on the input. If the `type` contains the "/" character, then it sets the `Content-Type` to the value of `type`. + +```javascript +res.type('.html'); // => 'text/html' +res.type('html'); // => 'text/html' +res.type('json'); // => 'application/json' +res.type('application/json'); // => 'application/json' +res.type('png'); // => 'image/png' +res.type('.doc'); // => 'application/msword' +res.type('text/css'); // => 'text/css' +``` + +For a complete list of auto supported types, see [mimemap.js](mindmap.js). Custom MIME types can be added by using the `mimeTypes` option when instantiating Lambda API + ### location The `location` convenience method sets the `Location:` header with the value of a single string argument. The value passed in is not validated but will be encoded before being added to the header. Values that are already encoded can be safely passed in. Note that a valid `3xx` status code must be set to trigger browser redirection. The value can be a relative/absolute path OR a FQDN. @@ -320,6 +344,25 @@ res.clearCookie('fooArray', { path: '/', httpOnly: true }).send() ``` **NOTE:** The `clearCookie()` method only sets the header. A execution ending method like `send()`, `json()`, etc. must be called to send the response. +### attachment + +### download + +### sendFile + +The `sendFile()` method takes up to three arguments. The first is the file. This is either a local filename (stored within your uploaded lambda code), a reference to a file in S3 (using the `s3://{my-bucket}/{path-to-file}` format), or a JavaScript `Buffer`. You can optionally pass an `options` object using the properties below as well as a callback function `fn(err)` that can handle custom errors or manipulate the response before sending to the client. + +| Property | Type | Description | Default | +| -------- | ---- | ----------- | ------- | +| maxAge | `Number` | Set the expiration time relative to the current time in milliseconds. Automatically sets the `Expires` header | 0 | +| root | `String` | Root directory for relative filenames. | | +| lastModified | `Boolean` or `String` | Sets the `Last-Modified` header to the last modified date of the file. This can be disabled by setting it to `false`, or overridden by setting it to a valid `Date` object | | +| headers | `Object` | Key value pairs of additional headers to be sent with the file | | +| cacheControl | `Boolean` or `String` | Enable or disable setting `Cache-Control` response header. Override value with custom string. | true | +| private | `Boolean` | Sets the `Cache-Control` to `private`. | false | + + + ## Path Parameters Path parameters are extracted from the path sent in by API Gateway. Although API Gateway supports path parameters, the API doesn't use these values but insteads extracts them from the actual path. This gives you more flexibility with the API Gateway configuration. Path parameters are defined in routes using a colon `:` as a prefix. From 9012dc66294eae90d3573acf6add00419f7e2bd7 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 20:16:07 -0400 Subject: [PATCH 20/35] close #18 by adding patch() convenience method --- index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/index.js b/index.js index ec3a17b..db9b048 100644 --- a/index.js +++ b/index.js @@ -78,6 +78,11 @@ class API { this.METHOD('PUT', path, handler) } + // PATCH: convenience method + patch(path, handler) { + this.METHOD('PATCH', path, handler) + } + // DELETE: convenience method delete(path, handler) { this.METHOD('DELETE', path, handler) From 71316e9fe9ae9e3b9db34d21915de2f81cb50113 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 20:18:45 -0400 Subject: [PATCH 21/35] add patch() documentation --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4fb40bf..bbc050a 100644 --- a/README.md +++ b/README.md @@ -138,11 +138,9 @@ Require the `lambda-api` module into your Lambda handler script and instantiate const api = require('lambda-api')({ version: 'v1.0', base: 'v1' }); ``` -Supported `options`: version, base, callbackName, mimeTypes - ## Routes and HTTP Methods -Routes are defined by using convenience methods or the `METHOD` method. There are currently five convenience route methods: `get()`, `post()`, `put()`, `delete()` and `options()`. Convenience route methods require two parameters, a *route* and a function that accepts two arguments. A *route* is simply a path such as `/users`. The second parameter must be a function that accepts a `REQUEST` and a `RESPONSE` argument. These arguments can be named whatever you like, but convention dictates `req` and `res`. Examples using convenience route methods: +Routes are defined by using convenience methods or the `METHOD` method. There are currently six convenience route methods: `get()`, `post()`, `put()`, `patch()`, `delete()` and `options()`. Convenience route methods require two parameters, a *route* and a function that accepts two arguments. A *route* is simply a path such as `/users`. The second parameter must be a function that accepts a `REQUEST` and a `RESPONSE` argument. These arguments can be named whatever you like, but convention dictates `req` and `res`. Examples using convenience route methods: ```javascript api.get('/users', function(req,res) { @@ -173,7 +171,7 @@ The `REQUEST` object contains a parsed and normalized request from API Gateway. - `version`: The version set at initialization - `params`: Dynamic path parameters parsed from the path (see [path parameters](#path-parameters)) - `method`: The HTTP method of the request -- `path`: The path passed in by the request +- `path`: The path passed in by the request including `base` and `prefix`es - `query`: Querystring parameters parsed into an object - `headers`: An object containing the request headers (properties converted to lowercase for HTTP/2, see [rfc7540 8.1.2. HTTP Header Fields](https://tools.ietf.org/html/rfc7540)) - `rawHeaders`: An object containing the original request headers (property case preserved) From 7e5ee408f9b8b4b0640412d9a805be4ac2f1ebc0 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 20:20:25 -0400 Subject: [PATCH 22/35] documentation cleanup --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bbc050a..47951ab 100644 --- a/README.md +++ b/README.md @@ -171,14 +171,14 @@ The `REQUEST` object contains a parsed and normalized request from API Gateway. - `version`: The version set at initialization - `params`: Dynamic path parameters parsed from the path (see [path parameters](#path-parameters)) - `method`: The HTTP method of the request -- `path`: The path passed in by the request including `base` and `prefix`es +- `path`: The path passed in by the request including the `base` and any `prefix` assigned to routes - `query`: Querystring parameters parsed into an object - `headers`: An object containing the request headers (properties converted to lowercase for HTTP/2, see [rfc7540 8.1.2. HTTP Header Fields](https://tools.ietf.org/html/rfc7540)) - `rawHeaders`: An object containing the original request headers (property case preserved) - `body`: The body of the request. - - If the `Content-Type` header is `application/json`, it will attempt to parse the request using `JSON.parse()` - - If the `Content-Type` header is `application/x-www-form-urlencoded`, it will attempt to parse a URL encoded string using `querystring` - - Otherwise it will be plain text. + - If the `Content-Type` header is `application/json`, it will attempt to parse the request using `JSON.parse()` + - If the `Content-Type` header is `application/x-www-form-urlencoded`, it will attempt to parse a URL encoded string using `querystring` + - Otherwise it will be plain text. - `route`: The matched route of the request - `requestContext`: The `requestContext` passed from the API Gateway - `namespace` or `ns`: A reference to modules added to the app's namespace (see [namespaces](#namespaces)) From 853019990d2d68f0f84fa509f05c42fde39bc21a Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 21:00:02 -0400 Subject: [PATCH 23/35] add register() documentation --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index 47951ab..a358eed 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,52 @@ api.METHOD('patch','/users', function(req,res) { }) ``` +## Route Prefixing + +Lambda API makes it easy to create multiple versions of the same api without changing routes by hand. The `register()` method allows you to load routes from an external file and prefix all of those routes using the `prefix` option. For example: + +```javascript +// handler.js +const api = require('lambda-api')() + +api.register(require('./routes/v1/products'), { prefix: '/v1' }) +api.register(require('./routes/v2/products'), { prefix: '/v2' }) + +module.exports.handler = (event, context, callback) => { + api.run(event, context, callback) +} +``` + +```javascript +// routes/v1/products.js +module.exports = (api, opts) => { + api.get('/product', handler_v1) +} +``` + +```javascript +// routes/v2/products.js +module.exports = (api, opts) => { + api.get('/product', handler_v2) +} +``` + +Even though both modules create a `/product` route, Lambda API will add the `prefix` to them creating two unique routes. Your users can now access: +- `/v1/product` +- `/v2/product` + +You can use `register()` as many times as you want AND it is recursive, so if you nest `register()` methods, the routes will build upon each other. For example: + +```javascript +module.exports = (api, opts) => { + api.get('/product', handler_v1) + api.register(require('./v2/products.js'), { prefix: '/v2'} ) +} +``` + +This would create a `/v1/product` and `/v1/v2/product` route. You can also use `register()` to load routes from an external file without the `prefix`. This will just add routes to your `base` path. **NOTE:** Prefixed routes are built off of your `base` path if one is set. If your `base` was set to `/api`, then the first example above would produce the routes: `/api/v1/product` and `/api/v2/product`. + + ## REQUEST The `REQUEST` object contains a parsed and normalized request from API Gateway. It contains the following values by default: From aa9bcaa194a83bebb287cd945b2cb8712d954362 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 21:09:07 -0400 Subject: [PATCH 24/35] add attachment() documentation --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a358eed..850d58d 100644 --- a/README.md +++ b/README.md @@ -389,11 +389,20 @@ res.clearCookie('fooArray', { path: '/', httpOnly: true }).send() **NOTE:** The `clearCookie()` method only sets the header. A execution ending method like `send()`, `json()`, etc. must be called to send the response. ### attachment +Sets the HTTP response `Content-Disposition` header field to "attachment". If a `filename` is provided, then the `Content-Type` is set based on the file extension using the `type()` method and the "filename=" parameter is added to the `Content-Disposition` header. + +```javascript +res.attachment() +// Content-Disposition: attachment + +res.attachment('path/to/logo.png') +// Content-Disposition: attachment; filename="logo.png" +// Content-Type: image/png +``` ### download ### sendFile - The `sendFile()` method takes up to three arguments. The first is the file. This is either a local filename (stored within your uploaded lambda code), a reference to a file in S3 (using the `s3://{my-bucket}/{path-to-file}` format), or a JavaScript `Buffer`. You can optionally pass an `options` object using the properties below as well as a callback function `fn(err)` that can handle custom errors or manipulate the response before sending to the client. | Property | Type | Description | Default | From 6c745d61c03f855a9582f6d1e70700fffca097ba Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 22:38:51 -0400 Subject: [PATCH 25/35] add download() method --- response.js | 61 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/response.js b/response.js index 53505fe..68d2ef2 100644 --- a/response.js +++ b/response.js @@ -150,31 +150,59 @@ class RESPONSE { // Set content-disposition header and content type - attachment() { + attachment(filename) { // Check for supplied filename/path - let filename = arguments.length > 0 ? path.parse(arguments[0]) : undefined - this.header('Content-Disposition','attachment' + (filename ? '; filename="' + filename.base + '"' : '')) + let name = typeof filename === 'string' && filename.trim().length > 0 ? path.parse(filename) : undefined + this.header('Content-Disposition','attachment' + (name ? '; filename="' + name.base + '"' : '')) - // If filename exits, attempt to set the type - if (filename) { this.type(filename.ext) } + // If name exits, attempt to set the type + if (name) { this.type(name.ext) } return this } - // TODO: use attachment() to set headers - download(file) { - // file, filename, options, fn + // Convenience method combining attachment() and sendFile() + download(file, filename, options, callback) { + + let name = filename + let opts = typeof options === 'object' ? options : {} + let fn = typeof callback === 'function' ? callback : undefined + + // Add optional parameter support for callback + if (typeof filename === 'function') { + name = undefined + fn = filename + } else if (typeof options === 'function') { + fn = options + } + + // Add optional parameter support for options + if (typeof filename === 'object') { + name = undefined + opts = filename + } + + // Add the Content-Disposition header + this.attachment(name ? name : (typeof file === 'string' ? path.basename(file) : null) ) + + // Send the file + this.sendFile(file, opts, fn) + } - sendFile(file) { + // Convenience method for returning static files + sendFile(file, options, callback) { let buffer, modified - let opts = arguments.length > 1 && typeof arguments[1] === 'object' ? arguments[1] : {} - let fn = arguments.length > 1 && typeof arguments[1] === 'function' ? arguments[1] : - (arguments.length > 2 && typeof arguments[2] === 'function' ? arguments[2] : - e => { if(e) this.error(e) } ) + let opts = typeof options === 'object' ? options : {} + let fn = typeof callback === 'function' ? callback : e => { if(e) this.error(e) } + + // Add optional parameter support + if (typeof options === 'function') { + fn = options + } // Begin a promise chain Promise.try(() => { @@ -186,7 +214,7 @@ class RESPONSE { buffer = 'empty' } else { buffer = fs.readFileSync((opts.root ? opts.root : '') + file) - modified = fs.statSync((opts.root ? opts.root : '') + file).mtime // only if last-modified? + modified = opts.lastModified !== false ? fs.statSync((opts.root ? opts.root : '') + file).mtime : undefined this.type(path.extname(file)) } } else if (Buffer.isBuffer(file)) { @@ -235,10 +263,10 @@ class RESPONSE { this.error(e) }) - } + } // end sendFile - // TODO: type + // Convenience method for setting type type(type) { let mimeType = mimeLookup(type,this.app._mimeTypes) if (mimeType) { @@ -248,6 +276,7 @@ class RESPONSE { } + // TODO: sendStatus From 36437135b691b31bd02680cb83fb80494a4b0349 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 22:39:17 -0400 Subject: [PATCH 26/35] add tests for download() method --- test/download.js | 239 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 test/download.js diff --git a/test/download.js b/test/download.js new file mode 100644 index 0000000..0772942 --- /dev/null +++ b/test/download.js @@ -0,0 +1,239 @@ +'use strict'; + +const Promise = require('bluebird') // Promise library +const expect = require('chai').expect // Assertion library + +const fs = require('fs') // Require Node.js file system + +// Init API instance +const api = require('../index')({ version: 'v1.0', mimeTypes: { test: 'text/test' } }) + +// NOTE: Set test to true +api._test = true; + +let event = { + httpMethod: 'get', + path: '/', + body: {}, + headers: { + 'Content-Type': 'application/json' + } +} + +/******************************************************************************/ +/*** DEFINE TEST ROUTES ***/ +/******************************************************************************/ + +api.get('/download/badpath', function(req,res) { + res.download() +}) + +api.get('/download', function(req,res) { + res.download('./test-missing.txt') +}) + +api.get('/download/err', function(req,res) { + res.download('./test-missing.txt', err => { + if (err) { + res.status(404).error('There was an error accessing the requested file') + } + }) +}) + +api.get('/download/test', function(req,res) { + res.download('test/test.txt' + (req.query.test ? req.query.test : ''), err => { + + if (err) { + return Promise.try(() => { + for(let i = 0; i<40000000; i++) {} + return true + }).then((x) => { + res.status(501).error('Custom File Error') + // throw('test error') + }) + } else { + return Promise.try(() => { + for(let i = 0; i<40000000; i++) {} + return true + }).then((x) => { + res.status(201) + }) + } + + }) +}) + +api.get('/download/buffer', function(req,res) { + res.download(fs.readFileSync('test/test.txt'), req.query.filename ? req.query.filename : undefined) +}) + +api.get('/download/headers', function(req,res) { + res.download('test/test.txt', { + headers: { 'x-test': 'test', 'x-timestamp': 1 } + }) +}) + +api.get('/download/headers-private', function(req,res) { + res.download('test/test.txt', { + headers: { 'x-test': 'test', 'x-timestamp': 1 }, + private: true + }) +}) + +api.get('/download/all', function(req,res) { + res.download('test/test.txt', 'test-file.txt', { private: true, maxAge: 3600000 }, err => { res.header('x-callback','true') }) +}) + +// Error Middleware +api.use(function(err,req,res,next) { + res.header('x-error','true') + next() +}) + +/******************************************************************************/ +/*** BEGIN TESTS ***/ +/******************************************************************************/ + +describe('Download Tests:', function() { + + it('Bad path', function() { + let _event = Object.assign({},event,{ path: '/download/badpath' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json', 'x-error': 'true' }, statusCode: 500, body: '{"error":"Invalid file"}', isBase64Encoded: false }) + }) + }) // end it + + it('Missing file', function() { + let _event = Object.assign({},event,{ path: '/download' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json', 'x-error': 'true' }, statusCode: 500, body: '{"error":"ENOENT: no such file or directory, open \'./test-missing.txt\'"}', isBase64Encoded: false }) + }) + }) // end it + + + + it('Missing file with custom catch', function() { + let _event = Object.assign({},event,{ path: '/download/err' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json', 'x-error': 'true' }, statusCode: 404, body: '{"error":"There was an error accessing the requested file"}', isBase64Encoded: false }) + }) + }) // end it + + + it('Text file w/ callback override (promise)', function() { + let _event = Object.assign({},event,{ path: '/download/test' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'Cache-Control': 'max-age=0', + 'Expires': result.headers.Expires, + 'Last-Modified': result.headers['Last-Modified'], + 'Content-Disposition': 'attachment; filename="test.txt"' + }, + statusCode: 201, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + + it('Text file error w/ callback override (promise)', function() { + let _event = Object.assign({},event,{ path: '/download/test', queryStringParameters: { test: 'x' } }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json', 'x-error': 'true' }, statusCode: 501, body: '{"error":"Custom File Error"}', isBase64Encoded: false }) + }) + }) // end it + + + + it('Buffer Input (no filename)', function() { + let _event = Object.assign({},event,{ path: '/download/buffer' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'max-age=0', + 'Expires': result.headers.Expires, + 'Last-Modified': result.headers['Last-Modified'], + 'Content-Disposition': 'attachment' + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + + + it('Buffer Input (w/ filename)', function() { + let _event = Object.assign({},event,{ path: '/download/buffer', queryStringParameters: { filename: 'test.txt' } }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'Cache-Control': 'max-age=0', + 'Expires': result.headers.Expires, + 'Last-Modified': result.headers['Last-Modified'], + 'Content-Disposition': 'attachment; filename="test.txt"' + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + + it('Text file w/ headers', function() { + let _event = Object.assign({},event,{ path: '/download/headers' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'x-test': 'test', + 'x-timestamp': 1, + 'Cache-Control': 'max-age=0', + 'Expires': result.headers.Expires, + 'Last-Modified': result.headers['Last-Modified'], + 'Content-Disposition': 'attachment; filename="test.txt"' + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + + it('Text file w/ filename, options, and callback', function() { + let _event = Object.assign({},event,{ path: '/download/all' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'x-callback': 'true', + 'Cache-Control': 'private, max-age=3600', + 'Expires': result.headers.Expires, + 'Last-Modified': result.headers['Last-Modified'], + 'Content-Disposition': 'attachment; filename="test-file.txt"' + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + +}) // end HEADER tests From f86af2d17567d1e87c60fa200bc29bde54c5ee65 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 22:40:56 -0400 Subject: [PATCH 27/35] add additional attachment tests --- test/attachments.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/attachments.js b/test/attachments.js index 06bce0c..425efc8 100644 --- a/test/attachments.js +++ b/test/attachments.js @@ -35,13 +35,21 @@ api.get('/attachment/png', function(req,res) { }) api.get('/attachment/csv', function(req,res) { - res.attachment('/test/path/foo.csv').send('filedata') + res.attachment('test/path/foo.csv').send('filedata') }) api.get('/attachment/custom', function(req,res) { res.attachment('/test/path/foo.test').send('filedata') }) +api.get('/attachment/empty-string', function(req,res) { + res.attachment(' ').send('filedata') +}) + +api.get('/attachment/null-string', function(req,res) { + res.attachment(null).send('filedata') +}) + /******************************************************************************/ /*** BEGIN TESTS ***/ @@ -99,4 +107,24 @@ describe('Attachment Tests:', function() { }) }) // end it + it('Empty string', function() { + let _event = Object.assign({},event,{ path: '/attachment/empty-string' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Disposition': 'attachment', 'Content-Type': 'application/json' }, statusCode: 200, body: 'filedata', isBase64Encoded: false }) + }) + }) // end it + + it('Null string', function() { + let _event = Object.assign({},event,{ path: '/attachment/empty-string' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ headers: { 'Content-Disposition': 'attachment', 'Content-Type': 'application/json' }, statusCode: 200, body: 'filedata', isBase64Encoded: false }) + }) + }) // end it + }) // end HEADER tests From 86c0a1778bf3bfb7664cf842a864733de3c20285 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 22:41:26 -0400 Subject: [PATCH 28/35] add strip headers on error --- index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index db9b048..da7384a 100644 --- a/index.js +++ b/index.js @@ -157,9 +157,12 @@ class API { }).catch((e) => { - // Error messages are never base64 encoded + // Error messages should never be base64 encoded response._isBase64 = false + // Strip the headers (TODO: find a better way to handle this) + response._headers = {} + let message; if (e instanceof Error) { From aed99f6a437bd5ba46f954e01c09214d8bdf0f40 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Fri, 30 Mar 2018 23:02:56 -0400 Subject: [PATCH 29/35] additional documentation for download/sendfile and other updates --- README.md | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 850d58d..3b983c9 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ The request object can be used to pass additional information through the proces The `RESPONSE` object is used to send a response back to the API Gateway. The `RESPONSE` object contains several methods to manipulate responses. All methods are chainable unless they trigger a response. -### status +### status(code) The `status` method allows you to set the status code that is returned to API Gateway. By default this will be set to `200` for normal requests or `500` on a thrown error. Additional built-in errors such as `404 Not Found` and `405 Method Not Allowed` may also be returned. The `status()` method accepts a single integer argument. ```javascript @@ -245,7 +245,7 @@ api.get('/users', function(req,res) { }) ``` -### header +### header(field, value) The `header` method allows for you to set additional headers to return to the client. By default, just the `Content-Type` header is sent with `application/json` as the value. Headers can be added or overwritten by calling the `header()` method with two string arguments. The first is the name of the header and then second is the value. ```javascript @@ -254,10 +254,10 @@ api.get('/users', function(req,res) { }) ``` -### send +### send(body) The `send` methods triggers the API to return data to the API Gateway. The `send` method accepts one parameter and sends the contents through as is, e.g. as an object, string, integer, etc. AWS Gateway expects a string, so the data should be converted accordingly. -### json +### json(body) There is a `json` convenience method for the `send` method that will set the headers to `application/json` as well as perform `JSON.stringify()` on the contents passed to it. ```javascript @@ -266,7 +266,7 @@ api.get('/users', function(req,res) { }) ``` -### jsonp +### jsonp(body) There is a `jsonp` convenience method for the `send` method that will set the headers to `application/json`, perform `JSON.stringify()` on the contents passed to it, and wrap the results in a callback function. By default, the callback function is named `callback`. ```javascript @@ -295,7 +295,7 @@ res.jsonp({ foo: 'bar' }) // => bar({ "foo": "bar" }) ``` -### html +### html(body) There is also an `html` convenience method for the `send` method that will set the headers to `text/html` and pass through the contents. ```javascript @@ -304,7 +304,7 @@ api.get('/users', function(req,res) { }) ``` -### type +### type(type) Sets the `Content-Type` header for you based on a single `String` input. There are thousands of MIME types, many of which are likely never to be used by your application. Lambda API stores a list of the most popular file types and will automatically set the correct `Content-Type` based on the input. If the `type` contains the "/" character, then it sets the `Content-Type` to the value of `type`. ```javascript @@ -319,7 +319,7 @@ res.type('text/css'); // => 'text/css' For a complete list of auto supported types, see [mimemap.js](mindmap.js). Custom MIME types can be added by using the `mimeTypes` option when instantiating Lambda API -### location +### location(path) The `location` convenience method sets the `Location:` header with the value of a single string argument. The value passed in is not validated but will be encoded before being added to the header. Values that are already encoded can be safely passed in. Note that a valid `3xx` status code must be set to trigger browser redirection. The value can be a relative/absolute path OR a FQDN. ```javascript @@ -332,7 +332,7 @@ api.get('/redirectToGithub', function(req,res) { }) ``` -### redirect +### redirect([status,] path) The `redirect` convenience method triggers a redirection and ends the current API execution. This method is similar to the `location()` method, but it automatically sets the status code and calls `send()`. The redirection URL (relative/absolute path OR a FQDN) can be specified as the only parameter or as a second parameter when a valid `3xx` status code is supplied as the first parameter. The status code is set to `302` by default, but can be changed to `300`, `301`, `302`, `303`, `307`, or `308` by adding it as the first parameter. ```javascript @@ -345,7 +345,7 @@ api.get('/redirectToGithub', function(req,res) { }) ``` -### error +### error(message) An error can be triggered by calling the `error` method. This will cause the API to stop execution and return the message to the client. Custom error handling can be accomplished using the [Error Handling](#error-handling) feature. ```javascript @@ -354,7 +354,7 @@ api.get('/users', function(req,res) { }) ``` -### cookie +### cookie(name, value [,options]) Convenience method for setting cookies. This method accepts a `name`, `value` and an optional `options` object with the following parameters: @@ -378,7 +378,7 @@ res.cookie('fooObject', { foo: 'bar' }, { domain: '.test.com', path: '/admin', h res.cookie('fooArray', [ 'one', 'two', 'three' ], { path: '/', httpOnly: true }).send() ``` -### clearCookie +### clearCookie(name [,options]) Convenience method for expiring cookies. Requires the `name` and optional `options` object as specified in the [cookie](#cookie) method. This method will automatically set the expiration time. However, most browsers require the same options to clear a cookie as was used to set it. E.g. if you set the `path` to "/admin" when you set the cookie, you must use this same value to clear it. ```javascript @@ -388,7 +388,7 @@ res.clearCookie('fooArray', { path: '/', httpOnly: true }).send() ``` **NOTE:** The `clearCookie()` method only sets the header. A execution ending method like `send()`, `json()`, etc. must be called to send the response. -### attachment +### attachment([filename]) Sets the HTTP response `Content-Disposition` header field to "attachment". If a `filename` is provided, then the `Content-Type` is set based on the file extension using the `type()` method and the "filename=" parameter is added to the `Content-Disposition` header. ```javascript @@ -400,10 +400,25 @@ res.attachment('path/to/logo.png') // Content-Type: image/png ``` -### download +### download(file [, filename] [, options] [, callback]) +This transfers the `file` (either a local path, S3 file reference, or Javascript `Buffer`) as an "attachment". This is a convenience method that combines `attachment()` and `sendFile()` to prompt the user to download the file. This method optionally takes a `filename` as a second parameter that will overwrite the "filename=" parameter of the `Content-Disposition` header, otherwise it will use the filename from the `file`. An optional `options` object passes through to the [sendFile()](#sendfile) method and takes the same parameters. Finally, a optional `callback` method can be defined which is passed through to [sendFile()](#sendfile) as well. -### sendFile -The `sendFile()` method takes up to three arguments. The first is the file. This is either a local filename (stored within your uploaded lambda code), a reference to a file in S3 (using the `s3://{my-bucket}/{path-to-file}` format), or a JavaScript `Buffer`. You can optionally pass an `options` object using the properties below as well as a callback function `fn(err)` that can handle custom errors or manipulate the response before sending to the client. +```javascript +res.download('/files/sales-report.pdf') + +res.download('/files/sales-report.pdf', 'report.pdf') + +res.download('s3://my-bucket/path/to/file.png', 'logo.png', { maxAge: 3600000 }) + +res.download(, 'my-file.docx', { maxAge: 3600000 }, (err) => { + if (err) { + res.error('Custom File Error') + } +}) +``` + +### sendFile(file [, options] [, callback]) +The `sendFile()` method takes up to three arguments. The first is the `file`. This is either a local filename (stored within your uploaded lambda code), a reference to a file in S3 (using the `s3://{my-bucket}/{path-to-file}` format), or a JavaScript `Buffer`. You can optionally pass an `options` object using the properties below as well as a callback function `fn(err)` that can handle custom errors or manipulate the response before sending to the client. | Property | Type | Description | Default | | -------- | ---- | ----------- | ------- | From 591da6d472f5a42e3d8aeb37caec2b3d443b87f1 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Sun, 1 Apr 2018 08:23:40 -0400 Subject: [PATCH 30/35] add aws-sdk as dev dependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 41b0c6a..2c81b15 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "bluebird": "^3.5.1" }, "devDependencies": { + "aws-sdk": "^2.218.1", "chai": "^4.1.2", "mocha": "^4.0.1" }, From 28b9c33d4d8907dc82e8fc3c637aa984de16e6a6 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Sun, 1 Apr 2018 10:31:36 -0400 Subject: [PATCH 31/35] add s3 getObject support w/ tests --- package.json | 3 +- response.js | 49 +++++++++++++--- s3-service.js | 7 +++ test/download.js | 134 +++++++++++++++++++++++++++++++++++++++----- test/sendFile.js | 141 ++++++++++++++++++++++++++++++++++++++++------- 5 files changed, 292 insertions(+), 42 deletions(-) create mode 100644 s3-service.js diff --git a/package.json b/package.json index 2c81b15..1cfabec 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "devDependencies": { "aws-sdk": "^2.218.1", "chai": "^4.1.2", - "mocha": "^4.0.1" + "mocha": "^4.0.1", + "sinon": "^4.5.0" }, "engines": { "node": ">= 6.10.0" diff --git a/response.js b/response.js index 68d2ef2..85d2a5f 100644 --- a/response.js +++ b/response.js @@ -15,7 +15,7 @@ const fs = require('fs') // Require Node.js file system const path = require('path') // Require Node.js path const Promise = require('bluebird') // Promise library - +const AWS = require('aws-sdk') // AWS SDK (automatically available in Lambda) class RESPONSE { @@ -209,20 +209,55 @@ class RESPONSE { // Create buffer based on input if (typeof file === 'string') { - if (/^s3:\/\//i.test(file)) { - console.log('S3'); - buffer = 'empty' + + let filepath = file.trim() + + // If an S3 file identifier + if (/^s3:\/\//i.test(filepath)) { + + let s3object = filepath.replace(/^s3:\/\//i,'').split('/') + let s3bucket = s3object.shift() + let s3key = s3object.join('/') + + if (s3bucket.length > 0 && s3key.length > 0) { + + // Require AWS S3 service + const S3 = require('./s3-service') + + let params = { + Bucket: s3bucket, + Key: s3key + } + + // Attempt to get the object from S3 + return S3.getObjectAsync(params).then(data => { + buffer = data.Body + modified = data.LastModified + this.type(data.ContentType) + this.header('ETag',data.ETag) + }).catch(err => { + throw new Error(err) + }) + + } else { + throw new Error('Invalid S3 path') + } + + // else try and load the file locally } else { - buffer = fs.readFileSync((opts.root ? opts.root : '') + file) - modified = opts.lastModified !== false ? fs.statSync((opts.root ? opts.root : '') + file).mtime : undefined - this.type(path.extname(file)) + buffer = fs.readFileSync((opts.root ? opts.root : '') + filepath) + modified = opts.lastModified !== false ? fs.statSync((opts.root ? opts.root : '') + filepath).mtime : undefined + this.type(path.extname(filepath)) } + // If the input is a buffer, pass through } else if (Buffer.isBuffer(file)) { buffer = file } else { throw new Error('Invalid file') } + }).then(() => { + // Add headers from options if (typeof opts.headers === 'object') { Object.keys(opts.headers).map(header => { diff --git a/s3-service.js b/s3-service.js new file mode 100644 index 0000000..79afc13 --- /dev/null +++ b/s3-service.js @@ -0,0 +1,7 @@ +'use strict' + +const AWS = require('aws-sdk') // AWS SDK +const Promise = require('bluebird') // Promise library + +// Export +module.exports = Promise.promisifyAll(new AWS.S3()) diff --git a/test/download.js b/test/download.js index 0772942..53dccff 100644 --- a/test/download.js +++ b/test/download.js @@ -5,6 +5,12 @@ const expect = require('chai').expect // Assertion library const fs = require('fs') // Require Node.js file system +// Require Sinon.js library +const sinon = require('sinon') + +const AWS = require('aws-sdk') // AWS SDK (automatically available in Lambda) +const S3 = require('../s3-service') // Init S3 Service + // Init API instance const api = require('../index')({ version: 'v1.0', mimeTypes: { test: 'text/test' } }) @@ -43,22 +49,19 @@ api.get('/download/err', function(req,res) { api.get('/download/test', function(req,res) { res.download('test/test.txt' + (req.query.test ? req.query.test : ''), err => { - if (err) { - return Promise.try(() => { - for(let i = 0; i<40000000; i++) {} - return true - }).then((x) => { + // Return a promise + return Promise.try(() => { + for(let i = 0; i<40000000; i++) {} + return true + }).then((x) => { + if (err) { + // set custom error code and message on error res.status(501).error('Custom File Error') - // throw('test error') - }) - } else { - return Promise.try(() => { - for(let i = 0; i<40000000; i++) {} - return true - }).then((x) => { + } else { + // else set custom response code res.status(201) - }) - } + } + }) }) }) @@ -84,6 +87,46 @@ api.get('/download/all', function(req,res) { res.download('test/test.txt', 'test-file.txt', { private: true, maxAge: 3600000 }, err => { res.header('x-callback','true') }) }) +// S3 file +api.get('/download/s3', function(req,res) { + + stub.withArgs({Bucket: 'my-test-bucket', Key: 'test.txt'}).resolves({ + AcceptRanges: 'bytes', + LastModified: new Date('2018-04-01T13:32:58.000Z'), + ContentLength: 23, + ETag: '"ae771fbbba6a74eeeb77754355831713"', + ContentType: 'text/plain', + Metadata: {}, + Body: Buffer.from('Test file for sendFile\n') + }) + + res.download('s3://my-test-bucket/test.txt') +}) + +api.get('/download/s3path', function(req,res) { + + stub.withArgs({Bucket: 'my-test-bucket', Key: 'test/test.txt'}).resolves({ + AcceptRanges: 'bytes', + LastModified: new Date('2018-04-01T13:32:58.000Z'), + ContentLength: 23, + ETag: '"ae771fbbba6a74eeeb77754355831713"', + ContentType: 'text/plain', + Metadata: {}, + Body: Buffer.from('Test file for sendFile\n') + }) + + res.download('s3://my-test-bucket/test/test.txt') +}) + +api.get('/download/s3missing', function(req,res) { + + stub.withArgs({Bucket: 'my-test-bucket', Key: 'file-does-not-exist.txt'}) + .throws(new Error("NoSuchKey: The specified key does not exist.")) + + res.download('s3://my-test-bucket/file-does-not-exist.txt') +}) + + // Error Middleware api.use(function(err,req,res,next) { res.header('x-error','true') @@ -94,8 +137,15 @@ api.use(function(err,req,res,next) { /*** BEGIN TESTS ***/ /******************************************************************************/ +let stub + describe('Download Tests:', function() { + before(function() { + // Stub getObjectAsync + stub = sinon.stub(S3,'getObjectAsync') + }) + it('Bad path', function() { let _event = Object.assign({},event,{ path: '/download/badpath' }) @@ -236,4 +286,58 @@ describe('Download Tests:', function() { }) // end it -}) // end HEADER tests + it('S3 file', function() { + let _event = Object.assign({},event,{ path: '/download/s3' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'Cache-Control': 'max-age=0', + 'Content-Disposition': 'attachment; filename="test.txt"', + 'Expires': result.headers['Expires'], + 'ETag': '"ae771fbbba6a74eeeb77754355831713"', + 'Last-Modified': result.headers['Last-Modified'] + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + it('S3 file w/ nested path', function() { + let _event = Object.assign({},event,{ path: '/download/s3path' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'Cache-Control': 'max-age=0', + 'Content-Disposition': 'attachment; filename="test.txt"', + 'Expires': result.headers['Expires'], + 'ETag': '"ae771fbbba6a74eeeb77754355831713"', + 'Last-Modified': result.headers['Last-Modified'] + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + it('S3 file error', function() { + let _event = Object.assign({},event,{ path: '/download/s3missing' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'application/json', + 'x-error': 'true' + }, statusCode: 500, body: '{"error":"NoSuchKey: The specified key does not exist."}', isBase64Encoded: false }) + }) + }) // end it + + after(function() { + stub.restore() + }) + +}) // end download tests diff --git a/test/sendFile.js b/test/sendFile.js index 24db35a..1a405c2 100644 --- a/test/sendFile.js +++ b/test/sendFile.js @@ -5,6 +5,12 @@ const expect = require('chai').expect // Assertion library const fs = require('fs') // Require Node.js file system +// Require Sinon.js library +const sinon = require('sinon') + +const AWS = require('aws-sdk') // AWS SDK (automatically available in Lambda) +const S3 = require('../s3-service') // Init S3 Service + // Init API instance const api = require('../index')({ version: 'v1.0', mimeTypes: { test: 'text/test' } }) @@ -43,22 +49,19 @@ api.get('/sendfile/err', function(req,res) { api.get('/sendfile/test', function(req,res) { res.sendFile('test/test.txt' + (req.query.test ? req.query.test : ''), err => { - if (err) { - return Promise.try(() => { - for(let i = 0; i<40000000; i++) {} - return true - }).then((x) => { + // Return a promise + return Promise.try(() => { + for(let i = 0; i<40000000; i++) {} + return true + }).then((x) => { + if (err) { + // set custom error code and message on error res.status(501).error('Custom File Error') - // throw('test error') - }) - } else { - return Promise.try(() => { - for(let i = 0; i<40000000; i++) {} - return true - }).then((x) => { + } else { + // else set custom response code res.status(201) - }) - } + } + }) }) }) @@ -104,8 +107,49 @@ api.get('/sendfile/custom-cache-control', function(req,res) { }) }) +// S3 file +api.get('/sendfile/s3', function(req,res) { + + stub.withArgs({Bucket: 'my-test-bucket', Key: 'test.txt'}).resolves({ + AcceptRanges: 'bytes', + LastModified: new Date('2018-04-01T13:32:58.000Z'), + ContentLength: 23, + ETag: '"ae771fbbba6a74eeeb77754355831713"', + ContentType: 'text/plain', + Metadata: {}, + Body: Buffer.from('Test file for sendFile\n') + }) + + res.sendFile('s3://my-test-bucket/test.txt') +}) + +api.get('/sendfile/s3path', function(req,res) { + + stub.withArgs({Bucket: 'my-test-bucket', Key: 'test/test.txt'}).resolves({ + AcceptRanges: 'bytes', + LastModified: new Date('2018-04-01T13:32:58.000Z'), + ContentLength: 23, + ETag: '"ae771fbbba6a74eeeb77754355831713"', + ContentType: 'text/plain', + Metadata: {}, + Body: Buffer.from('Test file for sendFile\n') + }) + + res.sendFile('s3://my-test-bucket/test/test.txt') +}) + +api.get('/sendfile/s3missing', function(req,res) { + + stub.withArgs({Bucket: 'my-test-bucket', Key: 'file-does-not-exist.txt'}) + .throws(new Error("NoSuchKey: The specified key does not exist.")) + + res.sendFile('s3://my-test-bucket/file-does-not-exist.txt') +}) + // Error Middleware api.use(function(err,req,res,next) { + // Set x-error header to test middleware execution + res.header('x-error','true') next() }) @@ -113,15 +157,22 @@ api.use(function(err,req,res,next) { /*** BEGIN TESTS ***/ /******************************************************************************/ +let stub + describe('SendFile Tests:', function() { + before(function() { + // Stub getObjectAsync + stub = sinon.stub(S3,'getObjectAsync') + }) + it('Bad path', function() { let _event = Object.assign({},event,{ path: '/sendfile/badpath' }) return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"Invalid file"}', isBase64Encoded: false }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json','x-error': 'true' }, statusCode: 500, body: '{"error":"Invalid file"}', isBase64Encoded: false }) }) }) // end it @@ -131,7 +182,7 @@ describe('SendFile Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 500, body: '{"error":"ENOENT: no such file or directory, open \'./test-missing.txt\'"}', isBase64Encoded: false }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json','x-error': 'true' }, statusCode: 500, body: '{"error":"ENOENT: no such file or directory, open \'./test-missing.txt\'"}', isBase64Encoded: false }) }) }) // end it @@ -143,7 +194,7 @@ describe('SendFile Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 404, body: '{"error":"There was an error accessing the requested file"}', isBase64Encoded: false }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json','x-error': 'true' }, statusCode: 404, body: '{"error":"There was an error accessing the requested file"}', isBase64Encoded: false }) }) }) // end it @@ -172,7 +223,7 @@ describe('SendFile Tests:', function() { return new Promise((resolve,reject) => { api.run(_event,{},function(err,res) { resolve(res) }) }).then((result) => { - expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json' }, statusCode: 501, body: '{"error":"Custom File Error"}', isBase64Encoded: false }) + expect(result).to.deep.equal({ headers: { 'Content-Type': 'application/json','x-error': 'true' }, statusCode: 501, body: '{"error":"Custom File Error"}', isBase64Encoded: false }) }) }) // end it @@ -295,4 +346,56 @@ describe('SendFile Tests:', function() { }) // end it -}) // end HEADER tests + it('S3 file', function() { + let _event = Object.assign({},event,{ path: '/sendfile/s3' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'Cache-Control': 'max-age=0', + 'Expires': result.headers['Expires'], + 'ETag': '"ae771fbbba6a74eeeb77754355831713"', + 'Last-Modified': result.headers['Last-Modified'] + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + it('S3 file w/ nested path', function() { + let _event = Object.assign({},event,{ path: '/sendfile/s3path' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'text/plain', + 'Cache-Control': 'max-age=0', + 'Expires': result.headers['Expires'], + 'ETag': '"ae771fbbba6a74eeeb77754355831713"', + 'Last-Modified': result.headers['Last-Modified'] + }, statusCode: 200, body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) + }) + }) // end it + + it('S3 file error', function() { + let _event = Object.assign({},event,{ path: '/sendfile/s3missing' }) + + return new Promise((resolve,reject) => { + api.run(_event,{},function(err,res) { resolve(res) }) + }).then((result) => { + expect(result).to.deep.equal({ + headers: { + 'Content-Type': 'application/json', + 'x-error': 'true' + }, statusCode: 500, body: '{"error":"NoSuchKey: The specified key does not exist."}', isBase64Encoded: false }) + }) + }) // end it + + after(function() { + stub.restore() + }) + +}) // end sendFile tests From eaa36340c59f7f5f7a9497cb42e013f45cd27c9e Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Sun, 1 Apr 2018 10:32:51 -0400 Subject: [PATCH 32/35] clean up comments --- request.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/request.js b/request.js index 768fe0e..79276da 100644 --- a/request.js +++ b/request.js @@ -73,11 +73,6 @@ class REQUEST { // Extract path from event (strip querystring just in case) let path = app._event.path.trim().split('?')[0].replace(/^\/(.*?)(\/)*$/,'$1').split('/') - // // Remove base if it exists - // if (app._base && app._base === path[0]) { - // path.shift() - // } // end remove base - // Init the route this.route = null From e42739f7a5a1b743aac06bcc9a15b262e904da93 Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Sun, 1 Apr 2018 21:18:11 -0400 Subject: [PATCH 33/35] add binary support documentation --- README.md | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3b983c9..68e8f10 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,10 @@ These other frameworks are extremely powerful, but that benefit comes with the s Lambda API has **ONE** dependency. We use [Bluebird](http://bluebirdjs.com/docs/getting-started.html) promises to serialize asynchronous execution. We use promises because AWS Lambda currently only supports Node v6.10, which doesn't support `async / await`. Bluebird is faster than native promise and it has **no dependencies** either, making it the perfect choice for Lambda API. -Lambda API was written to be extremely lightweight and built specifically for serverless applications using AWS Lambda. It provides support for API routing, serving up HTML pages, issuing redirects, and much more. It has a powerful middleware and error handling system, allowing you to implement everything from custom authentication to complex logging systems. Best of all, it was designed to work with Lambda's Proxy Integration, automatically handling all the interaction with API Gateway for you. It parses **REQUESTS** and formats **RESPONSES** for you, allowing you to focus on your application's core functionality, instead of fiddling with inputs and outputs. +Lambda API was written to be extremely lightweight and built specifically for serverless applications using AWS Lambda. It provides support for API routing, serving up HTML pages, issuing redirects, serving binary files and much more. It has a powerful middleware and error handling system, allowing you to implement everything from custom authentication to complex logging systems. Best of all, it was designed to work with Lambda's Proxy Integration, automatically handling all the interaction with API Gateway for you. It parses **REQUESTS** and formats **RESPONSES** for you, allowing you to focus on your application's core functionality, instead of fiddling with inputs and outputs. + +## New in v0.4 - Binary Support! +Binary support has been added! This allows you to both send and receive binary files from API Gateway. For more information, see [Enabling Binary Support](#enabling-binary-support). ## Breaking Change in v0.3 Please note that the invocation method has been changed. You no longer need to use the `new` keyword to instantiate Lambda API. It can now be instantiated in one line: @@ -401,7 +404,7 @@ res.attachment('path/to/logo.png') ``` ### download(file [, filename] [, options] [, callback]) -This transfers the `file` (either a local path, S3 file reference, or Javascript `Buffer`) as an "attachment". This is a convenience method that combines `attachment()` and `sendFile()` to prompt the user to download the file. This method optionally takes a `filename` as a second parameter that will overwrite the "filename=" parameter of the `Content-Disposition` header, otherwise it will use the filename from the `file`. An optional `options` object passes through to the [sendFile()](#sendfile) method and takes the same parameters. Finally, a optional `callback` method can be defined which is passed through to [sendFile()](#sendfile) as well. +This transfers the `file` (either a local path, S3 file reference, or Javascript `Buffer`) as an "attachment". This is a convenience method that combines `attachment()` and `sendFile()` to prompt the user to download the file. This method optionally takes a `filename` as a second parameter that will overwrite the "filename=" parameter of the `Content-Disposition` header, otherwise it will use the filename from the `file`. An optional `options` object passes through to the [sendFile()](#sendfilefile--options--callback) method and takes the same parameters. Finally, a optional `callback` method can be defined which is passed through to [sendFile()](#sendfilefile--options--callback) as well. ```javascript res.download('/files/sales-report.pdf') @@ -418,7 +421,7 @@ res.download(, 'my-file.docx', { maxAge: 3600000 }, (err) => { ``` ### sendFile(file [, options] [, callback]) -The `sendFile()` method takes up to three arguments. The first is the `file`. This is either a local filename (stored within your uploaded lambda code), a reference to a file in S3 (using the `s3://{my-bucket}/{path-to-file}` format), or a JavaScript `Buffer`. You can optionally pass an `options` object using the properties below as well as a callback function `fn(err)` that can handle custom errors or manipulate the response before sending to the client. +The `sendFile()` method takes up to three arguments. The first is the `file`. This is either a local filename (stored within your uploaded lambda code), a reference to a file in S3 (using the `s3://{my-bucket}/{path-to-file}` format), or a JavaScript `Buffer`. You can optionally pass an `options` object using the properties below as well as a callback function `callback(err)` that can handle custom errors or manipulate the response before sending to the client. | Property | Type | Description | Default | | -------- | ---- | ----------- | ------- | @@ -429,7 +432,29 @@ The `sendFile()` method takes up to three arguments. The first is the `file`. Th | cacheControl | `Boolean` or `String` | Enable or disable setting `Cache-Control` response header. Override value with custom string. | true | | private | `Boolean` | Sets the `Cache-Control` to `private`. | false | +```javascript +res.sendFile('./img/logo.png') + +res.sendFile('./img/logo.png', { maxAge: 3600000 }) + +res.sendFile('s3://my-bucket/path/to/file.png', { maxAge: 3600000 }) + +res.sendFile(, 'my-file.docx', { maxAge: 3600000 }, (err) => { + if (err) { + res.error('Custom File Error') + } +}) +``` + +The `callback` function supports promises, allowing you to perform additional tasks *after* the file is successfully loaded from the source. This can be used to perform additional synchronous tasks before returning control to the API execution. + +See [Enabling Binary Support](#enabling-binary-support) for more information. +## Enabling Binary Support +To enable binary support, you need to add `*/*` under Binary Media Types in API Gateway > APIs > [your api] -> Settings. This will also base64 encode all body content, but Lambda API will automatically decode it for you. + +![Binary Media Types](http://jeremydaly.com//lambda-api/binary-media-types.png) +*Add *`*/*` *to Binary Media Types* ## Path Parameters Path parameters are extracted from the path sent in by API Gateway. Although API Gateway supports path parameters, the API doesn't use these values but insteads extracts them from the actual path. This gives you more flexibility with the API Gateway configuration. Path parameters are defined in routes using a colon `:` as a prefix. @@ -569,5 +594,3 @@ Contributions, ideas and bug reports are welcome and greatly appreciated. Please ## NOTES In order for "root" path mapping to work, you need to also create an `ANY` route for `/` in addition to your `{proxy+}` route. - -To enable binary support, you need to add `*/*` under Binary Media Types in API Gateway > APIs > [your api] -> Settings. This will also base64 encode all body content, but Lambda API will automatically decode it for you. From 0127fb89615c4db9bbaf15264141548e74b67f7b Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Sun, 1 Apr 2018 21:35:15 -0400 Subject: [PATCH 34/35] additional documentation and cleanup --- README.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 68e8f10..67c32c8 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ The `REQUEST` object contains a parsed and normalized request from API Gateway. - `route`: The matched route of the request - `requestContext`: The `requestContext` passed from the API Gateway - `namespace` or `ns`: A reference to modules added to the app's namespace (see [namespaces](#namespaces)) -- `cookies`: An object containing cookies sent from the browser (see the [cookie](#cookie) `RESPONSE` method) +- `cookies`: An object containing cookies sent from the browser (see the [cookie](#cookiename-value-options) `RESPONSE` method) The request object can be used to pass additional information through the processing chain. For example, if you are using a piece of authentication middleware, you can add additional keys to the `REQUEST` object with information about the user. See [middleware](#middleware) for more information. @@ -382,7 +382,7 @@ res.cookie('fooArray', [ 'one', 'two', 'three' ], { path: '/', httpOnly: true }) ``` ### clearCookie(name [,options]) -Convenience method for expiring cookies. Requires the `name` and optional `options` object as specified in the [cookie](#cookie) method. This method will automatically set the expiration time. However, most browsers require the same options to clear a cookie as was used to set it. E.g. if you set the `path` to "/admin" when you set the cookie, you must use this same value to clear it. +Convenience method for expiring cookies. Requires the `name` and optional `options` object as specified in the [cookie](#cookiename-value-options) method. This method will automatically set the expiration time. However, most browsers require the same options to clear a cookie as was used to set it. E.g. if you set the `path` to "/admin" when you set the cookie, you must use this same value to clear it. ```javascript res.clearCookie('foo', { secure: true }).send() @@ -448,13 +448,15 @@ res.sendFile(, 'my-file.docx', { maxAge: 3600000 }, (err) => { The `callback` function supports promises, allowing you to perform additional tasks *after* the file is successfully loaded from the source. This can be used to perform additional synchronous tasks before returning control to the API execution. +**NOTE:** In order to access S3 files, your Lambda function must have `GetObject` access to the files you're attempting to access. + See [Enabling Binary Support](#enabling-binary-support) for more information. ## Enabling Binary Support -To enable binary support, you need to add `*/*` under Binary Media Types in API Gateway > APIs > [your api] -> Settings. This will also base64 encode all body content, but Lambda API will automatically decode it for you. +To enable binary support, you need to add `*/*` under "Binary Media Types" in **API Gateway** -> **APIs** -> **[ your api ]** -> **Settings**. This will also `base64` encode all body content, but Lambda API will automatically decode it for you. ![Binary Media Types](http://jeremydaly.com//lambda-api/binary-media-types.png) -*Add *`*/*` *to Binary Media Types* +*Add* `*/*` *to Binary Media Types* ## Path Parameters Path parameters are extracted from the path sent in by API Gateway. Although API Gateway supports path parameters, the API doesn't use these values but insteads extracts them from the actual path. This gives you more flexibility with the API Gateway configuration. Path parameters are defined in routes using a colon `:` as a prefix. @@ -586,11 +588,7 @@ Conditional route support could be added via middleware or with conditional logi ## Configuring Routes in API Gateway Routes must be configured in API Gateway in order to support routing to the Lambda function. The easiest way to support all of your routes without recreating them is to use [API Gateway's Proxy Integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html#api-gateway-proxy-resource?icmpid=docs_apigateway_console). -Simply create one `{proxy+}` route that uses the `ANY` method and all requests will be routed to your Lambda function and processed by the `lambda-api` module. +Simply create a `{proxy+}` route that uses the `ANY` method and all requests will be routed to your Lambda function and processed by the `lambda-api` module. In order for a "root" path mapping to work, you also need to create an `ANY` route for `/`. ## Contributions Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/lambda-api/issues) for suggestions and bugs reports. - - -## NOTES -In order for "root" path mapping to work, you need to also create an `ANY` route for `/` in addition to your `{proxy+}` route. From 0516b7bfc4aadb85ecaffea55e43e8be4556ff5a Mon Sep 17 00:00:00 2001 From: Jeremy Daly Date: Sun, 1 Apr 2018 21:43:42 -0400 Subject: [PATCH 35/35] doc tweaks --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 67c32c8..4ef04b4 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ module.exports = (api, opts) => { } ``` -Even though both modules create a `/product` route, Lambda API will add the `prefix` to them creating two unique routes. Your users can now access: +Even though both modules create a `/product` route, Lambda API will add the `prefix` to them, creating two unique routes. Your users can now access: - `/v1/product` - `/v2/product` @@ -407,9 +407,9 @@ res.attachment('path/to/logo.png') This transfers the `file` (either a local path, S3 file reference, or Javascript `Buffer`) as an "attachment". This is a convenience method that combines `attachment()` and `sendFile()` to prompt the user to download the file. This method optionally takes a `filename` as a second parameter that will overwrite the "filename=" parameter of the `Content-Disposition` header, otherwise it will use the filename from the `file`. An optional `options` object passes through to the [sendFile()](#sendfilefile--options--callback) method and takes the same parameters. Finally, a optional `callback` method can be defined which is passed through to [sendFile()](#sendfilefile--options--callback) as well. ```javascript -res.download('/files/sales-report.pdf') +res.download('./files/sales-report.pdf') -res.download('/files/sales-report.pdf', 'report.pdf') +res.download('./files/sales-report.pdf', 'report.pdf') res.download('s3://my-bucket/path/to/file.png', 'logo.png', { maxAge: 3600000 }) @@ -591,4 +591,4 @@ Routes must be configured in API Gateway in order to support routing to the Lamb Simply create a `{proxy+}` route that uses the `ANY` method and all requests will be routed to your Lambda function and processed by the `lambda-api` module. In order for a "root" path mapping to work, you also need to create an `ANY` route for `/`. ## Contributions -Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/lambda-api/issues) for suggestions and bugs reports. +Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/lambda-api/issues) for suggestions and bug reports.