From 0bf4cb5ed71d27c48922c24b82802ce3b2b4c70c Mon Sep 17 00:00:00 2001 From: Nick Jaremek Date: Wed, 27 Apr 2016 09:11:36 +0200 Subject: [PATCH 01/10] Added util functions to deal with template files: convert from MJML format to HTML, read files and sanitize placeholders. --- .gitignore | 3 ++ lib/platforms/email.js | 74 ++++++++++++++++++++++++++++++- lib/util/convert_mjml_to_html.js | 15 +++++++ lib/util/process_html_template.js | 26 +++++++++++ lib/util/read_file.js | 18 ++++++++ package.json | 1 + templates/html/template.html | 41 +++++++++++++++++ test/sample_files/template.html | 41 +++++++++++++++++ test/util/read_file.js | 27 +++++++++++ 9 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 lib/util/convert_mjml_to_html.js create mode 100644 lib/util/process_html_template.js create mode 100644 lib/util/read_file.js create mode 100644 templates/html/template.html create mode 100644 test/sample_files/template.html create mode 100644 test/util/read_file.js diff --git a/.gitignore b/.gitignore index f0d0da2..c663035 100755 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ build/Release node_modules config.json npmrc + +# HTML files +html/* \ No newline at end of file diff --git a/lib/platforms/email.js b/lib/platforms/email.js index c1461d8..53196c4 100644 --- a/lib/platforms/email.js +++ b/lib/platforms/email.js @@ -1,9 +1,15 @@ 'use strict'; const config = require('config'); const _ = require('lodash'); +const fs = require('fs'); +const path = require('path'); +const convertMjmlToHtml = require('../util/convert_mjml_to_html'); +const processHtmlTemplate = require('../util/process_html_template'); +const readFile = require('../util/read_file'); const log = require('../util/logger'); const transport = require('../transport/mailgun'); +const PLACEHOLDER_REGEX = /\{\{([^}]+)\}\}/g; const BATCH_LIMIT = config.get('platform_batch_limits.mailgun'); const CUSTOM_BATCH_LIMIT = config.get('transport.mailgun.batch_limit') || BATCH_LIMIT; @@ -17,6 +23,15 @@ function sendEmailNotification(identities, body, callback) { })); content = body.content; + if (content.mjml) { + const readHtml = processHtmlTemplate(content.mjml); + + if (readHtml.error) { + return callback(readHtml.error); + } + content.message = readHtml.html; + } + log.info('Sending request to send emails', emails, content); transport.send(emails, content, function(err, response, body) { @@ -41,7 +56,17 @@ function sendSingleEmail(body, callback) { const emails = body.to; const content = body; content.subject = body.subject; - content.message = content.html; + + if (content.mjml) { + const readHtml = processHtmlTemplate(content.mjml); + + if (readHtml.error) { + return callback(readHtml.error); + } + content.message = readHtml.html; + } else { + content.message = content.html; + } log.info('Sending request to sendSingleEmail', emails, content); @@ -62,6 +87,50 @@ function sendSingleEmail(body, callback) { }); } +function generateHtmlFromMjml(body, callback) { + + const convertedMjml = convertMjmlToHtml(body.mjml); + if (convertedMjml.error) { + return callback(convertedMjml.error); + } + + fs.writeFile(path.join(__dirname, '../../html/' + body.filename), convertedMjml.html, function(err) { + + if (err) { + return callback(err); + } + + return callback(null, {output: 'done'}); + }); +} + +function getListOfHtmlTemplates(callback) { + + fs.readdir(path.join(__dirname, '../../html'), function(err, files) { + if (err) { + return callback(err); + } + + return callback(null, files); + }); +} + +function getHtmlTemplateDetails(filename, callback) { + + const data = readFile(path.join(__dirname, '../../html/' + filename)); + if (data.error) { + return callback(data.error); + } + + const placeholders = data.html.match(PLACEHOLDER_REGEX); + + const res = { + html: data.html, + placeholders: placeholders + }; + + return callback(null, res); +} const emailOptions = { resourceName: 'devices.email', batchLimit: _.min([BATCH_LIMIT, CUSTOM_BATCH_LIMIT]), @@ -71,5 +140,8 @@ const emailOptions = { module.exports = { send: sendEmailNotification, sendSingleEmail: sendSingleEmail, + generateHtmlFromMjml: generateHtmlFromMjml, + getListOfHtmlTemplates:getListOfHtmlTemplates, + getHtmlTemplateDetails: getHtmlTemplateDetails, options: emailOptions }; diff --git a/lib/util/convert_mjml_to_html.js b/lib/util/convert_mjml_to_html.js new file mode 100644 index 0000000..52d344d --- /dev/null +++ b/lib/util/convert_mjml_to_html.js @@ -0,0 +1,15 @@ +'use strict'; +const mjml2html = require('mjml').mjml2html; +const errors = require('./errors'); + +module.exports = function(mjmlBody) { + + const res = {}; + try { + res.html = mjml2html(mjmlBody); + return res; + } catch (err) { + res.error = new errors.BadRequestError(err.message); + return res; + } +}; diff --git a/lib/util/process_html_template.js b/lib/util/process_html_template.js new file mode 100644 index 0000000..98314e8 --- /dev/null +++ b/lib/util/process_html_template.js @@ -0,0 +1,26 @@ +'use strict'; + +const path = require('path'); +const _ = require('lodash'); +const readFile = require('./read_file'); +const generateFilenameWithExt = require('./generate_filename_with_ext'); + +const HTML_TEMPLATES_BASE_PATH = path.join(__dirname, '../../templates/html/'); + +module.exports = function(templateData) { + + let filename = generateFilenameWithExt(templateData.filename, 'html'); + const filepath = path.join(HTML_TEMPLATES_BASE_PATH, filename); + let htmlData = readFile(filepath); + + if (htmlData.html) { + const placeholderData = templateData.placeholders; + const placeholderKeys = _.keys(placeholderData); + + _.each(placeholderKeys, function(placeholderKey) { + htmlData.html = htmlData.html.replace('{{' + placeholderKey + '}}', placeholderData[placeholderKey], 'g'); + }); + } + + return htmlData; +}; diff --git a/lib/util/read_file.js b/lib/util/read_file.js new file mode 100644 index 0000000..a70931d --- /dev/null +++ b/lib/util/read_file.js @@ -0,0 +1,18 @@ +'use strict'; + +const fs = require('fs'); +const errors = require('./errors'); + +module.exports = function(filepath) { + const data = {}; + + try { + data.html = fs.readFileSync(filepath).toString(); + console.log('YOY'); + return data; + } catch (err) { + data.error = new errors.NotFoundError('HTML template not found'); + console.log('ERROR'); + return data; + } +}; diff --git a/package.json b/package.json index ae0bd56..3100256 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "express": "^4.13.3", "lodash": "^3.9.3", "method-override": "^2.3.5", + "mjml": "^1.3.4", "mongoose": "^4.1.8", "node-gcm": "^0.11.0", "request": "^2.58.0", diff --git a/templates/html/template.html b/templates/html/template.html new file mode 100644 index 0000000..e9bb15a --- /dev/null +++ b/templates/html/template.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + +

Hello {{USER}}
+ + \ No newline at end of file diff --git a/test/sample_files/template.html b/test/sample_files/template.html new file mode 100644 index 0000000..e9bb15a --- /dev/null +++ b/test/sample_files/template.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + +

Hello {{USER}}
+ + \ No newline at end of file diff --git a/test/util/read_file.js b/test/util/read_file.js new file mode 100644 index 0000000..d9301bb --- /dev/null +++ b/test/util/read_file.js @@ -0,0 +1,27 @@ +'use strict'; +const readFile = require('./../../lib/util/read_file'); +const expect = require('chai').expect; +const path = require('path'); + +const VALID_HTML_PATH = path.join(__dirname, '../sample_files/template.html'); +const INVALID_HTML_PATH = path.join(__dirname, '../sample_files/no-file.html'); + +describe('Read file: ', function() { + + it('reads the content of the template successfully', function(done) { + const output = readFile(VALID_HTML_PATH); + expect(output.html).to.not.equal(undefined); + expect(output.error).to.equal(undefined); + return done(); + }); + + it('returns an error due to non-existing file or other error', function(done) { + const output = readFile(INVALID_HTML_PATH); + expect(output.html).to.equal(undefined); + expect(output.error).to.have.property('message', 'HTML template not found'); + expect(output.error).to.have.property('statusCode', 404); + expect(output.error).to.have.property('body'); + expect(output.error.body).to.have.property('code', 'NotFoundError'); + return done(); + }); +}); From 06eb9e2f2ffdb58eb66b22179698860d78d378d1 Mon Sep 17 00:00:00 2001 From: Nick Jaremek Date: Wed, 27 Apr 2016 09:12:31 +0200 Subject: [PATCH 02/10] Added logic to generate email templates and fetch them via endpoints. --- lib/platforms/email.js | 65 ++---------------- lib/platforms/template.js | 95 ++++++++++++++++++++++++++ lib/routes/template.js | 72 +++++++++++++++++++ lib/service.js | 2 +- lib/util/generate_filename_with_ext.js | 11 +++ lib/util/process_html_template.js | 4 +- lib/util/read_file.js | 6 +- lib/util/sanitize_placeholders.js | 16 +++++ 8 files changed, 206 insertions(+), 65 deletions(-) create mode 100644 lib/platforms/template.js create mode 100644 lib/routes/template.js create mode 100644 lib/util/generate_filename_with_ext.js create mode 100644 lib/util/sanitize_placeholders.js diff --git a/lib/platforms/email.js b/lib/platforms/email.js index 53196c4..31fafac 100644 --- a/lib/platforms/email.js +++ b/lib/platforms/email.js @@ -1,15 +1,11 @@ 'use strict'; const config = require('config'); const _ = require('lodash'); -const fs = require('fs'); -const path = require('path'); -const convertMjmlToHtml = require('../util/convert_mjml_to_html'); + const processHtmlTemplate = require('../util/process_html_template'); -const readFile = require('../util/read_file'); const log = require('../util/logger'); const transport = require('../transport/mailgun'); -const PLACEHOLDER_REGEX = /\{\{([^}]+)\}\}/g; const BATCH_LIMIT = config.get('platform_batch_limits.mailgun'); const CUSTOM_BATCH_LIMIT = config.get('transport.mailgun.batch_limit') || BATCH_LIMIT; @@ -23,15 +19,15 @@ function sendEmailNotification(identities, body, callback) { })); content = body.content; - if (content.mjml) { - const readHtml = processHtmlTemplate(content.mjml); + if (content.template) { + const readHtml = processHtmlTemplate(content.template); if (readHtml.error) { return callback(readHtml.error); } content.message = readHtml.html; } - + log.info('Sending request to send emails', emails, content); transport.send(emails, content, function(err, response, body) { @@ -57,9 +53,9 @@ function sendSingleEmail(body, callback) { const content = body; content.subject = body.subject; - if (content.mjml) { - const readHtml = processHtmlTemplate(content.mjml); - + if (content.template) { + const readHtml = processHtmlTemplate(content.template); + if (readHtml.error) { return callback(readHtml.error); } @@ -87,50 +83,6 @@ function sendSingleEmail(body, callback) { }); } -function generateHtmlFromMjml(body, callback) { - - const convertedMjml = convertMjmlToHtml(body.mjml); - if (convertedMjml.error) { - return callback(convertedMjml.error); - } - - fs.writeFile(path.join(__dirname, '../../html/' + body.filename), convertedMjml.html, function(err) { - - if (err) { - return callback(err); - } - - return callback(null, {output: 'done'}); - }); -} - -function getListOfHtmlTemplates(callback) { - - fs.readdir(path.join(__dirname, '../../html'), function(err, files) { - if (err) { - return callback(err); - } - - return callback(null, files); - }); -} - -function getHtmlTemplateDetails(filename, callback) { - - const data = readFile(path.join(__dirname, '../../html/' + filename)); - if (data.error) { - return callback(data.error); - } - - const placeholders = data.html.match(PLACEHOLDER_REGEX); - - const res = { - html: data.html, - placeholders: placeholders - }; - - return callback(null, res); -} const emailOptions = { resourceName: 'devices.email', batchLimit: _.min([BATCH_LIMIT, CUSTOM_BATCH_LIMIT]), @@ -140,8 +92,5 @@ const emailOptions = { module.exports = { send: sendEmailNotification, sendSingleEmail: sendSingleEmail, - generateHtmlFromMjml: generateHtmlFromMjml, - getListOfHtmlTemplates:getListOfHtmlTemplates, - getHtmlTemplateDetails: getHtmlTemplateDetails, options: emailOptions }; diff --git a/lib/platforms/template.js b/lib/platforms/template.js new file mode 100644 index 0000000..4c0e3e5 --- /dev/null +++ b/lib/platforms/template.js @@ -0,0 +1,95 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const async = require('async'); +const readFile = require('../util/read_file'); +const convertMjmlToHtml = require('../util/convert_mjml_to_html'); +const generateFilenameWithExt = require('../util/generate_filename_with_ext'); +const sanitizePlaceholders = require('../util/sanitize_placeholders'); + +const PLACEHOLDER_REGEX = /\{\{([^}]+)\}\}/g; +const CONVERTERS = { + 'mjml': generateHtmlFromMjml +}; + +const HTML_TEMPLATES_BASE_PATH = path.join(__dirname, '../../templates/html'); +const ORIGINAL_TEMPLATES_BASE_PATH = path.join(__dirname, '../../templates/originals'); + +function generateHtmlTemplate(data, callback) { + + let generator = CONVERTERS[data.type]; + + generator(data, callback); +} + +function writeToFile(filepath, fileContent, callback) { + + fs.writeFile(filepath, fileContent, function(err) { + if (err) { + return callback(err); + } + + return callback(null, {output: 'done'}); + }); +} + +function generateHtmlFromMjml(data, callback) { + + const convertedMjml = convertMjmlToHtml(data.content); + if (convertedMjml.error) { + return callback(convertedMjml.error); + } + + async.parallel({ + writeOriginal: function(done) { + const originalFilePath = path.join(ORIGINAL_TEMPLATES_BASE_PATH, generateFilenameWithExt(data.filename, data.type)); + writeToFile(originalFilePath, data.content, done); + }, + writeHtml: function(done) { + const htmlFilePath = path.join(HTML_TEMPLATES_BASE_PATH, generateFilenameWithExt(data.filename, 'html')); + writeToFile(htmlFilePath, convertedMjml.html, done); + } + }, function(err) { + if (err) { + return callback(err); + } + + return callback(null, {output: 'done'}); + }); +} + +function getListOfHtmlTemplates(callback) { + + fs.readdir(HTML_TEMPLATES_BASE_PATH, function(err, files) { + if (err) { + return callback(err); + } + + return callback(null, files); + }); +} + +function getTemplateDetails(filename, type, callback) { + + const basepath = (type === 'html') ? HTML_TEMPLATES_BASE_PATH : ORIGINAL_TEMPLATES_BASE_PATH; + const data = readFile(path.join(basepath, generateFilenameWithExt(filename, type))); + + if (data.error) { + return callback(data.error); + } + + const placeholders = data.content.match(PLACEHOLDER_REGEX); + + const res = { + content: data.content, + placeholders: sanitizePlaceholders(placeholders, ['{{', '}}']) + }; + + return callback(null, res); +} + +module.exports = { + generateHtmlTemplate: generateHtmlTemplate, + getListOfHtmlTemplates: getListOfHtmlTemplates, + getTemplateDetails: getTemplateDetails +}; diff --git a/lib/routes/template.js b/lib/routes/template.js new file mode 100644 index 0000000..c308327 --- /dev/null +++ b/lib/routes/template.js @@ -0,0 +1,72 @@ +'use strict'; + +const _ = require('lodash'); +const templatePlatform = require('../platforms/template'); +const errors = require('../util/errors'); + +const VALID_TEMPLATE_TYPES = ['mjml']; +let routes = {}; + +routes.createEmailTemplate = function(req, res, next) { + + const content = req.body.content; + let filename = req.body.filename; + const type = req.body.type; + + if (!content || !filename || !type || !_.includes(VALID_TEMPLATE_TYPES, type)) { + return new errors.BadRequestError('Missing or invalid parameters: content, filename, type'); + } + + const input = { + filename: filename, + content: content, + type: type + }; + + templatePlatform.generateHtmlTemplate(input, function(err, output) { + if (err) { + return next(err); + } + + res.send(output); + }); +}; + +routes.listHtmlTemplates = function(req, res, next) { + + templatePlatform.getListOfHtmlTemplates(function(err, files) { + if (err) { + return next(err); + } + + const fileList = files.map(function(file) { + return file.replace('.html', ''); + }); + + res.send(fileList); + }); +}; + +routes.getTemplateDetails = function(req, res, next) { + + let templateName = req.params.templateName; + let templateType = req.params.type; + + if (!templateName || !templateType) { + return new errors.BadRequestError('Missing template name and/or type'); + } + + templatePlatform.getTemplateDetails(templateName, templateType, function(err, details) { + if (err) { + return next(err); + } + + res.send(details); + }); +}; + +module.exports = function(server) { + server.post('/api/notification/email/template', routes.createEmailTemplate); + server.get('/api/notification/email/template/list', routes.listHtmlTemplates); + server.get('/api/notification/email/template/:templateName.:type', routes.getTemplateDetails); +}; diff --git a/lib/service.js b/lib/service.js index 5b751b5..c11e7b7 100755 --- a/lib/service.js +++ b/lib/service.js @@ -42,7 +42,7 @@ server.use(checkUrl()); server.use(validateIdentity()); /* Routes */ -const routes = ['channel', 'heartbeat', 'identity', 'sms', 'push', 'email']; +const routes = ['channel', 'heartbeat', 'identity', 'sms', 'push', 'email', 'template']; routes.forEach(function loadRoutes(file) { require(ROUTES_FOLDER + file)(server); diff --git a/lib/util/generate_filename_with_ext.js b/lib/util/generate_filename_with_ext.js new file mode 100644 index 0000000..6ef72a3 --- /dev/null +++ b/lib/util/generate_filename_with_ext.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = function(filename, type) { + const extension = type ? '.' + type : ''; + + if (filename.indexOf(extension) === -1) { + return filename + extension; + } + + return filename; +}; diff --git a/lib/util/process_html_template.js b/lib/util/process_html_template.js index 98314e8..9b89c2a 100644 --- a/lib/util/process_html_template.js +++ b/lib/util/process_html_template.js @@ -13,12 +13,12 @@ module.exports = function(templateData) { const filepath = path.join(HTML_TEMPLATES_BASE_PATH, filename); let htmlData = readFile(filepath); - if (htmlData.html) { + if (htmlData.content) { const placeholderData = templateData.placeholders; const placeholderKeys = _.keys(placeholderData); _.each(placeholderKeys, function(placeholderKey) { - htmlData.html = htmlData.html.replace('{{' + placeholderKey + '}}', placeholderData[placeholderKey], 'g'); + htmlData.content = htmlData.content.replace('{{' + placeholderKey + '}}', placeholderData[placeholderKey], 'g'); }); } diff --git a/lib/util/read_file.js b/lib/util/read_file.js index a70931d..b4d7ed9 100644 --- a/lib/util/read_file.js +++ b/lib/util/read_file.js @@ -1,4 +1,4 @@ -'use strict'; +'uhtmlse strict'; const fs = require('fs'); const errors = require('./errors'); @@ -7,12 +7,10 @@ module.exports = function(filepath) { const data = {}; try { - data.html = fs.readFileSync(filepath).toString(); - console.log('YOY'); + data.content = fs.readFileSync(filepath).toString(); return data; } catch (err) { data.error = new errors.NotFoundError('HTML template not found'); - console.log('ERROR'); return data; } }; diff --git a/lib/util/sanitize_placeholders.js b/lib/util/sanitize_placeholders.js new file mode 100644 index 0000000..16f6b8f --- /dev/null +++ b/lib/util/sanitize_placeholders.js @@ -0,0 +1,16 @@ +'use strict'; + +const _ = require('lodash'); +module.exports = function(placeholders, charsToSanitize) { + + const chars = charsToSanitize || []; + let sanitizedPlaceholders = placeholders || []; + + _.each(chars, function(charGroup) { + _.each(sanitizedPlaceholders, function(placeholder, idx) { + sanitizedPlaceholders[idx] = placeholder.replace(charGroup, ''); + }); + }); + + return sanitizedPlaceholders; +}; From 2038d22d30d0375206bc16f4497f6a0853368b31 Mon Sep 17 00:00:00 2001 From: Nick Jaremek Date: Wed, 27 Apr 2016 09:13:05 +0200 Subject: [PATCH 03/10] Added unit tests for email template generation and MJML template conversion logic. --- .gitignore | 4 +- test/platforms/single_email.js | 31 +++++- test/platforms/template.js | 134 ++++++++++++++++++++++++ test/sample_files/Orchestrator.json | 22 +++- test/sample_files/template.html | 40 +------ test/util/convert_mjml_to_html.js | 34 ++++++ test/util/generate_filename_with_ext.js | 21 ++++ test/util/process_html_template.js | 44 ++++++++ test/util/read_file.js | 4 +- test/util/sanitize_placeholders.js | 27 +++++ 10 files changed, 316 insertions(+), 45 deletions(-) create mode 100644 test/platforms/template.js create mode 100644 test/util/convert_mjml_to_html.js create mode 100644 test/util/generate_filename_with_ext.js create mode 100644 test/util/process_html_template.js create mode 100644 test/util/sanitize_placeholders.js diff --git a/.gitignore b/.gitignore index c663035..56dd524 100755 --- a/.gitignore +++ b/.gitignore @@ -30,5 +30,5 @@ node_modules config.json npmrc -# HTML files -html/* \ No newline at end of file +# Email template files +templates/* \ No newline at end of file diff --git a/test/platforms/single_email.js b/test/platforms/single_email.js index a904b6e..5c00ea7 100644 --- a/test/platforms/single_email.js +++ b/test/platforms/single_email.js @@ -20,9 +20,9 @@ describe('Single email', function() { return done(); }); - it('sends a single email to a destination', function(done) { + it('sends a single HTML email to a destination', function(done) { - const requestBody = require(TEST_FILES + 'Orchestrator').single_email; + const requestBody = require(TEST_FILES + 'Orchestrator').html_single_email; emailPlatform.sendSingleEmail(requestBody, function(error, output) { expect(error).to.equal(null); @@ -31,4 +31,31 @@ describe('Single email', function() { return done(); }); }); + + it('sends a single MJML email to a destination', function(done) { + + const requestBody = require(TEST_FILES + 'Orchestrator').mjml_single_email; + + emailPlatform.sendSingleEmail(requestBody, function(error, output) { + expect(error).to.equal(null); + expect(output.response).to.equal('Queued'); + expect(output.body.output).to.equal('Queued notifications'); + return done(); + }); + }); + + it('yields an error due to invalid MJML content', function(done) { + + const requestBody = require(TEST_FILES + 'Orchestrator').invalid_mjml_single_email; + + emailPlatform.sendSingleEmail(requestBody, function(error, output) { + expect(error).to.not.equal(null); + expect(output).to.equal(undefined); + expect(error).to.have.property('message', 'HTML template not found'); + expect(error).to.have.property('statusCode', 404); + expect(error).to.have.property('body'); + expect(error.body).to.have.property('code', 'NotFoundError'); + return done(); + }); + }); }); diff --git a/test/platforms/template.js b/test/platforms/template.js new file mode 100644 index 0000000..9f36dab --- /dev/null +++ b/test/platforms/template.js @@ -0,0 +1,134 @@ +'use strict'; +const sinon = require('sinon'); +const expect = require('chai').expect; +const fs = require('fs'); + +const templatePlatform = require('./../../lib/platforms/template'); +let fsStub = {}; + +const VALID_MJML = 'Hello World'; +const INVALID_MJML = ''; +const FILE_LIST = ['template-1', 'template-2', 'template-3']; + +describe('Template email', function() { + + beforeEach(function(done) { + fsStub.writeFile = sinon.stub(fs, 'writeFile'); + fsStub.writeFile.yields(null); + fsStub.readdir = sinon.stub(fs, 'readdir'); + fsStub.readdir.yields(null, FILE_LIST); + fsStub.readFileSync = sinon.stub(fs, 'readFileSync'); + fsStub.readFileSync.returns(VALID_MJML); + return done(); + }); + + afterEach(function(done) { + fsStub.writeFile.restore(); + fsStub.readdir.restore(); + fsStub.readFileSync.restore(); + return done(); + }); + + it('generates an HTML from an MJML', function(done) { + + const input = { + filename: 'template', + content: VALID_MJML, + type: 'mjml' + }; + + templatePlatform .generateHtmlTemplate(input, function(err, output) { + expect(err).to.equal(null); + expect(output).to.deep.equal({ output: 'done'}); + return done(); + }); + }); + + it('yields an error when generating an HTML from an MJML', function(done) { + + const input = { + filename: 'template', + content: INVALID_MJML, + type: 'mjml' + }; + + templatePlatform .generateHtmlTemplate(input, function(err, output) { + expect(output).to.equal(undefined); + expect(err).to.have.property('message', '[MJMLError] EmptyMJMLError: Null element found in mjmlElementParser'); + expect(err).to.have.property('statusCode', 400); + expect(err).to.have.property('body'); + expect(err.body).to.have.property('code', 'BadRequestError'); + return done(); + }); + }); + + it('yields an error when saving the HTML and MJML files', function(done) { + + const input = { + filename: 'template', + content: VALID_MJML, + type: 'mjml' + }; + + const writeError = new Error('Write error'); + fs.writeFile.yields(writeError); + + templatePlatform .generateHtmlTemplate(input, function(err, output) { + expect(output).to.equal(undefined); + expect(err).to.deep.equal(writeError); + return done(); + }); + }); + + it('returns a list of HTML templates', function(done) { + + templatePlatform.getListOfHtmlTemplates(function(err, list) { + expect(err).to.equal(null); + expect(list).to.be.an('array'); + expect(list).to.deep.equal(FILE_LIST); + return done(); + }); + }); + + it('yields an error when failing to read the HTML templates directory', function(done) { + + const readError = new Error('Read error'); + fsStub.readdir.yields(readError); + + templatePlatform.getListOfHtmlTemplates(function(err, list) { + expect(list).to.equal(undefined); + expect(err).to.deep.equal(readError); + return done(); + }); + }); + + it('returns the details associated to an HTML template', function(done) { + + const templateName = 'template'; + const templateType = 'mjml'; + templatePlatform.getTemplateDetails(templateName, templateType, function(err, details) { + expect(err).to.equal(null); + expect(details).to.have.property('content'); + expect(details.content).to.equal(VALID_MJML); + expect(details).to.have.property('placeholders'); + expect(details.placeholders).to.deep.equal([]); + return done(); + }); + }); + + it('fails because of not found HTML template', function(done) { + const templateName = 'no-file'; + const templateType = 'mjml'; + + fsStub.readFileSync.throws(new Error('Not Found')); + + templatePlatform.getTemplateDetails(templateName, templateType, function(err, details) { + expect(details).to.equal(undefined); + expect(err).to.have.property('message', 'HTML template not found'); + expect(err).to.have.property('statusCode', 404); + expect(err).to.have.property('body'); + expect(err.body).to.have.property('code', 'NotFoundError'); + return done(); + }); + }); +}); diff --git a/test/sample_files/Orchestrator.json b/test/sample_files/Orchestrator.json index 7c2b7f4..3ddd2e5 100644 --- a/test/sample_files/Orchestrator.json +++ b/test/sample_files/Orchestrator.json @@ -15,9 +15,29 @@ "message": "Hola holita RESONATOR" } }, - "single_email": { + "html_single_email": { "to": "mac@into.sh", "html": "Echo echo echo!", "subject": "Testing" + }, + "mjml_single_email": { + "to": "mac@into.sh", + "template": { + "filename": "template.html", + "placeholders": { + "USER": "Mr. Invent" + } + }, + "subject": "Testing" + }, + "invalid_mjml_single_email": { + "to": "mac@into.sh", + "template": { + "filename": "no-file.html", + "placeholders": { + "USER": "Mr. Invent" + } + }, + "subject": "Testing" } } \ No newline at end of file diff --git a/test/sample_files/template.html b/test/sample_files/template.html index e9bb15a..66cfdf1 100644 --- a/test/sample_files/template.html +++ b/test/sample_files/template.html @@ -1,41 +1,5 @@ - - - - - - - - - - - + -

Hello {{USER}}
+

Hello {{USER}}!

\ No newline at end of file diff --git a/test/util/convert_mjml_to_html.js b/test/util/convert_mjml_to_html.js new file mode 100644 index 0000000..82a3afe --- /dev/null +++ b/test/util/convert_mjml_to_html.js @@ -0,0 +1,34 @@ +'use strict'; +const convertMjmlToHtml = require('./../../lib/util/convert_mjml_to_html'); +const expect = require('chai').expect; + +const VALID_MJML = 'Hello World'; +const INVALID_MJML = ''; + +describe('Convert MJML to HTML: ', function() { + + it('converts an input MJML file to an HTML equivalent', function(done) { + + const input = VALID_MJML; + + const htmlOutput = convertMjmlToHtml(input); + expect(htmlOutput).to.not.have.property('error'); + expect(htmlOutput).to.have.property('html'); + expect(htmlOutput.html).to.not.equal(undefined); + return done(); + }); + + it('converts an input MJML file to an HTML equivalent', function(done) { + + const input = INVALID_MJML; + + const htmlOutput = convertMjmlToHtml(input); + expect(htmlOutput).to.not.have.property('html'); + expect(htmlOutput).to.have.property('error'); + expect(htmlOutput.error).to.have.property('message', '[MJMLError] EmptyMJMLError: Null element found in mjmlElementParser'); + expect(htmlOutput.error).to.have.property('statusCode', 400); + expect(htmlOutput.error).to.have.property('body'); + expect(htmlOutput.error.body).to.have.property('code', 'BadRequestError'); + return done(); + }); +}); diff --git a/test/util/generate_filename_with_ext.js b/test/util/generate_filename_with_ext.js new file mode 100644 index 0000000..a5ac820 --- /dev/null +++ b/test/util/generate_filename_with_ext.js @@ -0,0 +1,21 @@ +'use strict'; +const generateFilenameWithExt = require('./../../lib/util/generate_filename_with_ext'); +const expect = require('chai').expect; + +const FILENAME_WITHOUT_EXTENSION = 'template'; +const FILENAME_WITH_EXTENSION = 'template.html'; + +describe('Generate filename with extension: ', function() { + + it('should return the filename with the \'.html\' extension appended', function(done) { + const outFilename = generateFilenameWithExt(FILENAME_WITHOUT_EXTENSION, 'html'); + expect(outFilename).to.equal(FILENAME_WITH_EXTENSION); + return done(); + }); + + it('should return the same filename without modifications', function(done) { + const outFilename = generateFilenameWithExt(FILENAME_WITH_EXTENSION, ''); + expect(outFilename).to.equal(FILENAME_WITH_EXTENSION); + return done(); + }); +}); diff --git a/test/util/process_html_template.js b/test/util/process_html_template.js new file mode 100644 index 0000000..4b91acb --- /dev/null +++ b/test/util/process_html_template.js @@ -0,0 +1,44 @@ +'use strict'; +const processHtmlTemplate = require('./../../lib/util/process_html_template'); +const expect = require('chai').expect; + +const EXISTING_HTML_TEMPLATE = 'template.html'; +const NON_EXISTING_HTML_TEMPLATE = 'no-file.html'; + +describe('Process HTML template: ', function() { + + it('returns HTML data with the placeholders replaced', function(done) { + + const input = { + filename: EXISTING_HTML_TEMPLATE, + placeholders: { + 'USER': 'Mr. Invent' + } + }; + + const processedHtml = processHtmlTemplate(input); + expect(processedHtml).to.have.property('content'); + expect(processedHtml.content.indexOf(input.placeholders.USER)).to.not.equal(-1); + expect(processedHtml).to.not.have.property('error'); + return done(); + }); + + it('returns an error due to non existing filename', function(done) { + + const input = { + filename: NON_EXISTING_HTML_TEMPLATE, + placeholders: { + 'USER': 'Mr. Invent' + } + }; + + const processedHtml = processHtmlTemplate(input); + expect(processedHtml).to.not.have.property('content'); + expect(processedHtml).to.have.property('error'); + expect(processedHtml.error).to.have.property('message', 'HTML template not found'); + expect(processedHtml.error).to.have.property('statusCode', 404); + expect(processedHtml.error).to.have.property('body'); + expect(processedHtml.error.body).to.have.property('code', 'NotFoundError'); + return done(); + }); +}); diff --git a/test/util/read_file.js b/test/util/read_file.js index d9301bb..b3b7cfb 100644 --- a/test/util/read_file.js +++ b/test/util/read_file.js @@ -10,14 +10,14 @@ describe('Read file: ', function() { it('reads the content of the template successfully', function(done) { const output = readFile(VALID_HTML_PATH); - expect(output.html).to.not.equal(undefined); + expect(output.content).to.not.equal(undefined); expect(output.error).to.equal(undefined); return done(); }); it('returns an error due to non-existing file or other error', function(done) { const output = readFile(INVALID_HTML_PATH); - expect(output.html).to.equal(undefined); + expect(output.content).to.equal(undefined); expect(output.error).to.have.property('message', 'HTML template not found'); expect(output.error).to.have.property('statusCode', 404); expect(output.error).to.have.property('body'); diff --git a/test/util/sanitize_placeholders.js b/test/util/sanitize_placeholders.js new file mode 100644 index 0000000..a0bead5 --- /dev/null +++ b/test/util/sanitize_placeholders.js @@ -0,0 +1,27 @@ +'use strict'; +const sanitizePlaceholders = require('./../../lib/util/sanitize_placeholders'); +const expect = require('chai').expect; + +describe('Sanitize placeholders: ', function() { + + it('returns an array of strings with {{ and }} removed', function(done) { + + const placeholders = ['{{USER}}', '{{SUBJECT}}']; + const sanitizedPlaceholders = sanitizePlaceholders(placeholders, ['{{', '}}']); + expect(sanitizedPlaceholders).to.deep.equal(['USER', 'SUBJECT']); + return done(); + }); + + it('returns the same placeholders whe no sanitization characters are specified', function(done) { + const placeholders = ['{{USER}}', '{{SUBJECT}}']; + const sanitizedPlaceholders = sanitizePlaceholders(placeholders); + expect(sanitizedPlaceholders).to.deep.equal(placeholders); + return done(); + }); + + it('returns an empty array when no placeholders are passed', function(done) { + const sanitizedPlaceholders = sanitizePlaceholders(undefined, ['{{', '}}']); + expect(sanitizedPlaceholders).to.deep.equal([]); + return done(); + }); +}); From f472b16bf668ffbdbfc9632f2910feaa5c8d238b Mon Sep 17 00:00:00 2001 From: Nick Jaremek Date: Wed, 27 Apr 2016 13:12:39 +0200 Subject: [PATCH 04/10] Added Cucumber tests using Sinon stubs to test MJML template sending. --- .../email_features/email_send.feature | 27 +++++++++-- .../features/step_definitions/email_steps.js | 47 +++++++++++++++++-- .../batch/invalid_custom_template_email.json | 15 ++++++ .../batch/valid_custom_template_email.json | 17 +++++++ .../test_files/email/invalid_template.json | 10 ++++ .../invalid_template_email_response.json | 3 ++ .../invalid_single_custom_template_email.json | 9 ++++ .../valid_single_custom_template_email.json | 11 +++++ cucumber/test_files/email/valid_template.json | 5 ++ lib/middleware/check_email.js | 9 ++-- lib/middleware/check_single_email.js | 9 ++-- lib/platforms/email.js | 5 +- lib/platforms/template.js | 4 +- lib/util/{read_file.js => file_handler.js} | 6 ++- lib/util/process_html_template.js | 4 +- templates/html/template.html | 41 ---------------- test/middlewares/check_email.js | 28 +++++++++-- test/middlewares/check_single_email.js | 4 +- test/platforms/single_email.js | 9 +++- test/sample_files/Orchestrator.json | 4 +- test/util/{read_file.js => file_handler.js} | 6 +-- test/util/process_html_template.js | 8 +++- 22 files changed, 207 insertions(+), 74 deletions(-) create mode 100644 cucumber/test_files/email/batch/invalid_custom_template_email.json create mode 100644 cucumber/test_files/email/batch/valid_custom_template_email.json create mode 100644 cucumber/test_files/email/invalid_template.json create mode 100644 cucumber/test_files/email/invalid_template_email_response.json create mode 100644 cucumber/test_files/email/single/invalid_single_custom_template_email.json create mode 100644 cucumber/test_files/email/single/valid_single_custom_template_email.json create mode 100644 cucumber/test_files/email/valid_template.json rename lib/util/{read_file.js => file_handler.js} (80%) delete mode 100644 templates/html/template.html rename test/util/{read_file.js => file_handler.js} (83%) diff --git a/cucumber/features/email_features/email_send.feature b/cucumber/features/email_features/email_send.feature index 1311403..e9b857f 100644 --- a/cucumber/features/email_features/email_send.feature +++ b/cucumber/features/email_features/email_send.feature @@ -1,11 +1,11 @@ Feature: the server receives a request to send an email - Scenario Outline: send batch Email to an identity objects + Scenario Outline: send in-body HTML batch Email to identity objects Given an authenticated identity in the app with Then a request is sent to to send an email and returns Examples: - | identity_id | endpoint | email | response | + | identity_id | endpoint | email | response | | 01f0000000000000003f0001 | /api/notification/email | email/batch/valid_email_over_batch_limit.json | email/valid_email_response.json | | 01f0000000000000003f0001 | /api/notification/email | email/batch/valid_email_under_batch_limit.json | email/valid_email_response.json | | 01f0000000000000003f0002 | /api/notification/email | email/batch/invalid_to_email.json | email/invalid_to_email_response.json | @@ -13,7 +13,17 @@ Feature: the server receives a request to send an email | 01f0000000000000003f0001 | /api/notification/email | email/batch/empty_email.json | email/empty_email_response.json | | 01f0000000000000003f0002 | /api/notification/email | email/batch/missing_email_message.json | email/missing_email_message_response.json | - Scenario Outline: send email to a single email address + @only + Scenario Outline: send custom HTML template batch Email to identity objects + Given an authenticated identity in the app with + Then a request is sent to to send a custom email template