Skip to content

Commit

Permalink
close #22 by adding preliminary etag support
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremydaly committed May 16, 2018
1 parent c8a8c2e commit bab5ba8
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 6 deletions.
33 changes: 28 additions & 5 deletions lib/response.js
Expand Up @@ -7,10 +7,6 @@
*/ */


const UTILS = require('./utils.js') const UTILS = require('./utils.js')
// 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 fs = require('fs') // Require Node.js file system
const path = require('path') // Require Node.js path const path = require('path') // Require Node.js path
Expand All @@ -37,6 +33,9 @@ class RESPONSE {


// Default callback function // Default callback function
this._callback = 'callback' this._callback = 'callback'

// Default Etag support
this._etag = false
} }


// Sets the statusCode // Sets the statusCode
Expand Down Expand Up @@ -352,10 +351,35 @@ class RESPONSE {
} }




// Enable/Disable Etag
etag(enable) {
this._etag = enable === true ? true : false
return this
}



// Sends the request to the main callback // Sends the request to the main callback
send(body) { send(body) {


// Generate Etag
if ( this._etag // if etag support enabled
&& this.app.request // if request exists
&& ['GET','HEAD'].includes(this.app.request.method)
&& !this.hasHeader('etag')
&& this._statusCode === 200
) {
this.header('etag','"'+UTILS.generateEtag(body)+'"')
}

// Check for matching Etag
if ( this.app.request
&& this.app.request.headers['if-none-match']
&& this.app.request.headers['if-none-match'] === this.getHeader('etag')
) {
this.status(304)
body = ''
}

// Create the response // Create the response
const response = { const response = {
headers: this._headers, headers: this._headers,
Expand All @@ -370,7 +394,6 @@ class RESPONSE {
} // end send } // end send





// Trigger API error // Trigger API error
error(e) { error(e) {
this.app.catchErrors(e) this.app.catchErrors(e)
Expand Down
10 changes: 9 additions & 1 deletion lib/utils.js
Expand Up @@ -7,6 +7,7 @@
*/ */


const QS = require('querystring') // Require the querystring library const QS = require('querystring') // Require the querystring library
const crypto = require('crypto') // Require Node.js crypto library


const entityMap = { const entityMap = {
"&": "&", "&": "&",
Expand All @@ -30,9 +31,11 @@ module.exports.encodeUrl = url => String(url)






module.exports.encodeBody = body => const encodeBody = body =>
typeof body === 'object' ? JSON.stringify(body) : (body && typeof body !== 'string' ? body.toString() : (body ? body : '')) typeof body === 'object' ? JSON.stringify(body) : (body && typeof body !== 'string' ? body.toString() : (body ? body : ''))


module.exports.encodeBody = encodeBody





module.exports.parseBody = body => { module.exports.parseBody = body => {
Expand Down Expand Up @@ -98,3 +101,8 @@ const extractRoutes = (routes,table=[]) => {
} }


module.exports.extractRoutes = extractRoutes module.exports.extractRoutes = extractRoutes


// Generate an Etag for the supplied value
module.exports.generateEtag = data =>
crypto.createHash('sha256').update(encodeBody(data)).digest("hex").substr(0,32)
86 changes: 86 additions & 0 deletions test/etag.js
@@ -0,0 +1,86 @@
'use strict';

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'
}
}

/******************************************************************************/
/*** DEFINE TEST ROUTES ***/
/******************************************************************************/

api.get('/testEtag', function(req,res) {
res.etag(true).send({ test: true })
})

api.get('/testEtag2', function(req,res) {
res.etag(true).send({ test: false })
})

/******************************************************************************/
/*** BEGIN TESTS ***/
/******************************************************************************/

describe('Etag Tests:', function() {

it('Initial request', async function() {
let _event = Object.assign({},event,{ path: '/testEtag'})
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: {
'content-type': 'application/json',
'etag': '"6fd977db9b2afe87a9ceee4843288129"'
}, statusCode: 200, body: '{"test":true}', isBase64Encoded: false })
}) // end it

it('Initial request 2', async function() {
let _event = Object.assign({},event,{ path: '/testEtag2'})
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: {
'content-type': 'application/json',
'etag': '"ad2ba8d138b3cda185243603ec9fcaa7"'
}, statusCode: 200, body: '{"test":false}', isBase64Encoded: false })
}) // end it

it('Second request', async function() {
let _event = Object.assign({},event,{ path: '/testEtag', headers: { 'If-None-Match': '"6fd977db9b2afe87a9ceee4843288129"' }})
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: {
'content-type': 'application/json',
'etag': '"6fd977db9b2afe87a9ceee4843288129"'
}, statusCode: 304, body: '', isBase64Encoded: false })
}) // end it

it('Second request 2', async function() {
let _event = Object.assign({},event,{ path: '/testEtag2', headers: { 'If-None-Match': '"ad2ba8d138b3cda185243603ec9fcaa7"' }})
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: {
'content-type': 'application/json',
'etag': '"ad2ba8d138b3cda185243603ec9fcaa7"'
}, statusCode: 304, body: '', isBase64Encoded: false })
}) // end it

it('Non-matching Etags', async function() {
let _event = Object.assign({},event,{ path: '/testEtag', headers: { 'If-None-Match': '"ad2ba8d138b3cda185243603ec9fcaa7"' }})
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: {
'content-type': 'application/json',
'etag': '"6fd977db9b2afe87a9ceee4843288129"'
}, statusCode: 200, body: '{"test":true}', isBase64Encoded: false })
}) // end it




}) // end ERROR HANDLING tests
32 changes: 32 additions & 0 deletions test/utils.js
Expand Up @@ -258,4 +258,36 @@ describe('Utility Function Tests:', function() {


}) // end extractRoutes }) // end extractRoutes



describe('generateEtag:', function() {

it('Sample text', function() {
expect(utils.generateEtag('this is a test string')).to.equal('f6774519d1c7a3389ef327e9c04766b9')
}) // end it

it('Sample object', function() {
expect(utils.generateEtag({ test: true, foo: 'bar' })).to.equal('def7648849c1e7f30c9a9c0ac79e4e52')
}) // end it

it('Sample JSON string object', function() {
expect(utils.generateEtag(JSON.stringify({ test: true, foo: 'bar' }))).to.equal('def7648849c1e7f30c9a9c0ac79e4e52')
}) // end it

it('Sample buffer', function() {
expect(utils.generateEtag(Buffer.from('this is a test string as a buffer'))).to.equal('6a2f7473a72cfebc96ae8cf93d643b70')
}) // end it

it('Long string', function() {
let longString = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
expect(utils.generateEtag(longString)).to.equal('2d8c2f6d978ca21712b5f6de36c9d31f')
}) // end it

it('Long string (minor variant)', function() {
let longString = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est Laborum.'
expect(utils.generateEtag(longString)).to.equal('bc82a4065a8ab48ade900c6466b19ccd')
}) // end it

}) // end generateEtag tests


}) // end UTILITY tests }) // end UTILITY tests

0 comments on commit bab5ba8

Please sign in to comment.