diff --git a/config/lib/helpers/broadcast.js b/config/lib/helpers/contentfulEntry.js similarity index 50% rename from config/lib/helpers/broadcast.js rename to config/lib/helpers/contentfulEntry.js index 6feb9298..a14960f6 100644 --- a/config/lib/helpers/broadcast.js +++ b/config/lib/helpers/contentfulEntry.js @@ -4,6 +4,7 @@ module.exports = { contentTypes: { askYesNo: { type: 'askYesNo', + broadcastable: true, templates: [ 'saidYes', 'saidNo', @@ -11,18 +12,42 @@ module.exports = { 'autoReply', ], }, - autoReplyBroadcast: { - type: 'autoReplyBroadcast', + autoReply: { + type: 'autoReply', templates: [ 'autoReply', ], }, + autoReplyBroadcast: { + type: 'autoReplyBroadcast', + broadcastable: true, + }, + defaultTopicTrigger: { + type: 'defaultTopicTrigger', + }, + message: { + type: 'message', + }, + photoPostConfig: { + type: 'photoPostConfig', + postType: 'photo', + }, + textPostConfig: { + type: 'textPostConfig', + postType: 'text', + }, + // Legacy types: // Ideally we'd backfill all legacy entries as their new types, but we likely can't change the // the type of a Contentful entry without changing its id (if that's the case - we'd need to // bulk update all documents in the Conversations messages DB) - legacy: { + // This externalPostConfig type will deprecated by an autoReply: + externalPostConfig: { + type: 'externalPostConfig', + postType: 'external', + }, + legacyBroadcast: { type: 'broadcast', - templates: [], + broadcastable: true, }, }, }; diff --git a/config/lib/helpers/defaultTopicTrigger.js b/config/lib/helpers/defaultTopicTrigger.js index 8a6dcac5..b0e02aa3 100644 --- a/config/lib/helpers/defaultTopicTrigger.js +++ b/config/lib/helpers/defaultTopicTrigger.js @@ -2,7 +2,4 @@ module.exports = { allDefaultTopicTriggersCacheKey: 'all', - contentTypes: [ - 'defaultTopicTrigger', - ], }; diff --git a/config/lib/helpers/topic.js b/config/lib/helpers/topic.js index c9e8f271..fe988bfe 100644 --- a/config/lib/helpers/topic.js +++ b/config/lib/helpers/topic.js @@ -1,34 +1,5 @@ 'use strict'; -// @see documentation/endpoints/topics -const contentTypes = { - askYesNo: { - type: 'askYesNo', - isBroadcast: true, - postType: null, - }, - autoReplyBroadcast: { - type: 'autoReplyBroadcast', - isBroadcast: true, - postType: null, - }, - externalPostConfig: { - type: 'externalPostConfig', - isBroadcast: false, - postType: 'external', - }, - photoPostConfig: { - type: 'photoPostConfig', - isBroadcast: false, - postType: 'photo', - }, - textPostConfig: { - type: 'textPostConfig', - isBroadcast: false, - postType: 'text', - }, -}; - const defaultText = { declined: 'Text MENU if you\'d like to find a different action to take.', invalidInput: 'Sorry, I didn\'t get that.', @@ -49,7 +20,6 @@ const photoPostDefaultText = { module.exports = { allTopicsCacheKey: 'all', - contentTypes, defaultPostType: 'photo', /* * Maps each content type with a map of templateNames and its corresponding field name and diff --git a/lib/helpers/broadcast.js b/lib/helpers/broadcast.js index 55ebd44e..4419541a 100644 --- a/lib/helpers/broadcast.js +++ b/lib/helpers/broadcast.js @@ -3,14 +3,13 @@ const logger = require('winston'); const contentful = require('../contentful'); const helpers = require('../helpers'); -const config = require('../../config/lib/helpers/broadcast'); /** * @return {Array} */ function getContentTypes() { - const contentTypeConfigs = Object.values(config.contentTypes); - return contentTypeConfigs.map(contentTypeConfig => contentTypeConfig.type); + const configs = Object.values(helpers.contentfulEntry.getContentTypeConfigs()); + return configs.filter(config => config.broadcastable).map(config => config.type); } /** @@ -66,20 +65,17 @@ function getById(broadcastId, resetCache = false) { */ function parseBroadcastFromContentfulEntry(contentfulEntry) { const contentType = contentful.getContentTypeFromContentfulEntry(contentfulEntry); - const contentTypeConfigs = config.contentTypes; - // A nice to have TODO is migrating all legacy broadcast entries into the respective new type, so // we can remove check and the function it calls entirely. - if (contentType === contentTypeConfigs.legacy.type) { + const contentTypeConfigs = helpers.contentfulEntry.getContentTypeConfigs(); + if (contentType === contentTypeConfigs.legacyBroadcast.type) { return module.exports.parseLegacyBroadcastFromContentfulEntry(contentfulEntry); } const data = helpers.contentfulEntry.getSummaryFromContentfulEntry(contentfulEntry); const message = module.exports .parseBroadcastMessageFromContentfulEntryAndTemplateName(contentfulEntry, contentType); - const fieldNames = contentTypeConfigs[contentType].templates; - const templates = module.exports - .parseTemplatesFromContentfulEntryAndFieldNames(contentfulEntry, fieldNames); + const templates = helpers.contentfulEntry.getMessageTemplatesFromContentfulEntry(contentfulEntry); return Promise.resolve(Object.assign(data, { message, templates })); } @@ -132,28 +128,6 @@ function parseBroadcastMessageFromContentfulEntryAndTemplateName(contentfulEntry .getMessageTemplateFromContentfulEntryAndTemplateName(broadcastMessageEntry, templateName); } -/** - * @param {Object} - * @param {String} - * @return {Object} - */ -function parseTemplatesFromContentfulEntryAndFieldNames(contentfulEntry, fieldNames) { - const data = {}; - if (!contentfulEntry) { - return data; - } - - fieldNames.forEach((fieldName) => { - const messageEntry = contentfulEntry.fields[fieldName]; - if (!messageEntry) { - return; - } - data[fieldName] = helpers.contentfulEntry - .getMessageTemplateFromContentfulEntryAndTemplateName(messageEntry, fieldName); - }); - return data; -} - module.exports = { fetch, fetchById, @@ -162,5 +136,4 @@ module.exports = { parseBroadcastFromContentfulEntry, parseBroadcastMessageFromContentfulEntryAndTemplateName, parseLegacyBroadcastFromContentfulEntry, - parseTemplatesFromContentfulEntryAndFieldNames, }; diff --git a/lib/helpers/contentfulEntry.js b/lib/helpers/contentfulEntry.js index 17890e1a..f854dd99 100644 --- a/lib/helpers/contentfulEntry.js +++ b/lib/helpers/contentfulEntry.js @@ -3,6 +3,14 @@ const logger = require('winston'); const contentful = require('../contentful'); const helpers = require('../helpers'); +const config = require('../../config/lib/helpers/contentfulEntry'); + +/** + * @return {Array} + */ +function getContentTypeConfigs() { + return config.contentTypes; +} /** * @param {String} contentfulId @@ -69,6 +77,48 @@ function getMessageTemplateFromContentfulEntryAndTemplateName(contentfulEntry, t }; } +/** + * @param {Object} + * @param {String} + * @return {Object} + */ +function getMessageTemplatesFromContentfulEntry(contentfulEntry) { + const contentType = contentful.getContentTypeFromContentfulEntry(contentfulEntry); + const fieldNames = config.contentTypes[contentType].templates; + const result = {}; + + if (!contentfulEntry || !fieldNames) { + return result; + } + + fieldNames.forEach((fieldName) => { + const messageEntry = contentfulEntry.fields[fieldName]; + if (!messageEntry) { + return; + } + result[fieldName] = module.exports + .getMessageTemplateFromContentfulEntryAndTemplateName(messageEntry, fieldName); + }); + return result; +} + +/** + * @param {Object} contentfulEntry + * @return {Boolean} + */ +function isAutoReply(contentfulEntry) { + return contentful.isContentType(contentfulEntry, config.contentTypes.autoReply.type); +} + +/** + * @param {Object} contentfulEntry + * @return {Boolean} + */ +function isBroadcastable(contentfulEntry) { + const contentType = contentful.getContentTypeFromContentfulEntry(contentfulEntry); + return config.contentTypes[contentType].broadcastable; +} + /** * @param {Object} contentfulEntry * @return {Boolean} @@ -82,13 +132,17 @@ function isDefaultTopicTrigger(contentfulEntry) { * @return {Boolean} */ function isMessage(contentfulEntry) { - return contentful.isContentType(contentfulEntry, 'message'); + return contentful.isContentType(contentfulEntry, config.contentTypes.message.type); } module.exports = { getById, + getContentTypeConfigs, getMessageTemplateFromContentfulEntryAndTemplateName, + getMessageTemplatesFromContentfulEntry, getSummaryFromContentfulEntry, + isAutoReply, + isBroadcastable, isDefaultTopicTrigger, isMessage, parseContentfulEntry, diff --git a/lib/helpers/defaultTopicTrigger.js b/lib/helpers/defaultTopicTrigger.js index b9b72f3d..014a7b55 100644 --- a/lib/helpers/defaultTopicTrigger.js +++ b/lib/helpers/defaultTopicTrigger.js @@ -11,7 +11,8 @@ const config = require('../../config/lib/helpers/defaultTopicTrigger'); * @return {Array} */ function getContentTypes() { - return config.contentTypes; + const configs = helpers.contentfulEntry.getContentTypeConfigs(); + return [configs.defaultTopicTrigger.type]; } /** diff --git a/lib/helpers/topic.js b/lib/helpers/topic.js index 2790072b..64d7c30c 100644 --- a/lib/helpers/topic.js +++ b/lib/helpers/topic.js @@ -9,8 +9,14 @@ const config = require('../../config/lib/helpers/topic'); * @return {Array} */ function getContentTypes() { - const contentTypeConfigs = Object.values(config.contentTypes); - return contentTypeConfigs.map(contentTypeConfig => contentTypeConfig.type); + const configs = Object.values(helpers.contentfulEntry.getContentTypeConfigs()); + // Return any configs that have templates set, or a postType. + // We'll eventually refactor this config to only check for existence of templates, with each item + // corresponding to a reference field to a message or changeTopicMessage entry, like how it does + // in an askYesNo or autoReply topic (vs a textPostConfig or photoPostConfig, where we're using + // text fields and the fieldName map defined in the topic helper config). + return configs.filter(typeConfig => typeConfig.templates || typeConfig.postType) + .map(typeConfig => typeConfig.type); } /** @@ -155,16 +161,22 @@ function parseTemplateFromContentfulEntryAndTemplateName(contentfulEntry, templa * @param {Object} entry * @return {Object} */ -function parseTemplatesFromContentfulEntryAndCampaign(entry, campaign) { +function parseTemplatesFromContentfulEntryAndCampaign(contentfulEntry, campaign) { + if (helpers.contentfulEntry.isAutoReply(contentfulEntry)) { + return helpers.contentfulEntry.getMessageTemplatesFromContentfulEntry(contentfulEntry); + } + const contentType = contentful.getContentTypeFromContentfulEntry(contentfulEntry); const data = {}; - const contentType = contentful.getContentTypeFromContentfulEntry(entry); const templateNames = Object.keys(config.templatesByContentType[contentType]); templateNames.forEach((templateName) => { data[templateName] = module.exports - .parseTemplateFromContentfulEntryAndTemplateName(entry, templateName); - data[templateName].rendered = helpers + .parseTemplateFromContentfulEntryAndTemplateName(contentfulEntry, templateName); + data[templateName].text = helpers .replacePhoenixCampaignVars(data[templateName].raw, campaign); + // TODO: Remove rendered property once Gambit Conversations references text. + data[templateName].rendered = data[templateName].text; }); + return data; } @@ -173,7 +185,8 @@ function parseTemplatesFromContentfulEntryAndCampaign(entry, campaign) { * @return {String} */ function getPostTypeFromContentType(contentType) { - return config.contentTypes[contentType].postType; + const contentTypeConfigs = helpers.contentfulEntry.getContentTypeConfigs(); + return contentTypeConfigs[contentType].postType; } /** @@ -181,18 +194,14 @@ function getPostTypeFromContentType(contentType) { * @return {Promise} */ function parseTopicFromContentfulEntry(contentfulEntry) { - const topicId = contentful.getContentfulIdFromContentfulEntry(contentfulEntry); - logger.debug('parseTopicFromContentfulEntry', { topicId }); const contentType = contentful.getContentTypeFromContentfulEntry(contentfulEntry); - if (config.contentTypes[contentType].isBroadcast) { + + if (helpers.contentfulEntry.isBroadcastable(contentfulEntry)) { return helpers.broadcast.parseBroadcastFromContentfulEntry(contentfulEntry); } - const data = { - id: topicId, - name: contentful.getNameTextFromContentfulEntry(contentfulEntry), - type: contentType, - postType: module.exports.getPostTypeFromContentType(contentType), - }; + + const data = helpers.contentfulEntry.getSummaryFromContentfulEntry(contentfulEntry); + data.postType = module.exports.getPostTypeFromContentType(contentType); const campaignConfigEntry = contentfulEntry.fields.campaign; let promise = Promise.resolve(null); let campaignId; diff --git a/test/lib/lib-helpers/contentfulEntry.test.js b/test/lib/lib-helpers/contentfulEntry.test.js index 53623304..466307ff 100644 --- a/test/lib/lib-helpers/contentfulEntry.test.js +++ b/test/lib/lib-helpers/contentfulEntry.test.js @@ -10,13 +10,17 @@ const sinon = require('sinon'); const contentful = require('../../../lib/contentful'); const stubs = require('../../utils/stubs'); const askYesNoEntryFactory = require('../../utils/factories/contentful/askYesNo'); +const autoReplyFactory = require('../../utils/factories/contentful/autoReply'); +const autoReplyBroadcastFactory = require('../../utils/factories/contentful/autoReplyBroadcast'); +const defaultTopicTriggerFactory = require('../../utils/factories/contentful/defaultTopicTrigger'); +const messageFactory = require('../../utils/factories/contentful/message'); // stubs -const stubEntryDate = Date.now(); -const stubEntry = askYesNoEntryFactory.getValidAskYesNo(stubEntryDate); -const stubEntryId = stubs.getContentfulId(); -const stubContentType = stubs.getTopicContentType(); -const stubNameText = stubs.getBroadcastName(); +const askYesNoEntry = askYesNoEntryFactory.getValidAskYesNo(); +const autoReplyEntry = autoReplyFactory.getValidAutoReply(); +const autoReplyBroadcastEntry = autoReplyBroadcastFactory.getValidAutoReplyBroadcast(); +const defaultTopicTriggerEntry = defaultTopicTriggerFactory.getValidDefaultTopicTrigger(); +const messageEntry = messageFactory.getValidMessage(); // Module to test const contentfulEntryHelper = require('../../../lib/helpers/contentfulEntry'); @@ -24,24 +28,34 @@ const contentfulEntryHelper = require('../../../lib/helpers/contentfulEntry'); chai.should(); chai.use(sinonChai); - const sandbox = sinon.sandbox.create(); -test.beforeEach(() => { - sandbox.stub(contentful, 'getContentfulIdFromContentfulEntry') - .returns(stubEntryId); - sandbox.stub(contentful, 'getContentTypeFromContentfulEntry') - .returns(stubContentType); - sandbox.stub(contentful, 'getNameTextFromContentfulEntry') - .returns(stubNameText); -}); - test.afterEach(() => { sandbox.restore(); }); +// getMessageTemplatesFromContentfulEntry +test('getMessageTemplatesFromContentfulEntry returns an object with templates values if content type config has templates', () => { + const result = contentfulEntryHelper.getMessageTemplatesFromContentfulEntry(autoReplyEntry); + result.autoReply.text.should.equal(autoReplyEntry.fields.autoReply.fields.text); +}); + +test('getMessageTemplatesFromContentfulEntry returns an empty object if content type config does not have templates', () => { + const result = contentfulEntryHelper + .getMessageTemplatesFromContentfulEntry(autoReplyBroadcastEntry); + result.should.deep.equal({}); +}); + // getSummaryFromContentfulEntry test('getSummaryFromContentfulEntry returns an object with name and type properties', () => { + const stubEntryDate = Date.now(); + const stubEntry = askYesNoEntryFactory.getValidAskYesNo(stubEntryDate); + const stubEntryId = stubs.getContentfulId(); + const stubContentType = stubs.getTopicContentType(); + const stubNameText = stubs.getBroadcastName(); + sandbox.stub(contentful, 'getContentTypeFromContentfulEntry') + .returns(stubContentType); + const result = contentfulEntryHelper.getSummaryFromContentfulEntry(stubEntry); result.id.should.equal(stubEntryId); result.type.should.equal(stubContentType); @@ -50,35 +64,26 @@ test('getSummaryFromContentfulEntry returns an object with name and type propert result.updatedAt.should.equal(stubEntryDate); }); -// isDefaultTopicTrigger -test('isDefaultTopicTrigger is truthy if contentful.isContentType', (t) => { - sandbox.stub(contentful, 'isContentType') - .returns(true); - t.truthy(contentfulEntryHelper.isDefaultTopicTrigger(stubEntry)); +// isAutoReply +test('isAutoReply returns whether content type is autoReply', (t) => { + t.falsy(contentfulEntryHelper.isAutoReply(askYesNoEntry)); + t.truthy(contentfulEntryHelper.isAutoReply(autoReplyEntry)); }); -test('isDefaultTopicTrigger is falsy if not contentful.isContentType', (t) => { - sandbox.stub(contentful, 'isContentType') - .returns(false); - t.falsy(contentfulEntryHelper.isDefaultTopicTrigger(stubEntry)); +// isBroadcastable +test('isBroadcastable returns whether content type is broadcastable', (t) => { + t.truthy(contentfulEntryHelper.isBroadcastable(askYesNoEntry)); + t.falsy(contentfulEntryHelper.isBroadcastable(messageEntry)); }); - -// isMessage -test('isMessage is truthy if contentful.isContentType', (t) => { - sandbox.stub(contentful, 'isContentType') - .returns(true); - t.truthy(contentfulEntryHelper.isMessage(stubEntry)); -}); - -test('isMessage is falsy if not contentful.isContentType', (t) => { - sandbox.stub(contentful, 'isContentType') - .returns(false); - t.falsy(contentfulEntryHelper.isMessage(stubEntry)); +// isDefaultTopicTrigger +test('isDefaultTopicTrigger returns whether content type is defaultTopicTrigger', (t) => { + t.truthy(contentfulEntryHelper.isDefaultTopicTrigger(defaultTopicTriggerEntry)); + t.falsy(contentfulEntryHelper.isDefaultTopicTrigger(messageEntry)); }); -test('isMessage returns false if not contentful.isContentType result with args entry and message ', (t) => { - sandbox.stub(contentful, 'isContentType') - .returns(false); - t.falsy(contentfulEntryHelper.isMessage(stubEntry)); +// isMessage +test('isMessage returns whether content type is message', (t) => { + t.truthy(contentfulEntryHelper.isMessage(messageEntry)); + t.falsy(contentfulEntryHelper.isMessage(autoReplyEntry)); }); diff --git a/test/lib/lib-helpers/defaultTopicTrigger.test.js b/test/lib/lib-helpers/defaultTopicTrigger.test.js index 401e9c79..e9bd7223 100644 --- a/test/lib/lib-helpers/defaultTopicTrigger.test.js +++ b/test/lib/lib-helpers/defaultTopicTrigger.test.js @@ -31,10 +31,17 @@ test.afterEach(() => { sandbox.restore(); }); +test.afterEach(() => { + sandbox.restore(); +}); + // fetch test('fetch returns contentful.fetchByContentTypes parsed as defaultTopicTrigger objects', async () => { const entries = [firstEntry, secondEntry]; + const types = ['defaultTopicTrigger']; const fetchEntriesResult = stubs.contentful.getFetchByContentTypesResultWithArray(entries); + sandbox.stub(defaultTopicTriggerHelper, 'getContentTypes') + .returns(types); sandbox.stub(contentful, 'fetchByContentTypes') .returns(Promise.resolve(fetchEntriesResult)); sandbox.stub(defaultTopicTriggerHelper, 'parseDefaultTopicTriggerFromContentfulEntry') @@ -45,7 +52,7 @@ test('fetch returns contentful.fetchByContentTypes parsed as defaultTopicTrigger const result = await defaultTopicTriggerHelper.fetch(); contentful.fetchByContentTypes - .should.have.been.calledWith(config.contentTypes); + .should.have.been.calledWith(types); defaultTopicTriggerHelper.parseDefaultTopicTriggerFromContentfulEntry .should.have.been.calledWith(firstEntry); defaultTopicTriggerHelper.parseDefaultTopicTriggerFromContentfulEntry diff --git a/test/lib/lib-helpers/topic.test.js b/test/lib/lib-helpers/topic.test.js index 20c4b216..d891e4e7 100644 --- a/test/lib/lib-helpers/topic.test.js +++ b/test/lib/lib-helpers/topic.test.js @@ -222,7 +222,8 @@ test('getFieldValueFromContentfulEntryAndTemplateName returns the entry field va // getPostTypeFromContentType test('getPostTypeFromContentType returns postType string property from contentTypeConfig', () => { const result = topicHelper.getPostTypeFromContentType(topicContentType); - result.should.equal(config.contentTypes[topicContentType].postType); + const contentTypeConfigs = helpers.contentfulEntry.getContentTypeConfigs(); + result.should.equal(contentTypeConfigs[topicContentType].postType); }); // parseTopicFromContentfulEntry diff --git a/test/utils/factories/contentful/autoReply.js b/test/utils/factories/contentful/autoReply.js new file mode 100644 index 00000000..6839d8ea --- /dev/null +++ b/test/utils/factories/contentful/autoReply.js @@ -0,0 +1,20 @@ +'use strict'; + +const stubs = require('../../stubs'); +const messageFactory = require('./message'); + +function getValidAutoReply(date = Date.now()) { + const data = { + sys: stubs.contentful.getSysWithTypeAndDate('autoReply', date), + fields: { + name: stubs.getBroadcastName(), + autoReply: messageFactory.getValidMessage(), + // TODO: Add a campaign reference field, returning a campaign factory's valid campaign. + }, + }; + return data; +} + +module.exports = { + getValidAutoReply, +}; diff --git a/test/utils/factories/contentful/autoReplyBroadcast.js b/test/utils/factories/contentful/autoReplyBroadcast.js index e1ebe5df..a30b071c 100644 --- a/test/utils/factories/contentful/autoReplyBroadcast.js +++ b/test/utils/factories/contentful/autoReplyBroadcast.js @@ -2,6 +2,7 @@ const stubs = require('../../stubs'); const messageFactory = require('./message'); +const autoReplyFactory = require('./autoReply'); function getValidAutoReplyBroadcast() { const data = { @@ -15,7 +16,7 @@ function getValidAutoReplyBroadcast() { }, fields: { broadcast: messageFactory.getValidMessage(), - autoReply: messageFactory.getValidMessage(), + topic: autoReplyFactory.getValidAutoReply(), }, }; return data;