UNPKG

express-keenio

Version:

Express middleware for creating events from request-responses.

998 lines (860 loc) 31.3 kB
"use strict"; var express = require('express'), request = require('supertest'), should = require('chai').should(), sinon = require('sinon'), proxyquire = require('proxyquire'); var bodyParser = require("body-parser"), multipart = require("connect-multiparty"), session = require("express-session"), cookieParser = require("cookie-parser"); var path = require('path'), fs = require('fs'); var mockKeenClientModule = { configure: function (options) { return { addEvent: function (eventCollection, event, callback) { console.log("This should never be printed."); } }; } }; describe("keenioMiddleware", function () { var keenioMiddleware; beforeEach(function () { var cache = path.resolve('./route-schemas.cache'); if (fs.existsSync(cache)) { fs.unlinkSync(cache); } keenioMiddleware = proxyquire('../lib/keenio-middleware', { "keen-js": function Keen() { return mockKeenClientModule; } }); }); it('should inherit from EventEmitter', function (done) { keenioMiddleware.on('foo', function (word) { done(); }); keenioMiddleware.emit('foo', 'word!'); }); describe("configure()", function () { it("should error if invalid configuration was passed in", function () { // Test the client configuration. var tests = [{ configuration: null, errorMessage: "No options specified for the keen.io middleware." }, { configuration: {}, errorMessage: "No client options specified for the keen.io middleware." }, { configuration: { client: { notProjectId: '<test>', writeKey: '<test>' } }, errorMessage: "projectId is missing from the client options passed into the keen.io middleware and was mandatory." }, { configuration: { client: { projectId: '<test>', notWriteKey: '<test>' } }, errorMessage: "writeKey is missing from the client options passed into the keen.io middleware and was mandatory." }, { configuration: 'here is my projectId (1) and writeKey (47)', errorMessage: "Configuration must be a valid object." }]; tests.forEach(function (test) { (function () { var configure = keenioMiddleware.configure.bind(keenioMiddleware); configure(test.configuration); }).should.throw(Error, test.errorMessage); }); }); it("should error if both routes and excludeRoutes are passed in", function () { var configuration = { client: { projectId: '<test>', writeKey: '<test>' }, excludeRoutes: [], routes: [] }; (function () { var configure = keenioMiddleware.configure.bind(keenioMiddleware); configure(configuration); }).should.throw(Error, "You must only specify routes or excludeRoutes, never both."); }); it("should not error if only routes or excludeRoutes was passed in", function () { var tests = [{ client: { projectId: '<test>', writeKey: '<test>' }, routes: [] }, { client: { projectId: '<test>', writeKey: '<test>' }, excludeRoutes: [] }]; tests.forEach(function (configuration) { (function () { var configure = keenioMiddleware.configure.bind(keenioMiddleware); configure(configuration); }).should.not.throw(Error, "You must only specify routes or excludeRoutes, never both."); }); }); it("should return a valid middleware if trackRoute() is executed after valid configuration", function () { var configuration = { client: { projectId: '<test>', writeKey: '<test>' } }, afterConfiguration = { client: { projectId: '<test>', writeKey: '<test>' }, handlers: {}, whitelistProperties: {}, blacklistProperties: [], httpErrorsRecorded: false, defaults: { MAX_PROPERTY_HIERARCHY_DEPTH: 10, MAX_STRING_LENGTH: 1000, MAX_PROPERTY_QUANTITY: 300, addons: { ipToGeo: false, userAgentParser: false }, eventualSchemas: { cache: true, cachePath: './route-schemas.cache', query: { MAX_PROPERTIES: 30, NUMBER_OF_INSTANCES: 500, NUMBER_OF_DAYS: 7 }, body: { MAX_PROPERTIES: 80, NUMBER_OF_INSTANCES: 500, NUMBER_OF_DAYS: 7 }, reaction: { MAX_PROPERTIES: 120, NUMBER_OF_INSTANCES: 500, NUMBER_OF_DAYS: 7 } } } }; var configure = keenioMiddleware.configure.bind(keenioMiddleware); (function () { configure(configuration); }).should.not.throw(Error); keenioMiddleware.options.should.eql(afterConfiguration); var handle = keenioMiddleware.trackRoute.bind(keenioMiddleware); handle.should.not.throw(Error, 'express-keenio middleware must be configured before use. Please call ' + 'keenio-middleware#configure(options).'); }); it("should return a valid middleware if handleAll() is executed after valid configuration", function () { var configuration = { client: { projectId: '<test>', writeKey: '<test>' } }, afterConfiguration = { client: { projectId: '<test>', writeKey: '<test>' }, handlers: {}, whitelistProperties: {}, blacklistProperties: [], httpErrorsRecorded: false, defaults: { MAX_PROPERTY_HIERARCHY_DEPTH: 10, MAX_STRING_LENGTH: 1000, MAX_PROPERTY_QUANTITY: 300, addons: { ipToGeo: false, userAgentParser: false }, eventualSchemas: { cache: true, cachePath: './route-schemas.cache', query: { MAX_PROPERTIES: 30, NUMBER_OF_INSTANCES: 500, NUMBER_OF_DAYS: 7 }, body: { MAX_PROPERTIES: 80, NUMBER_OF_INSTANCES: 500, NUMBER_OF_DAYS: 7 }, reaction: { MAX_PROPERTIES: 120, NUMBER_OF_INSTANCES: 500, NUMBER_OF_DAYS: 7 } } } }; var configure = keenioMiddleware.configure.bind(keenioMiddleware); (function () { configure(configuration); }).should.not.throw(Error); keenioMiddleware.options.should.eql(afterConfiguration); var handleAll = keenioMiddleware.handleAll.bind(keenioMiddleware); handleAll.should.not.throw(Error, 'express-keenio middleware must be configured before use. Please call ' + 'keenio-middleware#configure(options).'); }); it("should error if handle() is executed before calling configure()", function () { var handle = keenioMiddleware.handle.bind(keenioMiddleware); handle.should.throw(Error, 'express-keenio middleware must be configured before use. Please call ' + 'keenio-middleware#configure(options).'); }); it("should error if handleAll() is executed before calling configure()", function () { var handleAll = keenioMiddleware.handleAll.bind(keenioMiddleware); handleAll.should.throw(Error, 'express-keenio middleware must be configured before use. Please call ' + 'keenio-middleware#configure(options).'); }); }); describe("handle() - default routes", function () { var app; beforeEach(function () { keenioMiddleware.configure({ client: { projectId: "<fake-project-id>", writeKey: "<fake-write-key>" }, excludeRoutes: [{ route: '/disabled-insecure-route', method: 'GET' }] }); app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded()); // note: these two replace: app.use(express.bodyParser()); app.use(multipart()); // see: http://stackoverflow.com/questions/19581146/how-to-get-rid-of-connect-3-0-deprecation-alert app.use(cookieParser('S3CRE7')); app.use(session({ store: new session.MemoryStore, secret: 'S3CRE7', key: 'sid' })); app.use(keenioMiddleware.handleAll()); app.post('/test', function (req, res) { var requestBody = req.body; res.send(requestBody); }); app.post('/params/:userId/:someParam/:someOtherParam', function (req, res) { var requestBody = req.body; res.send(requestBody); }); app.get('/disabled-insecure-route', function (req, res) { var requestBody = req.body; res.send(requestBody); }); }); it("should send valid event data to keen.io on making a json body request", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/test') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs, eventCollection, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } eventCollection = callArgs[0]; event = callArgs[1]; eventCollection.should.equal("post-test"); event.should.have.property('intention'); event.intention.should.eql({ method: 'POST', path: '/test', query: {}, body: { user: "seb" }, params: {} }); event.reaction.should.eql({ user: "seb" }); done(); }); }); it("should send valid event data to keen.io on making a params request", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/params/5/7/8') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs, eventCollection, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } eventCollection = callArgs[0]; event = callArgs[1]; eventCollection.should.equal("post-params-userId-someParam-someOtherParam"); event.should.have.property('intention'); event.intention.should.eql({ method: 'POST', path: '/params/5/7/8', body: { user: "seb" }, query: {}, params: { userId: '5', someParam: '7', someOtherParam: '8' } }); event.should.have.property("reaction"); event.reaction.should.eql({ user: "seb" }); done(); }); }); it("should send valid event data to keen.io on making a query request", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/test?someArgument=2&anotherArgument=4') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs, eventCollection, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } eventCollection = callArgs[0]; event = callArgs[1]; eventCollection.should.equal("post-test"); event.should.have.property('intention'); event.intention.should.eql({ method: 'POST', path: '/test', query: { someArgument: '2', anotherArgument: '4' }, body: { user: "seb" }, params: {} }); event.should.have.property("reaction"); event.reaction.should.eql({ user: "seb" }); done(); }); }); it("should track a user if they could be identified from a request", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/test') .set('Referer', 'http://www.google.co.uk') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } var event = callArgs[1]; event.intention.referer.should.eql('http://www.google.co.uk'); should.exist(event.identity.session); done(); }); }); it("should send a reaction to Keen.IO if application/json is specified as the response", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/test') .send({ "value": "json" }) .expect('{\n "value": "json"\n}', function () { var callArgs, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } event = callArgs[1]; event.intention.body.should.eql({ "value": "json" }); done(); }); }); it("should send a reaction to Keen.IO if application/x-www-form-urlencoded is specified as the response", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/test') .type('form') .send('value=urlencoded') .expect('{\n "value": "urlencoded"\n}', function () { var callArgs, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } event = callArgs[1]; event.intention.body.should.eql({ "value": "urlencoded" }); done(); }); }); it("should send empty identity data to keen.io by default", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/test') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } event = callArgs[1]; should.exist(event.identity.session); done(); }); }); it("should ignore events that are explicitly denied in the configuration", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).get('/disabled-insecure-route') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { testRequest.calledOnce.should.be.false; done(); }); }); }); describe("handle() - preconfigured routes + altered handlers", function () { var app; beforeEach(function () { keenioMiddleware.configure({ client: { projectId: "<fake-project-id>", writeKey: "<fake-write-key>" }, handlers: { generateIdentity: function (req) { return { test: "lies all lies - see later test" } } }, routes: [{ route: '/params/:userId/:someParam/:someOtherParam', method: 'POST', eventCollectionName: "testEventCollectionName" }, { route: '/test', method: 'GET', tag: "testTagName" }] }); app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded()); // note: these two replace: app.use(express.bodyParser()); // see: http://stackoverflow.com/questions/19581146/how-to-get-rid-of-connect-3-0-deprecation-alert app.use(keenioMiddleware.handleAll()); app.get('/test', function (req, res) { var requestBody = req.body; res.send(requestBody); }); app.post('/test', function (req, res) { var requestBody = req.body; res.send(requestBody); }); app.post('/params/:userId/:someParam/:someOtherParam', function (req, res) { var requestBody = req.body; res.send(requestBody); }); }); it("should no longer pick up events from routes not defined in the routes configuration", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/test') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { testRequest.calledOnce.should.be.false; done(); }); }); it("should allow you to set the eventCollectionName for a route from the configuration", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/params/5/6/7') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs, eventCollectionName; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } eventCollectionName = callArgs[0]; eventCollectionName.should.equal("testEventCollectionName"); done(); }); }); it("should allow you to tag the event for a route from the configuration", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).get('/test') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } event = callArgs[1]; event.tag.should.equal("testTagName"); done(); }); }); it("should send specific identity data to keen.io if the configuration mandated this", function (done) { var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).get('/test') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } event = callArgs[1]; event.identity.should.eql({ "test": "lies all lies - see later test" }); done(); }); }); }); describe("trackRoute(eventCollection, whitelistProperties, eventTag) - specific route", function () { var app; beforeEach(function () { keenioMiddleware.configure({ client: { projectId: "<fake-project-id>", writeKey: "<fake-write-key>" } }); app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded()); // note: these two replace: app.use(express.bodyParser()); // see: http://stackoverflow.com/questions/19581146/how-to-get-rid-of-connect-3-0-deprecation-alert }); it("should send valid event data to keen.io on making a json body request", function (done) { app.post('/test', keenioMiddleware.trackRoute('eventCollectionName', {}, "Posted to test"), function (req, res) { var requestBody = req.body; res.send(requestBody); }); var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/test') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs, eventCollection, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } eventCollection = callArgs[0]; event = callArgs[1]; eventCollection.should.equal("eventCollectionName"); event.tag.should.equal("Posted to test"); event.should.have.property('intention'); event.intention.should.eql({ method: 'POST', path: '/test', query: {}, body: { user: "seb" }, params: {} }); event.should.have.property("reaction"); event.reaction.should.eql({ user: "seb" }); done(); }); }); it("should send valid event data to keen.io on making a params request", function (done) { app.post('/params/:userId/:someParam/:someOtherParam', keenioMiddleware.trackRoute(), function (req, res) { var requestBody = req.body; res.send(requestBody); }); var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/params/5/7/8') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs, eventCollection, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } eventCollection = callArgs[0]; event = callArgs[1]; eventCollection.should.equal("post-params-userId-someParam-someOtherParam"); event.should.have.property('intention'); event.intention.should.eql({ method: 'POST', path: '/params/5/7/8', query: {}, body: { user: "seb" }, params: { userId: '5', someParam: '7', someOtherParam: '8' } }); event.should.have.property("reaction"); event.reaction.should.eql({ user: "seb" }); done(); }); }); it("should send valid event data to keen.io on making a query request", function (done) { app.get('/test', keenioMiddleware.trackRoute(), function (req, res) { var requestBody = req.body; res.send(requestBody); }); var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).get('/test?someArgument=2&anotherArgument=4') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs, eventCollection, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } eventCollection = callArgs[0]; event = callArgs[1]; eventCollection.should.equal("get-test"); event.should.have.property('intention'); event.intention.should.eql({ method: 'GET', path: '/test', query: { someArgument: '2', anotherArgument: '4' }, body: { user: "seb" }, params: {} }); event.should.have.property("reaction"); event.reaction.should.eql({ user: "seb" }); done(); }); }); it("should not track a user if they could not be identified from a request", function (done) { app.get('/test', keenioMiddleware.trackRoute(), function (req, res) { var requestBody = req.body; res.send(requestBody); }); var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).get('/test') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } var event = callArgs[1]; delete event.identity.ipAddress; event.identity.should.eql({ }); done(); }); }); it("should send a reaction to Keen.IO if application/json is specified as the response", function (done) { app.get('/test', keenioMiddleware.trackRoute(), function (req, res) { var requestBody = req.body; res.send(requestBody); }); var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).get('/test') .send({ "value": "json" }) .expect('{\n "value": "json"\n}', function () { var callArgs, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } event = callArgs[1]; event.intention.body.should.eql({ "value": "json" }); done(); }); }); it("should send a reaction to Keen.IO if application/x-www-form-urlencoded is specified as the response", function (done) { app.get('/test', keenioMiddleware.trackRoute(), function (req, res) { var requestBody = req.body; res.send(requestBody); }); var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).get('/test') .type('form') .send('value=urlencoded') .expect('{\n "value": "urlencoded"\n}', function () { var callArgs, event; callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } event = callArgs[1]; event.intention.body.should.eql({ "value": "urlencoded" }); done(); }); }); it("should allow you to set the eventCollectionName with the first argument", function (done) { app.post('/test', keenioMiddleware.trackRoute('eventCollectionName'), function (req, res) { var requestBody = req.body; res.send(requestBody); }); var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/test') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } var eventCollection = callArgs[0]; eventCollection.should.equal("eventCollectionName"); done(); }); }); it("should allow you to tag the event with the second argument", function (done) { app.post('/test', keenioMiddleware.trackRoute(null, {}, "Event tag"), function (req, res) { var requestBody = req.body; res.send(requestBody); }); var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/test') .send({ "user": "seb" }) .expect('{\n "user": "seb"\n}', function () { var callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } var event = callArgs[1]; event.tag.should.equal("Event tag"); done(); }); }); }); describe("isAcceptableStatusCode()", function () { it('should not allow a 5xx status code', function () { keenioMiddleware.isAcceptableStatusCode(500).should.be.false; }); it('should allow 401, 402, and 404 status codes', function () { keenioMiddleware.isAcceptableStatusCode(401).should.be.true; keenioMiddleware.isAcceptableStatusCode(402).should.be.true; keenioMiddleware.isAcceptableStatusCode(404).should.be.true; }); it('should not allow most other 4xx status code', function () { keenioMiddleware.isAcceptableStatusCode(400).should.be.false; keenioMiddleware.isAcceptableStatusCode(403).should.be.false; keenioMiddleware.isAcceptableStatusCode(411).should.be.false; }); }); describe("When blacklisting HTTP referer", function () { var app; beforeEach(function () { keenioMiddleware.configure({ client: { projectId: "<fake-project-id>", writeKey: "<fake-write-key>" }, blacklistProperties: ['referer'] }); app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded()); // note: these two replace: app.use(express.bodyParser()); // see: http://stackoverflow.com/questions/19581146/how-to-get-rid-of-connect-3-0-deprecation-alert }); it("should remove the referer from the event that is created", function (done) { app.post('/test', keenioMiddleware.trackRoute('eventCollectionName', {}, "Posted to test"), function (req, res) { var requestBody = req.body; res.send(requestBody); }); var testRequest = sinon.spy(); keenioMiddleware.keenClient.addEvent = testRequest; request(app).post('/test') .send({ "user": "seb" }) .set('Referer', 'http://www.google.co.uk') .expect('{\n "user": "seb"\n}', function () { var callArgs = testRequest.called ? testRequest.getCall(0).args : null; if (!callArgs) { should.Throw("No request was ever made to Keen.IO"); } var event = callArgs[1]; should.not.exist(event.intention.referer); done(); }); }); }); });