diff --git a/lib/cacheEntryRepository.js b/lib/cacheEntryRepository.js index 4d698cd..ea524a5 100644 --- a/lib/cacheEntryRepository.js +++ b/lib/cacheEntryRepository.js @@ -1,4 +1,6 @@ 'use strict'; +var _ = require('lodash'); +var $crypto = require('crypto'); var $utils = require('./utils'); module.exports = CacheEntries; @@ -18,25 +20,54 @@ function CacheEntries (Nocca) { // --- Simple recording playback function considerRecordingRequest(reqContext) { - addSingleRecording(reqContext.endpoint.key, reqContext.requestKey, reqContext.getProxyResponse()); + addSingleRecording(reqContext.endpoint.key, reqContext.requestKey, reqContext.getProxyResponse().dump()); + + // Always accepts recordings + return true; } + + function hashRequestKey(requestKey) { return $crypto.createHash('sha256').update(requestKey).digest('hex'); } - function addSingleRecording (endpoint, requestKey, recordedResponse) { + function addSingleRecording (endpointKey, requestKey, recordedResponse) { - // ensure presence of endpoint - recordings[endpoint] = recordings[endpoint] || {}; + // ensure presence of endpointKey + recordings[endpointKey] = recordings[endpointKey] || {}; - recordings[endpoint][requestKey] = recordedResponse; + var generatedHash = hashRequestKey(requestKey); + + recordings[endpointKey][generatedHash] = { + hash: generatedHash, + requestKey: requestKey, + playbackResponse: recordedResponse + }; + + return generatedHash; + } + + function removeSingleRecording(endpointKey, requestKey) { + + removeSingleRecordingByHash(endpointKey, hashRequestKey(requestKey)); + + } + + function removeSingleRecordingByHash(endpointKey, requestKeyHash) { + + if (recordings.hasOwnProperty(endpointKey)) { + + delete recordings[endpointKey][requestKeyHash]; + + } } function simpleRequestKeyRequestMatcher (reqContext) { - var response = null; + var playbackResponse = null; + var requestKeyHash = hashRequestKey(reqContext.requestKey); if (typeof recordings[reqContext.endpoint.key] !== 'undefined' && - typeof recordings[reqContext.endpoint.key][reqContext.requestKey] !== 'undefined') { + typeof recordings[reqContext.endpoint.key][requestKeyHash] !== 'undefined') { - response = recordings[reqContext.endpoint.key][reqContext.requestKey]; + playbackResponse = recordings[reqContext.endpoint.key][requestKeyHash].playbackResponse; Nocca.logInfo('Found a matching record using simpleMatcher!'); @@ -46,7 +77,7 @@ function CacheEntries (Nocca) { Nocca.logDebug('No matching record found using simpleMatcher'); } - return response; + return playbackResponse; } @@ -61,18 +92,45 @@ function CacheEntries (Nocca) { function initRestRoutes() { - Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['GET:/caches', getCaches]); - Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['PUT:/caches/memory-caches/:endpoint/:entryHash', true, saveOrReplaceCacheEntry]); + Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['GET:/repositories/memory-caches/endpoints/', getCachesForAllEndpoints]); + Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['GET:/repositories/memory-caches/endpoints/:endpointKey/caches/', true, getCacheEntriesForEndpoint]); + Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['GET:/repositories/memory-caches/endpoints/:endpointKey/caches/:requestKeyHash', true, getCacheEntryByKey]); + Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['PUT:/repositories/memory-caches/endpoints/:endpointKey/caches/:requestKeyHash', true, saveOrReplaceCacheEntry]); + Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['POST:/repositories/memory-caches/endpoints/:endpointKey/caches/', true, saveOrReplaceCacheEntry]); + Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['DELETE:/repositories/memory-caches/endpoints/:endpointKey/caches/:requestKeyHash', true, deleteCacheEntry]); Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['POST:/caches/package', addCachePackage]); } - function getCaches (req, res, config, matches, writeHead, writeEnd) { - writeHead(res, 200).writeEnd(JSON.stringify(exportRecordings(), null, 4)); + function getCachesForAllEndpoints (apiReq) { + apiReq.ok().end(JSON.stringify(recordings)); + } + + function getCacheEntriesForEndpoint (apiReq) { + + if (typeof recordings[apiReq.matches.endpointKey] !== 'undefined') { + + apiReq.ok().end(JSON.stringify(recordings[apiReq.matches.endpointKey])); + } + else { + apiReq.notFound().end(); + } } - function addCachePackage (req, res, config, matches, writeHead, writeEnd) { - $utils.readBody(req).then(function(body) { + function getCacheEntryByKey (apiReq) { + + if (typeof recordings[apiReq.matches.endpointKey] !== 'undefined' && + typeof recordings[apiReq.matches.endpointKey][apiReq.matches.requestKeyHash] !== 'undefined') { + + apiReq.ok().end(JSON.stringify(recordings[apiReq.matches.endpointKey][apiReq.matches.requestKeyHash])); + } + else { + apiReq.notFound().end(); + } + } + + function addCachePackage (apiReq) { + $utils.readBody(apiReq.req).then(function(body) { body = JSON.parse(body); @@ -91,31 +149,58 @@ function CacheEntries (Nocca) { downloadObj = recordings; } - writeHead(res, 200, { + apiReq.ok({ 'Content-Type': 'application/json' - }).writeEnd(JSON.stringify(downloadObj)); + }).end(JSON.stringify(downloadObj)); }).fail(function() { - writeHead(res, 400).writeEnd('Request body could not be parsed, is it a valid JSON string?'); + apiReq.badRequest().end('Request body could not be parsed, is it a valid JSON string?'); }); } - function saveOrReplaceCacheEntry(req, res, config, matches, writeHead, writeEnd) { - $utils.readBody(req).then(function(body) { + function saveOrReplaceCacheEntry(apiReq) { + $utils.readBody(apiReq.req).then(function(body) { body = JSON.parse(body); + if (!_.has(body, 'requestKey') || !_.has(body, 'playbackResponse')) { + apiReq.badRequest().end('Object requires at least a requestKey and response property'); + return; + } + var newHash = addSingleRecording(apiReq.matches.endpointKey, body.requestKey, body.playbackResponse); + + if (typeof apiReq.matches.requestKeyHash !== 'undefined' && newHash !== apiReq.matches.requestKeyHash) { + // Request key changed, delete old entry + removeSingleRecordingByHash(apiReq.matches.endpointKey, apiReq.matches.requestKeyHash); + } + + apiReq.ok('Ok', { 'Location': '/repositories/memory-caches/endpoints/' + apiReq.matches.endpointKey + '/caches/' + newHash }).end(); - }).fail(function() { + }).fail(function(err) { - writeHead(res, 400).writeEnd('Request body could not be parsed, is it a valid JSON string?'); + apiReq.badRequest().end('Request body could not be parsed, is it a valid JSON string?'); }); } + function deleteCacheEntry(apiReq) { + + if (typeof recordings[apiReq.matches.endpointKey] !== 'undefined' && + typeof recordings[apiReq.matches.endpointKey][apiReq.matches.requestKeyHash] !== 'undefined') { + + var entryToDelete = recordings[apiReq.matches.endpointKey][apiReq.matches.requestKeyHash]; + delete recordings[apiReq.matches.endpointKey][apiReq.matches.requestKeyHash]; + + apiReq.ok().end(JSON.stringify(entryToDelete)); + } + else { + apiReq.notFound().end(); + } + + } diff --git a/lib/httpApi.js b/lib/httpApi.js index c0e0e98..55fdb65 100644 --- a/lib/httpApi.js +++ b/lib/httpApi.js @@ -62,7 +62,7 @@ function HttpApi (Nocca) { Nocca.logDebug('Checking route: ' + route); if (routes.direct.hasOwnProperty(route)) { - routes.direct[route](req, res, config, null, _writeHead, _writeEnd); + invokeRoute(routes.direct[route], req, res);//(req, res, config, null, _writeHead, _writeEnd); } else { var match, handler; @@ -73,7 +73,7 @@ function HttpApi (Nocca) { } if (typeof handler !== 'undefined') { - handler(req, res, config, match, _writeHead, _writeEnd); + invokeRoute(handler, req, res, match);//, config, match, _writeHead, _writeEnd); } else { res.writeHead(404, 'Not found', { @@ -88,6 +88,73 @@ function HttpApi (Nocca) { } } + + function invokeRoute(handler, req, res, matches) { + handler(new ApiRequest(req, res, matches)); + } + + function ApiRequest(req, res, matches) { + this.req = req; + this.res = res; + this.matches = matches; + this.headWritten = false; + } + + ApiRequest.prototype.nocca = function() { return Nocca; }; + ApiRequest.prototype.writeHead = function (statusCode, statusMessage, headers) { + + var writeHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET,PUT,DELETE', + 'Access-Control-Allow-Headers': 'content-type' + }; + + statusCode = statusCode || 200; + statusMessage = statusMessage || $http.STATUS_CODES[statusCode]; + + if (typeof headers === 'undefined' && + typeof statusMessage === 'object') { + + headers = statusMessage; + statusMessage = $http.STATUS_CODES[statusCode]; + + } + + if (typeof headers === 'object') { + // add headers for cross domain access + writeHeaders = $extend(true, {}, writeHeaders, headers); + } + + this.res.writeHead(statusCode, statusMessage, writeHeaders); + this.headWritten = true; + + var self = this; + return { + end: function (data) { + self.end(data); + } + }; + }; + ApiRequest.prototype.end = function (data) { + + if (typeof data !== 'undefined') { + // Hmm... I want ES6 + var self = this; + this.res.write(data, function () { + self.res.end(); + }); + } + else { + this.res.end(); + } + + }; + ApiRequest.prototype.ok = function(message, headers) { return this.writeHead(200, message, headers); }; + ApiRequest.prototype.moved = function(message, headers) { return this.writeHead(301, message, headers); }; + ApiRequest.prototype.badRequest = function(message, headers) { return this.writeHead(400, message, headers); }; + ApiRequest.prototype.notFound = function(message, headers) { return this.writeHead(404, message, headers); }; + ApiRequest.prototype.conflict = function(message, headers) { return this.writeHead(409, message, headers); }; + ApiRequest.prototype.internalError = function(message, headers) { return this.writeHead(500, message, headers); }; // --- Route definitions @@ -122,63 +189,19 @@ function HttpApi (Nocca) { } - function getConfig (req, res, config) { - _writeHead(res, 200).writeEnd(JSON.stringify(config, null, 4)); + function getConfig (apiReq) { + apiReq.ok().end(JSON.stringify(apiReq.nocca().config, null, 4)); } - function getEnumScenariosType (req, res) { - _writeHead(res).writeEnd(JSON.stringify($scenario.TYPE)); + function getEnumScenariosType (apiReq) { + apiReq.ok().end(JSON.stringify(Nocca.scenario.TYPE)); } - function getEnumScenariosRepeatable (req, res) { - _writeHead(res).writeEnd(JSON.stringify($scenario.REPEATABLE)); + function getEnumScenariosRepeatable (apiReq) { + apiReq.ok().end(JSON.stringify(Nocca.scenario.REPEATABLE)); } - function _writeHead (res, statusCode, statusMessage, headers) { - - var writeHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET,PUT,DELETE', - 'Access-Control-Allow-Headers': 'content-type' - }; - - statusCode = statusCode || 200; - statusMessage = statusMessage || $http.STATUS_CODES[statusCode]; - - if (typeof headers === 'undefined' && - typeof statusMessage === 'object') { - - headers = statusMessage; - statusMessage = $http.STATUS_CODES[statusCode]; - - } - if (typeof headers === 'object') { - // add headers for cross domain access - writeHeaders = $extend(true, {}, writeHeaders, headers); - } - - res.writeHead(statusCode, statusMessage, writeHeaders); - - return { - writeEnd: function (data) { - _writeEnd(res, data); - } - }; - - } - - function _writeEnd (res, data) { - - if (typeof data !== 'undefined') { - res.write(data, function () { - res.end(); - }); - } - else { - res.end(); - } - - } + } diff --git a/lib/recorder.js b/lib/recorder.js index f04c486..8b1447c 100644 --- a/lib/recorder.js +++ b/lib/recorder.js @@ -15,7 +15,7 @@ function Recorder (Nocca) { if (reqContext.getProxyResponse()) { - var acceptedRepository = _.find(Nocca.repositories, function(repository) { repository.considerRecording(reqContext); }); + var acceptedRepository = _.find(Nocca.repositories, function(repository) { return repository.considerRecording(reqContext); }); reqContext.flagRecorded = typeof acceptedRepository !== 'undefined'; diff --git a/lib/scenarioRecorder.js b/lib/scenarioRecorder.js index 0b64f63..c01b40f 100644 --- a/lib/scenarioRecorder.js +++ b/lib/scenarioRecorder.js @@ -20,6 +20,7 @@ function ScenarioRecorder(Nocca, Repository) { var activeScenarioBuilder; function considerRecordingRequest(reqContext) { + var recordedResponse = false; if (typeof activeScenarioBuilder === 'undefined') { Nocca.logDebug('Currently not recording a scenario, not recording this request here'); @@ -36,8 +37,10 @@ function ScenarioRecorder(Nocca, Repository) { endpointKey: reqContext.endpoint.key, playbackResponse: reqContext.getProxyResponse().dump() }); + recordedResponse = true; } + return recordedResponse; } function isRecording () { @@ -108,80 +111,80 @@ function ScenarioRecorder(Nocca, Repository) { } - function allowRecorderCall (req, res, config, match, writeHead, writeEnd) { + function allowRecorderCall (apiReq) { - writeHead(res, 200, { + apiReq.ok({ 'Allow': 'GET,PUT' - }).writeEnd(); + }).end(); } - function getRecorder (req, res, config, match, writeHead, writeEnd) { + function getRecorder (apiReq) { var recordingState = isRecording(); if (recordingState !== false) { - writeHead(res, 200, { + apiReq.ok({ 'Content-Type': 'application/json' - }).writeEnd(JSON.stringify(recordingState)); + }).end(JSON.stringify(recordingState)); } else { - writeHead(res, 404).writeEnd(); + apiReq.notFound().end(); } } - function changeRecorderState (req, res, config, match, writeHead, writeEnd) { + function changeRecorderState (apiReq) { - $utils.readBody(req) + $utils.readBody(apiReq.req) .then(function (body) { body = JSON.parse(body); if (body.startRecording === true) { - doStartRecordingScenario(req, res, body, writeHead, writeEnd); + doStartRecordingScenario(apiReq, body); } else if (body.stopRecording === true) { - doStopRecordingScenario(req, res, body, writeHead, writeEnd); + doStopRecordingScenario(apiReq, body); } else { - writeHead(res, 400).writeEnd('Bad body content'); + apiReq.badRequest('Bad body content').end(); } }); } - function doStartRecordingScenario (req, res, body, writeHead, writeEnd) { + function doStartRecordingScenario (apiReq, body) { try { var builder = startRecordingScenario(body); - writeHead(res, 200, { + apiReq.ok({ 'Content-Type': 'application/json', 'Location': '/scenarios/' + body.name - }).writeEnd(JSON.stringify(builder.scenario)); + }).end(JSON.stringify(builder.scenario)); } catch (e) { switch (e.id) { case Nocca.constants.ERRORS.ALREADY_RECORDING: - writeHead(res, 409, 'Already Recording'); + apiReq.conflict('Already Recording'); break; case Nocca.constants.ERRORS.INCOMPLETE_OBJECT: - writeHead(res, 400, 'Supply a name and title for the new scenario'); + apiReq.badRequest('Supply a name and title for the new scenario'); break; default: - writeHead(res, 500); + apiReq.internalError(); } - writeEnd(res, e.message); + apiReq.end(e.message); } } - function doStopRecordingScenario (req, res, body, writeHead, writeEnd) { + function doStopRecordingScenario (apiReq) { try { var scriptOutputDir = undefined; if (Nocca.config.scenarios.writeNewScenarios && Nocca.config.scenarios.scenarioOutputDir) { @@ -190,30 +193,30 @@ function ScenarioRecorder(Nocca, Repository) { var scenario = finishRecordingScenario(scriptOutputDir); - writeHead(res, 200, { + apiReq.ok({ 'Content-Type': 'application/json' - }).writeEnd(JSON.stringify(scenario)); + }).end(JSON.stringify(scenario)); } catch (e) { Nocca.logError(e); - writeHead(res, 409, 'Finish Recording Failed').writeEnd(e.message); + apiReq.conflict('Finish Recording Failed').end(e.message); } } - function cancelRecordingScenario (req, res, config, match, writeHead, writeEnd) { + function cancelRecordingScenario (apiReq) { try { var scriptOutputDir = undefined; var scenario = finishRecordingScenario(scriptOutputDir); - writeHead(res, 200, { + apiReq.ok({ 'Content-Type': 'application/json' - }).writeEnd(JSON.stringify(scenario)); + }).end(JSON.stringify(scenario)); } catch (e) { - writeHead(res, 409, 'Finish Recording Failed').writeEnd(e.message); + apiReq.conflict('Finish Recording Failed').end(e.message); } } diff --git a/lib/scenarioRepository.js b/lib/scenarioRepository.js index 90c8cd5..8fd5a8f 100644 --- a/lib/scenarioRepository.js +++ b/lib/scenarioRepository.js @@ -1,7 +1,9 @@ 'use strict'; +var _ = require('lodash'); // This repository explicitly depends on the internal scenarioRecorder (other repositories may depend on other recorders) var $scenarioRecorder = require('./scenarioRecorder'); +var $utils = require('./utils'); module.exports = ScenarioRepository; @@ -39,7 +41,7 @@ function ScenarioRepository (Nocca) { function considerRecordingRequest(reqContext) { // TODO: Add consideration logic :) - scenarioRecorder.considerRecording(reqContext); + return scenarioRecorder.considerRecording(reqContext); } @@ -85,7 +87,7 @@ function ScenarioRepository (Nocca) { registerScenarioOnEndpoint(scenarioPlayer, scenarioPlayer.currentPosition.state.endpointKey); } - + function deregisterScenarioFromEndpoint(scenarioPlayer, endpoint) { delete scenarioEndpointBindings[endpoint][scenarioPlayer.scenario.name]; scenarioPlayer.$$active = false; @@ -128,44 +130,80 @@ function ScenarioRepository (Nocca) { Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['GET:/scenarios', getAllScenarios]); Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['GET:/scenarios/:scenarioKey', true, getScenarioByKey]); Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['GET:/scenarios/:scenarioKey/active', true, getScenarioStatusByKey]); + Nocca.pubsub.publish(Nocca.constants.PUBSUB_REST_ROUTE_ADDED, ['PUT:/scenarios/:scenarioKey/active', true, setScenarioStatusByKey]); } - function resetScenarioByKey(req, res, config, params, writeHead, writeEnd) { + function resetScenarioByKey(apiReq) { try { - resetScenario(params.scenarioKey); + resetScenario(apiReq.matches.scenarioKey); - writeHead(res) - .writeEnd(JSON.stringify(exportScenarios(params.scenarioKey).currentPosition)); + apiReq.ok().end(JSON.stringify(exportScenarios(apiReq.matches.scenarioKey).currentPosition)); } catch (e) { - writeHead(res, 404).writeEnd(); + apiReq.notFound().end(); } } - function getAllScenarios(req, res, config, match, writeHead, writeEnd) { - writeHead(res).writeEnd(JSON.stringify(exportScenarios())); + function getAllScenarios(apiReq) { + apiReq.ok().end(JSON.stringify(exportScenarios())); } - function getScenarioByKey (req, res, config, params, writeHead, writeEnd) { + function getScenarioByKey (apiReq) { try { + var requestedScenario = exportScenarios(apiReq.matches.scenarioKey); + if (typeof requestedScenario === 'undefined') { throw new Error(); } + apiReq.ok().end(JSON.stringify(requestedScenario)); + } catch (e) { + apiReq.notFound().end(); + } + } - writeHead(res) - .writeEnd(JSON.stringify(exportScenarios(params.scenarioKey))); - + function getScenarioStatusByKey (apiReq) { + try { + // This statement may cause exceptions while attempting to deref the scenario status, which will result in 404 + var requestedScenarioStatus = exportScenarios(apiReq.matches.scenarioKey).$$active; + apiReq.ok().end(JSON.stringify(requestedScenarioStatus)); } catch (e) { - writeHead(res, 404).writeEnd(); + apiReq.notFound().end(); } } - function getScenarioStatusByKey (req, res, config, params, writeHead, writeEnd) { - // TODO: noooo, you're dependant on the $$active property now! Blegh + function setScenarioStatusByKey (apiReq) { try { - writeHead(res) - .writeEnd(JSON.stringify(exportScenarios(params.scenarioKey).$$active)); + // This statement may cause exceptions while attempting to deref the scenario status, which will result in 404 + var requestedScenario = exportScenarios(apiReq.matches.scenarioKey); + if (typeof requestedScenario === 'undefined') { throw new Error(); } + + $utils.readBody(apiReq.req).then(function(body) { + body = JSON.parse(body); + + if (_.isBoolean(body)) { + + if (body && !requestedScenario.$$active) { + registerScenarioOnEndpoint(requestedScenario, requestedScenario.currentPosition.state.endpointKey); + } + else if (!body && requestedScenario.$$active) { + deregisterScenarioFromEndpoint(requestedScenario, requestedScenario.currentPosition.state.endpointKey); + } + + apiReq.ok().end(JSON.stringify(requestedScenario.$$active)); + + } + else { + + apiReq.badRequest().end('Status must be boolean'); + + } + }).fail(function() { + + apiReq.badRequest().end('Unable to read response'); + + }); + } catch (e) { - writeHead(res, 404).writeEnd(); + apiReq.notFound().end(); } } diff --git a/test/node/test_v2.js b/test/node/test_v2.js index a2bd255..dd9d23d 100644 --- a/test/node/test_v2.js +++ b/test/node/test_v2.js @@ -19,6 +19,7 @@ var config = { writeNewScenarios: true, scenarioOutputDir: 'D:/dev/tmp/' } + }; @@ -94,11 +95,11 @@ var googleScenario = googleScenarioBuilder.sequentialScenario() .title('Get Google Woohoo Page') .matchUsing(Matchers.anyMatcher) .respondWith(GoogleResponses.f2) - .then() - .on('yahoo') - .title('Get Yahoo Page') - .matchUsing(Matchers.ginInUrlBuilder('1234')) - .respondWith(GoogleResponses.f3) + //.then() + //.on('yahoo') + //.title('Get Yahoo Page') + //.matchUsing(Matchers.ginInUrlBuilder('1234')) + //.respondWith(GoogleResponses.f3) .build(); //var SandboxedModule = require('sandboxed-module');