diff --git a/lib/control.js b/lib/control.js index 5d8f6e41..20806c58 100644 --- a/lib/control.js +++ b/lib/control.js @@ -23,7 +23,6 @@ function setReadOnly(obj) { if (isFreezable(obj)) { _.forOwn(obj, function (value) { if (typeof value === 'object' && value !== null) { - //console.log('setReadOnly', value); setReadOnly(value); } }); diff --git a/lib/media.test.js b/lib/media.test.js index a46f226d..85b2e1bb 100644 --- a/lib/media.test.js +++ b/lib/media.test.js @@ -1,6 +1,6 @@ 'use strict'; -var _ = require('lodash'), +const _ = require('lodash'), filename = __filename.split('/').pop().split('.').shift(), lib = require('./' + filename), expect = require('chai').expect, diff --git a/lib/rest.js b/lib/rest.js new file mode 100644 index 00000000..23496e54 --- /dev/null +++ b/lib/rest.js @@ -0,0 +1,18 @@ +'use strict'; + +const nodeFetch = require('node-fetch'), + jsonHeaders = {'Content-type':'application/json'}; + +function getObject(url, options) { + return exports.fetch(url, options).then(function (response) { + return response.json(); + }); +} + +function putObject(url, data) { + return exports.fetch(url, {method: 'PUT', body: JSON.stringify(data), headers: jsonHeaders}); +} + +module.exports.fetch = nodeFetch; +module.exports.getObject = getObject; +module.exports.putObject = putObject; diff --git a/lib/rest.test.js b/lib/rest.test.js new file mode 100644 index 00000000..1186b46a --- /dev/null +++ b/lib/rest.test.js @@ -0,0 +1,40 @@ +'use strict'; + +var _ = require('lodash'), + bluebird = require('bluebird'), + filename = __filename.split('/').pop().split('.').shift(), + lib = require('./' + filename), + sinon = require('sinon'); + +describe(_.startCase(filename), function () { + var sandbox; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + sandbox.stub(lib, 'fetch'); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('getObject', function () { + const fn = lib[this.title]; + + it('gets', function () { + lib.fetch.returns(bluebird.resolve({json: _.constant({})})); + + return fn('some-url'); + }); + }); + + describe('putObject', function () { + const fn = lib[this.title]; + + it('gets', function () { + lib.fetch.returns(bluebird.resolve()); + + return fn('some-url'); + }); + }); +}); diff --git a/lib/services/components.js b/lib/services/components.js index d6583f47..9c564039 100644 --- a/lib/services/components.js +++ b/lib/services/components.js @@ -297,7 +297,7 @@ function del(uri, locals) { * @returns {Promise} */ function post(uri, data, locals) { - uri += '/' + uid(); + uri += '/' + uid.get(); return cascadingPut(uri, data, locals).then(function (result) { result._ref = uri; return result; diff --git a/lib/services/notifications.js b/lib/services/notifications.js index 05cb8112..ccecac6e 100644 --- a/lib/services/notifications.js +++ b/lib/services/notifications.js @@ -1,14 +1,16 @@ 'use strict'; const _ = require('lodash'), + bluebird = require('bluebird'), + contentType = 'Content-type', log = require('../log'), - restler = require('restler'), - contentType = 'Content-type'; + rest = require('../rest'); /** * @param {string} event * @param {string} url * @param {object} data + * @returns {Promise} */ function callWebhook(event, url, data) { const headers = { @@ -27,8 +29,8 @@ function callWebhook(event, url, data) { options.body = data; } - restler.request(url, options).on('complete', function (result, response) { - log.info('called webhook', _.pick(response, ['status', 'statusText']), result); + return rest.fetch(url, options).then(function (response) { + log.info('called webhook', _.pick(response, ['status', 'statusText'])); }); } @@ -36,15 +38,15 @@ function callWebhook(event, url, data) { * @param {object} site * @param {string} eventName * @param {*} [data] - * @returns {function} + * @returns {Promise} */ function notify(site, eventName, data) { const events = _.get(site, 'notify.webhooks'); if (events && _.isArray(events[eventName])) { - _.each(events[eventName], function (url) { - callWebhook(eventName, url, data); - }); + return bluebird.all(_.map(events[eventName], function (url) { return callWebhook(eventName, url, data); })); + } else { + return bluebird.resolve(); } } diff --git a/lib/services/notifications.test.js b/lib/services/notifications.test.js index 1e69ea64..2b60a16f 100644 --- a/lib/services/notifications.test.js +++ b/lib/services/notifications.test.js @@ -1,11 +1,11 @@ 'use strict'; var _ = require('lodash'), + bluebird = require('bluebird'), expect = require('chai').expect, filename = __filename.split('/').pop().split('.').shift(), lib = require('./' + filename), log = require('../log'), - mockRestler = require('../../test/fixtures/mocks/restler'), - restler = require('restler'), + rest = require('../rest'), sinon = require('sinon'); @@ -14,7 +14,7 @@ describe(_.startCase(filename), function () { beforeEach(function () { sandbox = sinon.sandbox.create(); - sandbox.stub(restler); + sandbox.stub(rest); sandbox.stub(log); }); @@ -48,11 +48,11 @@ describe(_.startCase(filename), function () { site = {notify: {webhooks: {someEvent: [hookUrl]}}}, eventName = 'someEvent'; - restler.request.returns(mockRestler.createRequest()); + rest.fetch.returns(bluebird.resolve()); - fn(site, eventName); - - sinon.assert.calledWith(restler.request, hookUrl, { headers: { 'X-Event': 'someEvent' }, method: 'POST' }); + return fn(site, eventName).then(function () { + sinon.assert.calledWith(rest.fetch, hookUrl, { headers: { 'X-Event': 'someEvent' }, method: 'POST' }); + }); }); it('notifies with string value', function () { @@ -61,17 +61,17 @@ describe(_.startCase(filename), function () { data = 'some-string', eventName = 'someEvent'; - restler.request.returns(mockRestler.createRequest()); - - fn(site, eventName, data); - - sinon.assert.calledWith(restler.request, hookUrl, { - body: data, - headers: { - 'Content-type': 'text/plain', - 'X-Event': eventName - }, - method: 'POST' + rest.fetch.returns(bluebird.resolve()); + + return fn(site, eventName, data).then(function () { + sinon.assert.calledWith(rest.fetch, hookUrl, { + body: data, + headers: { + 'Content-type': 'text/plain', + 'X-Event': eventName + }, + method: 'POST' + }); }); }); @@ -81,33 +81,30 @@ describe(_.startCase(filename), function () { data = {a:'b'}, eventName = 'someEvent'; - restler.request.returns(mockRestler.createRequest()); - - fn(site, eventName, data); - - sinon.assert.calledWith(restler.request, hookUrl, { - body: JSON.stringify(data), - headers: { - 'Content-type': 'application/json', - 'X-Event': eventName - }, - method: 'POST' + rest.fetch.returns(bluebird.resolve()); + + return fn(site, eventName, data).then(function () { + sinon.assert.calledWith(rest.fetch, hookUrl, { + body: JSON.stringify(data), + headers: { + 'Content-type': 'application/json', + 'X-Event': eventName + }, + method: 'POST' + }); }); }); it('logs', function () { const hookUrl = 'some_url', site = {notify: {webhooks: {someEvent: [hookUrl]}}}, - eventName = 'someEvent', - request = mockRestler.createRequest(); - - sinon.stub(request, 'on'); - request.on.yields(); - restler.request.returns(request); + eventName = 'someEvent'; - fn(site, eventName); + rest.fetch.returns(bluebird.resolve()); - sinon.assert.calledWith(restler.request, hookUrl, { headers: { 'X-Event': 'someEvent' }, method: 'POST' }); + return fn(site, eventName).then(function () { + sinon.assert.calledWith(rest.fetch, hookUrl, { headers: { 'X-Event': 'someEvent' }, method: 'POST' }); + }); }); }); }); diff --git a/lib/services/pages.js b/lib/services/pages.js index 6ca64995..d69b415d 100644 --- a/lib/services/pages.js +++ b/lib/services/pages.js @@ -52,7 +52,7 @@ function getPrefix(uri) { function renameReferenceUniquely(uri) { const prefix = uri.substr(0, uri.indexOf('/components/')); - return prefix + '/components/' + components.getName(uri) + '/instances/' + uid(); + return prefix + '/components/' + components.getName(uri) + '/instances/' + uid.get(); } /** @@ -220,7 +220,7 @@ function create(uri, data, locals) { pageData = data && _.omit(data, 'layout'), prefix = uri.substr(0, uri.indexOf('/pages')), site = getSite(prefix, locals), - pageReference = prefix + '/pages/' + uid(); + pageReference = prefix + '/pages/' + uid.get(); if (!layoutReference) { throw new Error('Client: Data missing layout reference.'); diff --git a/lib/services/schedule.js b/lib/services/schedule.js index fec3c79e..84a891fe 100644 --- a/lib/services/schedule.js +++ b/lib/services/schedule.js @@ -1,18 +1,18 @@ 'use strict'; var interval, - intervalDelay = 60000; // one minute + intervalDelay = 6000; // one minute const _ = require('lodash'), bluebird = require('bluebird'), - components = require('../services/components'), db = require('../services/db'), + log = require('../log'), + publishProperty = 'publish', references = require('../services/references'), + rest = require('../rest'), siteService = require('../services/sites'), scheduledAtProperty = 'at', - publishProperty = 'publish', scheduledVersion = 'scheduled', - pages = require('../services/pages'), - log = require('../log'); + uid = require('../uid'); /** * @param {number} value @@ -22,20 +22,18 @@ function setScheduleInterval(value) { } /** - * @param {string} uri + * Note: There is nothing to do if this fails except log + * @param {string} url * @returns {Promise} */ -function publishPage(uri) { - return pages.publish(uri); -} +function publishExternally(url) { + const latest = references.replaceVersion(url), + published = references.replaceVersion(url, 'published'); -/** - * @param {string} uri - * @returns {Promise} - */ -function publishComponent(uri) { - return components.get(references.replaceVersion(uri)).then(function (data) { - return components.put(references.replaceVersion(uri, 'published'), data); + return rest.getObject(latest).then(function (data) { + return rest.putObject(published, data).then(function () { + log.info('published', latest); + }); }); } @@ -61,38 +59,6 @@ function getPublishableItems(list, now) { }); } -/** - * @param {string} uri - * @returns {Promise} - */ -function publishItem(uri) { - var promise; - - if (uri) { - if (uri.indexOf('/pages/') > -1) { - promise = publishPage(uri); - } else if (uri.indexOf('/components/') > -1) { - promise = publishComponent(uri); - } else { - log.error('Unknown publish type: ' + uri); - } - } else { - log.error('Missing publish type'); - } - - if (promise) { - promise.then(function () { - log.info('published', uri); - }).catch(function (err) { - log.error('error publishing', uri, err.stack); - }); - } else { - promise = bluebird.resolve(); - } - - return promise; -} - /** * Create the id for the new item * @@ -111,12 +77,11 @@ function createScheduleObjectUID(uri, data) { if (!_.isNumber(at)) { throw new Error('Client: Missing "at" property as number.'); - } else if (!_.isString(publish)) { - throw new Error('Client: Missing "publish" property as string.'); + } else if (!references.isUrl(publish)) { + throw new Error('Client: Missing "publish" property as valid url.'); } - return prefix + '/schedule/' + at.toString(36) + '-' + - publish.replace(/[\/\.]/g, '-').replace(/-(?=-)/g, ''); + return prefix + '/schedule/' + uid.get(); } /** @@ -135,8 +100,7 @@ function del(uri) { { type: 'del', key: targetReference } ]; - return db.batch(ops) - .return(data); + return db.batch(ops).return(data); }); } @@ -155,8 +119,7 @@ function post(uri, data) { { type: 'put', key: targetReference, value: JSON.stringify(referencedData) } ]; - return db.batch(ops) - .return(referencedData); + return db.batch(ops).return(referencedData); } /** @@ -168,7 +131,9 @@ function publishByTime(list) { const promises = _.reduce(getPublishableItems(list, new Date().getTime()), function (promises, item) { const uri = item.value[publishProperty]; - promises.push(publishItem(uri).then(function () { return del(item.key); })); + promises.push(publishExternally(uri) + .then(function () { return del(item.key); }) + .catch(function (error) { log.error('publishing error', error); })); return promises; }, []); @@ -183,14 +148,17 @@ function startListening() { if (!interval) { interval = setInterval(function () { // get list for each site - return _.map(siteService.sites(), function (site) { - return db.pipeToPromise(db.list({ + _.each(siteService.sites(), function (site) { + db.pipeToPromise(db.list({ prefix: site.host + site.path + '/schedule', keys: true, values: true, isArray: true })).then(JSON.parse) - .then(publishByTime); + .then(publishByTime) + .catch(function (error) { + log.error('failed to publish by time', error); + }); }); }, intervalDelay); } diff --git a/lib/services/schedule.test.js b/lib/services/schedule.test.js index 068b4212..95f1567a 100644 --- a/lib/services/schedule.test.js +++ b/lib/services/schedule.test.js @@ -1,16 +1,16 @@ 'use strict'; var _ = require('lodash'), - filename = __filename.split('/').pop().split('.').shift(), - lib = require('./' + filename), - components = require('./components'), - expect = require('chai').expect, - sinon = require('sinon'), bluebird = require('bluebird'), db = require('./db'), + expect = require('chai').expect, + filename = __filename.split('/').pop().split('.').shift(), + lib = require('./' + filename), log = require('../log'), - pages = require('./pages'), - siteService = require('./sites'); + rest = require('../rest'), + sinon = require('sinon'), + siteService = require('./sites'), + uid = require('../uid'); describe(_.startCase(filename), function () { var sandbox, @@ -24,13 +24,10 @@ describe(_.startCase(filename), function () { sandbox.stub(db, 'put'); sandbox.stub(db, 'batch'); sandbox.stub(db, 'pipeToPromise'); - sandbox.stub(pages, 'publish'); - sandbox.stub(components, 'get'); - sandbox.stub(components, 'put'); sandbox.stub(siteService, 'sites'); - sandbox.stub(log, 'info'); - sandbox.stub(log, 'warn'); - sandbox.stub(log, 'error'); + sandbox.stub(log); + sandbox.stub(uid); + sandbox.stub(rest); }); afterEach(function () { @@ -43,7 +40,7 @@ describe(_.startCase(filename), function () { it('throws on missing "at" property', function (done) { var ref = 'domain/pages/some-name', - data = {publish: 'abcg'}; + data = {publish: 'http://abcg'}; expect(function () { fn(ref, data).nodeify(done); @@ -65,7 +62,7 @@ describe(_.startCase(filename), function () { it('schedules a publish of abc at 123', function (done) { var ref = 'domain/pages/some-name', - data = {at: 123, publish: 'abcf'}; + data = {at: 123, publish: 'http://abcf'}; db.batch.returns(bluebird.resolve({})); @@ -79,7 +76,7 @@ describe(_.startCase(filename), function () { var fn = lib[this.title]; it('deletes a scheduled item', function (done) { - var publishTarget = 'abcf', + var publishTarget = 'http://abcf', publishDate = 123, ref = 'domain/pages/some-name', data = {at: 123, publish: publishTarget}; @@ -97,20 +94,24 @@ describe(_.startCase(filename), function () { var fn = lib[this.title]; it('does not throw if started twice', function () { + rest.getObject.returns(bluebird.resolve({})); + rest.putObject.returns(bluebird.resolve({})); + expect(function () { fn(); fn(); }).to.not.throw(); }); - it('publishes page', function (done) { - var uri = 'abce/pages/abce', + it('publishes', function (done) { + var uri = 'http://abce', scheduledItem = {at: intervalDelay - 1, publish: uri}, data = {key:'some-key', value: JSON.stringify(scheduledItem)}; + rest.getObject.returns(bluebird.resolve({})); + rest.putObject.returns(bluebird.resolve({})); siteService.sites.returns([{host: 'a', path: '/'}]); db.pipeToPromise.returns(bluebird.resolve(JSON.stringify([data]))); - pages.publish.returns(bluebird.resolve()); db.get.returns(bluebird.resolve(JSON.stringify(scheduledItem))); db.batch.returns(bluebird.resolve()); @@ -118,53 +119,53 @@ describe(_.startCase(filename), function () { sandbox.clock.tick(intervalDelay); + log.error = done; log.info = function () { - sinon.assert.calledWith(pages.publish, uri); done(); }; }); + it('logs error if missing thing to publish', function (done) { + var uri = 'abce/pages/abcd', + data = {key:'some-key', value: JSON.stringify({at: intervalDelay - 1, publish: uri})}; - it('publishes component', function (done) { - var uri = 'abce/components/abce', - scheduledItem = {at: intervalDelay - 1, publish: uri}, - data = {key:'some-key', value: JSON.stringify(scheduledItem)}, - componentData = {someData: 'someValue'}; - + rest.getObject.returns(bluebird.reject(new Error('guess it was not found'))); siteService.sites.returns([{host: 'a', path: '/'}]); db.pipeToPromise.returns(bluebird.resolve(JSON.stringify([data]))); - components.get.returns(bluebird.resolve(componentData)); - components.put.returns(bluebird.resolve(componentData)); - db.get.returns(bluebird.resolve(JSON.stringify(scheduledItem))); - db.batch.returns(bluebird.resolve()); fn(); sandbox.clock.tick(intervalDelay); - log.info = function () { - sinon.assert.calledWith(components.put, uri + '@published', componentData); + log.info = done; + log.error = function (msg) { + expect(msg).to.equal('publishing error'); done(); }; }); it('logs error if failed to publish page', function (done) { - bluebird.onPossiblyUnhandledRejection(function () {}); + bluebird.onPossiblyUnhandledRejection(_.noop); // rejection.suppressUnhandledRejections(); when bluebird supports it better - var uri = 'abce/pages/abcd', - data = {key:'some-key', value: JSON.stringify({at: intervalDelay - 1, publish: uri})}; + var uri = 'http://abce', + scheduledItem = {at: intervalDelay - 1, publish: uri}, + data = {key:'some-key', value: JSON.stringify(scheduledItem)}; + rest.getObject.returns(bluebird.resolve({})); + rest.putObject.returns(bluebird.reject(new Error(''))); siteService.sites.returns([{host: 'a', path: '/'}]); db.pipeToPromise.returns(bluebird.resolve(JSON.stringify([data]))); - pages.publish.returns(bluebird.reject(new Error(''))); + db.get.returns(bluebird.resolve(JSON.stringify(scheduledItem))); + db.batch.returns(bluebird.resolve()); fn(); sandbox.clock.tick(intervalDelay); - log.error = function () { - sinon.assert.calledWith(pages.publish, uri); + log.info = done; + log.error = function (msg) { + expect(msg).to.equal('publishing error'); done(); }; }); diff --git a/lib/uid.js b/lib/uid.js index 5ee846e3..dbd3f124 100644 --- a/lib/uid.js +++ b/lib/uid.js @@ -1,3 +1,3 @@ 'use strict'; -module.exports = require('cuid'); \ No newline at end of file +module.exports.get = require('cuid'); \ No newline at end of file diff --git a/package.json b/package.json index 5228b001..8e8b9399 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "lodash-ny-util": "^0.1.0", "memdown": "^1.0.0", "multiplex-templates": "^1.2", - "restler": "^3.4.0", + "node-fetch": "^1.3.3", "through2-filter": "^1.4", "vhost": "^3.0.0", "winston": "^0.9" diff --git a/test/api/schedule/delete.js b/test/api/schedule/delete.js index 77ee73cf..d15772cb 100644 --- a/test/api/schedule/delete.js +++ b/test/api/schedule/delete.js @@ -14,7 +14,7 @@ describe(endpointName, function () { acceptsJsonBody = apiAccepts.acceptsJsonBody(_.camelCase(filename)), acceptsHtml = apiAccepts.acceptsHtml(_.camelCase(filename)), componentData = {}, - scheduleData = { at: new Date('2015-01-01').getTime(), publish: 'localhost.example.com/pages/valid' }, + scheduleData = { at: new Date('2015-01-01').getTime(), publish: 'http://localhost.example.com/pages/valid' }, layoutData = {}, pageData = {}; diff --git a/test/api/schedule/post.js b/test/api/schedule/post.js index 784cb75f..8947feb9 100644 --- a/test/api/schedule/post.js +++ b/test/api/schedule/post.js @@ -36,8 +36,8 @@ describe(endpointName, function () { acceptsHtml(path, {}, 406, '406 text/html not acceptable'); acceptsJsonBody(path, {}, _.assign({}, pageData), 400, { message: 'Missing "at" property as number.', code: 400 }); - acceptsJsonBody(path, {}, _.assign({at: time}, pageData), 400, { message: 'Missing "publish" property as string.', code: 400 }); - acceptsJsonBody(path, {}, _.assign({at: time, publish: 'abc'}, pageData), 201, { _ref: 'localhost.example.com/schedule/i4dd91c0-abc', at: time, publish: 'abc' }); + acceptsJsonBody(path, {}, _.assign({at: time}, pageData), 400, { message: 'Missing "publish" property as valid url.', code: 400 }); + acceptsJsonBody(path, {}, _.assign({at: time, publish: 'http://abc'}, pageData), 201, { _ref: 'localhost.example.com/schedule/some-uid', at: time, publish: 'http://abc' }); }); describe('/schedule/:name', function () { diff --git a/test/fixtures/api-accepts.js b/test/fixtures/api-accepts.js index 51199cb1..de7b41f7 100644 --- a/test/fixtures/api-accepts.js +++ b/test/fixtures/api-accepts.js @@ -14,6 +14,7 @@ var _ = require('lodash'), siteService = require('../../lib/services/sites'), expect = require('chai').expect, filter = require('through2-filter'), + uid = require('../../lib/uid'), app, host; @@ -368,6 +369,12 @@ function stubLogging(sandbox) { return sandbox; } +function stubUid(sandbox) { + sandbox.stub(uid); + uid.get.returns('some-uid'); + return sandbox; +} + /** * Before starting testing at all, prepare certain things to make sure our performance testing is accurate. */ @@ -384,6 +391,7 @@ function beforeTesting(suite, options) { stubGetTemplate(options.sandbox); stubMultiplexRender(options.sandbox); stubLogging(options.sandbox); + stubUid(options.sandbox); routes.addHost(app, options.hostname); return db.clear().then(function () { @@ -417,6 +425,7 @@ function beforeEachTest(options) { stubGetTemplate(options.sandbox); stubMultiplexRender(options.sandbox); stubLogging(options.sandbox); + stubUid(options.sandbox); routes.addHost(app, options.hostname); return db.clear().then(function () { diff --git a/test/fixtures/mocks/restler.js b/test/fixtures/mocks/restler.js deleted file mode 100644 index 2567d96f..00000000 --- a/test/fixtures/mocks/restler.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -function createRequest() { - var request; - - request = { - on() { - return request; - } - }; - - return request; -} - -module.exports.createRequest = createRequest; \ No newline at end of file