Permalink
Browse files

close #22 by adding preliminary etag support

  • Loading branch information...
jeremydaly committed May 16, 2018
1 parent c8a8c2e commit bab5ba8a98ab06609dbeca41d831045dbb18f7cc
Showing with 155 additions and 6 deletions.
  1. +28 −5 lib/response.js
  2. +9 −1 lib/utils.js
  3. +86 −0 test/etag.js
  4. +32 −0 test/utils.js
@@ -7,10 +7,6 @@
*/

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 path = require('path') // Require Node.js path
@@ -37,6 +33,9 @@ class RESPONSE {

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

// Default Etag support
this._etag = false
}

// Sets the statusCode
@@ -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
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
const response = {
headers: this._headers,
@@ -370,7 +394,6 @@ class RESPONSE {
} // end send



// Trigger API error
error(e) {
this.app.catchErrors(e)
@@ -7,6 +7,7 @@
*/

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

const entityMap = {
"&": "&",
@@ -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 : ''))

module.exports.encodeBody = encodeBody



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

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)
@@ -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
@@ -258,4 +258,36 @@ describe('Utility Function Tests:', function() {

}) // 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

0 comments on commit bab5ba8

Please sign in to comment.