diff --git a/.gitignore b/.gitignore
index f0d0da2..56dd524 100755
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,6 @@ build/Release
node_modules
config.json
npmrc
+
+# Email template files
+templates/*
\ No newline at end of file
diff --git a/apiary.apib b/apiary.apib
index d5c6300..dee8b50 100755
--- a/apiary.apib
+++ b/apiary.apib
@@ -568,6 +568,90 @@ __optionals__ | String | Optional | Object containing all send options (onlyGcm
}
+# Group Templates
+
+## Create email template [/api/notification/template]
+
+### Create an HTML email template [POST]
+Generates a new HTML email template from a non-HTML input.
+
+**Currently supported formats are .mjml files, using the format specified by [MJML](https://mjml.io/)**
+
+#### Attributes for the json body parameter
+
+Name | Type | Required | Description
+--- | --- | --- | ---
+__filename__ | String | Required | Name of the email template to be generated
+__content__ | String | Required | Input text that will be transformed into HTML
+__type__ | String | Required | The input type. Currently can be ´mjml´ **only**
+
++ Request (application/json; charset=utf-8)
+
+ + Body
+
+ {
+ "filename": "custom-template",
+ "content": "Hello World!",
+ "type": "mjml"
+ }
+
++ Response 200
+
+ {
+ "output": "done"
+ }
+
++ Response 400 (application/json; charset=utf-8)
+
+ {
+ "code": "BadRequestError",
+ "message": "Missing or invalid parameters: content, filename, type"
+ }
+
+## Email template listing [/api/notification/template/list]
+
+### Fetch list of email templates [GET]
+Returns a list of the email template filenames currently available
+
++ Request (application/json; charset=utf-8)
+
+ + Body
+
+ {}
+
++ Response 200
+
+ [
+ {
+ "name": "my-template-1",
+ "type": "html",
+ },
+ {
+ "name": "my-template-2",
+ "type": "html"
+ }
+ ]
+
+## Email template details [/api/notification/template/details/:templateName.:type]
+
+### Fetch email template details [GET]
+Returns a the content of an email template (either HTML or other) and the placeholders it has in its content.
+
++ Request (application/json; charset=utf-8)
+
++ Response 200
+
+ {
+ "html": "Hello World!",
+ "placeholders": ["USER"]
+ }
+
++ Response 404
+
+ {
+ "code": "NotFoundError",
+ "message": "Template not found"
+ }
# Data Structures
diff --git a/cucumber/features/email_features/email_send.feature b/cucumber/features/email_features/email_send.feature
index 1311403..9393c24 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,16 @@ 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
+ 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 for and returns
+
+ Examples:
+ | identity_id | endpoint | template | email | response |
+ | 01f0000000000000003f0001 | /api/notification/email | email/successful_template_read.json | email/batch/valid_custom_template_email.json | email/valid_email_response.json |
+ | 01f0000000000000003f0002 | /api/notification/email | email/errored_template_read.json | email/batch/invalid_custom_template_email.json | email/invalid_template_email_response.json |
+
+ Scenario Outline: send in-body HTML email to a single email address
Given an authenticated identity in the app with
Then a request is sent to to send an email and returns
@@ -23,4 +32,13 @@ Feature: the server receives a request to send an email
| 01f0000000000000003f0002 | /api/notification/singleEmail | email/single/invalid_to_single_email.json | email/invalid_to_email_response.json |
| 01f0000000000000003f0003 | /api/notification/singleEmail | email/single/missing_from_single_email.json | email/valid_email_response.json |
| 01f0000000000000003f0001 | /api/notification/singleEmail | email/single/empty_single_email.json | email/empty_email_response.json |
- | 01f0000000000000003f0002 | /api/notification/singleEmail | email/single/missing_single_email_message.json | email/missing_email_message_response.json |
\ No newline at end of file
+ | 01f0000000000000003f0002 | /api/notification/singleEmail | email/single/missing_single_email_message.json | email/missing_email_message_response.json |
+
+ Scenario Outline: send custom HTML template email to a single email address
+ Given an authenticated identity in the app with
+ Then a request is sent to to send a custom email template for and returns
+
+ Examples:
+ | identity_id | endpoint | template | email | response |
+ | 01f0000000000000003f0001 | /api/notification/singleEmail | email/successful_template_read.json | email/single/valid_custom_template_email.json | email/valid_email_response.json |
+ | 01f0000000000000003f0002 | /api/notification/singleEmail | email/errored_template_read.json | email/single/invalid_custom_template_email.json | email/invalid_template_email_response.json |
diff --git a/cucumber/features/step_definitions/email_steps.js b/cucumber/features/step_definitions/email_steps.js
index 3214280..0552b0a 100644
--- a/cucumber/features/step_definitions/email_steps.js
+++ b/cucumber/features/step_definitions/email_steps.js
@@ -1,9 +1,17 @@
'use strict';
const config = require('config');
const nock = require('nock');
+const sinon = require('sinon');
+const fileHandler = require('../../../lib/util/file_handler');
+const errors = require('../../../lib/util/errors');
+let fileHandlerStub;
nock.disableNetConnect();
+
+const MAILGUN_MESSAGES_BASE_URL = 'https://api.mailgun.net';
+const MAILGUN_MESSAGES_ENDPOINT_URL = '/v3/' + config.get('transport.mailgun.domain') + '/messages';
+
module.exports = function() {
this.World = require('../support/world').World;
@@ -14,9 +22,6 @@ module.exports = function() {
let emailObj = _this.readJSONResource(email);
let res = _this.readJSONResource(response);
- const MAILGUN_MESSAGES_BASE_URL = 'https://api.mailgun.net';
- const MAILGUN_MESSAGES_ENDPOINT_URL = '/v3/' + config.get('transport.mailgun.domain') + '/messages';
-
nock(MAILGUN_MESSAGES_BASE_URL)
.persist() // Required since multiple requests can be made in parallel from the platform
.post(MAILGUN_MESSAGES_ENDPOINT_URL)
@@ -31,4 +36,40 @@ module.exports = function() {
.expect(res.status)
.end(callback);
});
+
+ this.Then(/^a request is sent to (.*) to send a custom email template (.*) for (.*) and returns (.*)$/, function(endpoint, templateFile, email, response, callback) {
+ let _this = this;
+
+ let template = _this.readJSONResource(templateFile);
+ let emailObj = _this.readJSONResource(email);
+ let res = _this.readJSONResource(response);
+ fileHandlerStub = sinon.stub(fileHandler, 'readFile');
+
+ if (template.success) {
+ fileHandlerStub.returns(template.success);
+ } else {
+ fileHandlerStub.returns({error: new errors.NotFoundError(template.error.message)});
+ }
+
+ nock(MAILGUN_MESSAGES_BASE_URL)
+ .persist() // Required since multiple requests can be made in parallel from the platform
+ .post(MAILGUN_MESSAGES_ENDPOINT_URL)
+ .reply(200, res.data);
+
+ let request = this.buildRequest('POST', endpoint, {
+ 'x-user-id': this.get('identity')
+ });
+
+ request
+ .send(emailObj)
+ .expect(res.status)
+ .end(function(err) {
+ fileHandlerStub.restore();
+ if (err) {
+ return callback(err);
+ }
+
+ return callback();
+ });
+ });
};
diff --git a/cucumber/features/step_definitions/template_steps.js b/cucumber/features/step_definitions/template_steps.js
new file mode 100644
index 0000000..f86eb10
--- /dev/null
+++ b/cucumber/features/step_definitions/template_steps.js
@@ -0,0 +1,90 @@
+'use strict';
+const expect = require('chai').expect;
+const sinon = require('sinon');
+const templatePlatform = require('../../../lib/platforms/template');
+
+let templatePlatformStub = {};
+
+module.exports = function() {
+
+ this.World = require('../support/world').World;
+
+ this.Then(/^a template object (.*) is sent to (.*) yielding (.*)/, function(requestBody, endpoint, response, callback) {
+ let _this = this;
+
+ let requestBodyObj = _this.readJSONResource(requestBody);
+ let res = _this.readJSONResource(response);
+
+ templatePlatformStub.generateHtmlTemplate = sinon.stub(templatePlatform, 'generateHtmlTemplate');
+ templatePlatformStub.generateHtmlTemplate.yields(res.stubbed.error, res.stubbed.output);
+
+ let request = this.buildRequest('POST', endpoint, {
+ 'x-user-id': this.get('identity')
+ });
+
+ request
+ .send(requestBodyObj)
+ .expect(res.result.status)
+ .end(function(err, output) {
+ templatePlatformStub.generateHtmlTemplate.restore();
+ if (err) {
+ return callback(err);
+ }
+
+ expect(output.body).to.deep.equal(res.result.body);
+ return callback();
+ });
+ });
+
+ this.Then(/^a request for template listing is sent to (.*) yielding (.*)$/, function(endpoint, response, callback) {
+ let _this = this;
+
+ let res = _this.readJSONResource(response);
+
+ templatePlatformStub.getListOfHtmlTemplates = sinon.stub(templatePlatform, 'getListOfHtmlTemplates');
+ templatePlatformStub.getListOfHtmlTemplates.yields(res.stubbed.error, res.stubbed.output);
+
+ let request = this.buildRequest('GET', endpoint, {
+ 'x-user-id': this.get('identity')
+ });
+
+ request
+ .send()
+ .expect(res.result.status)
+ .end(function(err, output) {
+ templatePlatformStub.getListOfHtmlTemplates.restore();
+ if (err) {
+ return callback(err);
+ }
+
+ expect(output.body).to.deep.equal(res.result.body);
+ return callback();
+ });
+ });
+
+ this.Then(/^a request for template details is sent to (.*) yielding (.*)$/, function(endpoint, response, callback) {
+ let _this = this;
+
+ let res = _this.readJSONResource(response);
+
+ templatePlatformStub.getTemplateDetails = sinon.stub(templatePlatform, 'getTemplateDetails');
+ templatePlatformStub.getTemplateDetails.yields(res.stubbed.error, res.stubbed.output);
+
+ let request = this.buildRequest('GET', endpoint, {
+ 'x-user-id': this.get('identity')
+ });
+
+ request
+ .send()
+ .expect(res.result.status)
+ .end(function(err, output) {
+ templatePlatformStub.getTemplateDetails.restore();
+ if (err) {
+ return callback(err);
+ }
+
+ expect(output.body).to.deep.equal(res.result.body);
+ return callback();
+ });
+ });
+};
diff --git a/cucumber/features/template_features/template_create.feature b/cucumber/features/template_features/template_create.feature
new file mode 100644
index 0000000..14c8b0c
--- /dev/null
+++ b/cucumber/features/template_features/template_create.feature
@@ -0,0 +1,10 @@
+Feature: the server receives a request to create a new email template
+
+ Scenario Outline: generate an HTML email template based on the request item
+ Given an authenticated identity in the app with
+ Then a template object is sent to yielding
+
+ Examples:
+ | identity_id | endpoint | body | response |
+ | 01f0000000000000003f0001 | /api/notification/template | template/valid_template_creation_body.json | template/valid_template_creation_response.js |
+ | 01f0000000000000003f0001 | /api/notification/template | template/invalid_template_creation_body.json | template/invalid_template_creation_response.js |
diff --git a/cucumber/features/template_features/template_details.feature b/cucumber/features/template_features/template_details.feature
new file mode 100644
index 0000000..8eda3a0
--- /dev/null
+++ b/cucumber/features/template_features/template_details.feature
@@ -0,0 +1,11 @@
+Feature: the server receives a request to send a the details of a given email template
+
+ Scenario Outline: return successfully the details associated to a given email template
+ Given an authenticated identity in the app with
+ Then a request for template details is sent to yielding
+
+ Examples:
+ | identity_id | endpoint | response |
+ | 01f0000000000000003f0001 | /api/notification/template/details/some-template.html | template/valid_template_details_response.js |
+ | 01f0000000000000003f0001 | /api/notification/template/details/no-file.html | template/invalid_template_details_response.js |
+ | 01f0000000000000003f0001 | /api/notification/template/details/invalid-format | template/missing_type_template_details_response.js |
diff --git a/cucumber/features/template_features/template_list.feature b/cucumber/features/template_features/template_list.feature
new file mode 100644
index 0000000..5c6fab2
--- /dev/null
+++ b/cucumber/features/template_features/template_list.feature
@@ -0,0 +1,12 @@
+
+Feature: the server receives a request to send a list of the available email templates
+
+ Scenario Outline: list successfully the names of the generated email templates
+ Given an authenticated identity in the app with
+ Then a request for template listing is sent to yielding
+
+ Examples:
+ | identity_id | endpoint | response |
+ | 01f0000000000000003f0001 | /api/notification/template/list | template/valid_template_list_response.js |
+ | 01f0000000000000003f0001 | /api/notification/template/list | template/invalid_template_list_response.js |
+
diff --git a/cucumber/test_files/email/batch/invalid_custom_template_email.json b/cucumber/test_files/email/batch/invalid_custom_template_email.json
new file mode 100644
index 0000000..68edb30
--- /dev/null
+++ b/cucumber/test_files/email/batch/invalid_custom_template_email.json
@@ -0,0 +1,15 @@
+{
+ "identities": ["01f0000000000000003f0002"],
+ "channels": ["buddies"],
+ "content": {
+ "to": "john@doe.com",
+ "from": "mac@into.sh",
+ "template": {
+ "filename": "no-file",
+ "placeholders": {}
+ },
+ "subject": "Testing"
+ }
+}
+
+
diff --git a/cucumber/test_files/email/batch/valid_custom_template_email.json b/cucumber/test_files/email/batch/valid_custom_template_email.json
new file mode 100644
index 0000000..c4e5573
--- /dev/null
+++ b/cucumber/test_files/email/batch/valid_custom_template_email.json
@@ -0,0 +1,17 @@
+{
+ "identities": ["01f0000000000000003f0002"],
+ "channels": ["buddies"],
+ "content": {
+ "to": "john@doe.com",
+ "from": "mac@into.sh",
+ "template": {
+ "filename": "template",
+ "placeholders": {
+ "USER": "Mr. Invent"
+ }
+ },
+ "subject": "Testing"
+ }
+}
+
+
diff --git a/cucumber/test_files/email/errored_template_read.json b/cucumber/test_files/email/errored_template_read.json
new file mode 100644
index 0000000..b4f267f
--- /dev/null
+++ b/cucumber/test_files/email/errored_template_read.json
@@ -0,0 +1,10 @@
+{
+ "error": {
+ "message": "Template not found",
+ "statusCode": 404,
+ "body": {
+ "code": "NotFoundError",
+ "message": "Template not found"
+ }
+ }
+}
\ No newline at end of file
diff --git a/cucumber/test_files/email/invalid_template_email_response.json b/cucumber/test_files/email/invalid_template_email_response.json
new file mode 100644
index 0000000..d1156c0
--- /dev/null
+++ b/cucumber/test_files/email/invalid_template_email_response.json
@@ -0,0 +1,3 @@
+{
+ "status": 404
+}
\ No newline at end of file
diff --git a/cucumber/test_files/email/single/invalid_custom_template_email.json b/cucumber/test_files/email/single/invalid_custom_template_email.json
new file mode 100644
index 0000000..e7d8617
--- /dev/null
+++ b/cucumber/test_files/email/single/invalid_custom_template_email.json
@@ -0,0 +1,9 @@
+{
+ "to": "john@doe.com",
+ "from": "mac@into.sh",
+ "template": {
+ "filename": "no-file",
+ "placeholders": {}
+ },
+ "subject": "Testing"
+}
\ No newline at end of file
diff --git a/cucumber/test_files/email/single/valid_custom_template_email.json b/cucumber/test_files/email/single/valid_custom_template_email.json
new file mode 100644
index 0000000..e0c0588
--- /dev/null
+++ b/cucumber/test_files/email/single/valid_custom_template_email.json
@@ -0,0 +1,11 @@
+{
+ "to": "john@doe.com",
+ "from": "mac@into.sh",
+ "template": {
+ "filename": "template",
+ "placeholders": {
+ "USER": "Mr. Invent"
+ }
+ },
+ "subject": "Testing"
+}
\ No newline at end of file
diff --git a/cucumber/test_files/email/successful_template_read.json b/cucumber/test_files/email/successful_template_read.json
new file mode 100644
index 0000000..32798a6
--- /dev/null
+++ b/cucumber/test_files/email/successful_template_read.json
@@ -0,0 +1,5 @@
+{
+ "success": {
+ "content": "Hello {{USER}}
"
+ }
+}
\ No newline at end of file
diff --git a/cucumber/test_files/template/invalid_template_creation_body.json b/cucumber/test_files/template/invalid_template_creation_body.json
new file mode 100644
index 0000000..13c4c3e
--- /dev/null
+++ b/cucumber/test_files/template/invalid_template_creation_body.json
@@ -0,0 +1,5 @@
+{
+ "filename": "custom-template",
+ "content": "",
+ "type": "invent"
+}
\ No newline at end of file
diff --git a/cucumber/test_files/template/invalid_template_creation_response.js b/cucumber/test_files/template/invalid_template_creation_response.js
new file mode 100644
index 0000000..62135a6
--- /dev/null
+++ b/cucumber/test_files/template/invalid_template_creation_response.js
@@ -0,0 +1,17 @@
+'use strict';
+
+const errors = require('../../../lib/util/errors');
+
+module.exports = {
+ stubbed: {
+ error: new errors.BadRequestError('Missing or invalid parameters: content, filename, type'),
+ output: undefined
+ },
+ result: {
+ status: 400,
+ body: {
+ code: 'BadRequestError',
+ message: 'Missing or invalid parameters: content, filename, type'
+ }
+ }
+};
diff --git a/cucumber/test_files/template/invalid_template_details_response.js b/cucumber/test_files/template/invalid_template_details_response.js
new file mode 100644
index 0000000..a058acc
--- /dev/null
+++ b/cucumber/test_files/template/invalid_template_details_response.js
@@ -0,0 +1,18 @@
+'use strict';
+
+module.exports = {
+ stubbed: {
+ error: {
+ code: 'InternalError',
+ message: 'Read error'
+ },
+ output: undefined
+ },
+ result: {
+ status: 500,
+ body: {
+ code: 'InternalError',
+ message: 'Read error'
+ }
+ }
+};
diff --git a/cucumber/test_files/template/invalid_template_list_response.js b/cucumber/test_files/template/invalid_template_list_response.js
new file mode 100644
index 0000000..4c3e6c5
--- /dev/null
+++ b/cucumber/test_files/template/invalid_template_list_response.js
@@ -0,0 +1,20 @@
+'use strict';
+
+const errors = require('../../../lib/util/errors');
+
+module.exports = {
+ stubbed: {
+ error: {
+ code: 'InternalError',
+ message: 'Read error'
+ },
+ output: undefined
+ },
+ result: {
+ status: 500,
+ body: {
+ code: 'InternalError',
+ message: 'Read error'
+ }
+ }
+};
diff --git a/cucumber/test_files/template/missing_type_template_details_response.js b/cucumber/test_files/template/missing_type_template_details_response.js
new file mode 100644
index 0000000..6043683
--- /dev/null
+++ b/cucumber/test_files/template/missing_type_template_details_response.js
@@ -0,0 +1,17 @@
+'use strict';
+
+const errors = require('../../../lib/util/errors');
+
+module.exports = {
+ stubbed: {
+ error: new errors.BadRequestError('Invalid template filename: must have the . format'),
+ output: undefined
+ },
+ result: {
+ status: 400,
+ body: {
+ code: 'BadRequestError',
+ message: 'Invalid template filename: must have the . format'
+ }
+ }
+};
diff --git a/cucumber/test_files/template/valid_template_creation_body.json b/cucumber/test_files/template/valid_template_creation_body.json
new file mode 100644
index 0000000..6e11a50
--- /dev/null
+++ b/cucumber/test_files/template/valid_template_creation_body.json
@@ -0,0 +1,5 @@
+{
+ "filename": "custom-template",
+ "content": "Greetings, {{USER}}!",
+ "type": "mjml"
+}
\ No newline at end of file
diff --git a/cucumber/test_files/template/valid_template_creation_response.js b/cucumber/test_files/template/valid_template_creation_response.js
new file mode 100644
index 0000000..b811d8b
--- /dev/null
+++ b/cucumber/test_files/template/valid_template_creation_response.js
@@ -0,0 +1,16 @@
+'use strict';
+
+module.exports = {
+ stubbed: {
+ error: null,
+ output: {
+ output: 'done'
+ }
+ },
+ result: {
+ status: 200,
+ body: {
+ output: 'done'
+ }
+ }
+};
diff --git a/cucumber/test_files/template/valid_template_details_response.js b/cucumber/test_files/template/valid_template_details_response.js
new file mode 100644
index 0000000..0d483d7
--- /dev/null
+++ b/cucumber/test_files/template/valid_template_details_response.js
@@ -0,0 +1,20 @@
+'use strict';
+
+'use strict';
+
+module.exports = {
+ stubbed: {
+ error: null,
+ output: {
+ content: 'Hello, {{USER}}!
',
+ placeholders: ['USER']
+ }
+ },
+ result: {
+ status: 200,
+ body: {
+ content: 'Hello, {{USER}}!
',
+ placeholders: ['USER']
+ }
+ }
+};
diff --git a/cucumber/test_files/template/valid_template_list_response.js b/cucumber/test_files/template/valid_template_list_response.js
new file mode 100644
index 0000000..4274810
--- /dev/null
+++ b/cucumber/test_files/template/valid_template_list_response.js
@@ -0,0 +1,38 @@
+'use strict';
+
+module.exports = {
+ stubbed: {
+ error: null,
+ output: [
+ {
+ name: 'template-1',
+ type: 'html'
+ },
+ {
+ name: 'template-2',
+ type: 'html'
+ },
+ {
+ name: 'template-3',
+ type: 'html'
+ }
+ ]
+ },
+ result: {
+ status: 200,
+ body: [
+ {
+ name: 'template-1',
+ type: 'html'
+ },
+ {
+ name: 'template-2',
+ type: 'html'
+ },
+ {
+ name: 'template-3',
+ type: 'html'
+ }
+ ]
+ }
+}
diff --git a/doc/api.md b/doc/api.md
index 37beec9..7280807 100755
--- a/doc/api.md
+++ b/doc/api.md
@@ -14,6 +14,7 @@ Source code is available [here](https://github.com/thegameofcode/resonator).
!include(doc/api/sms.md)
!include(doc/api/email.md)
!include(doc/api/push.md)
+!include(doc/api/template.md)
# Data Structures
diff --git a/doc/api/template.md b/doc/api/template.md
new file mode 100644
index 0000000..ae27b8e
--- /dev/null
+++ b/doc/api/template.md
@@ -0,0 +1,84 @@
+# Group Templates
+
+## Create email template [/api/notification/template]
+
+### Create an HTML email template [POST]
+Generates a new HTML email template from a non-HTML input.
+
+**Currently supported formats are .mjml files, using the format specified by [MJML](https://mjml.io/)**
+
+#### Attributes for the json body parameter
+
+Name | Type | Required | Description
+--- | --- | --- | ---
+__filename__ | String | Required | Name of the email template to be generated
+__content__ | String | Required | Input text that will be transformed into HTML
+__type__ | String | Required | The input type. Currently can be ´mjml´ **only**
+
++ Request (application/json; charset=utf-8)
+
+ + Body
+
+ {
+ "filename": "custom-template",
+ "content": "Hello World!",
+ "type": "mjml"
+ }
+
++ Response 200
+
+ {
+ "output": "done"
+ }
+
++ Response 400 (application/json; charset=utf-8)
+
+ {
+ "code": "BadRequestError",
+ "message": "Missing or invalid parameters: content, filename, type"
+ }
+
+## Email template listing [/api/notification/template/list]
+
+### Fetch list of email templates [GET]
+Returns a list of the email template filenames currently available
+
++ Request (application/json; charset=utf-8)
+
+ + Body
+
+ {}
+
++ Response 200
+
+ [
+ {
+ "name": "my-template-1",
+ "type": "html",
+ },
+ {
+ "name": "my-template-2",
+ "type": "html"
+ }
+ ]
+
+## Email template details [/api/notification/template/details/:templateName.:type]
+
+### Fetch email template details [GET]
+Returns a the content of an email template (either HTML or other) and the placeholders it has in its content.
+
++ Request (application/json; charset=utf-8)
+
++ Response 200
+
+ {
+ "html": "Hello World!",
+ "placeholders": ["USER"]
+ }
+
++ Response 404
+
+ {
+ "code": "NotFoundError",
+ "message": "Template not found"
+ }
\ No newline at end of file
diff --git a/lib/middleware/check_email.js b/lib/middleware/check_email.js
index bb3dbaa..6be6403 100755
--- a/lib/middleware/check_email.js
+++ b/lib/middleware/check_email.js
@@ -16,9 +16,12 @@ module.exports = function() {
return next(new errors.BadRequestError('Missing \'from\' property in parameters'));
}
- if (_.isEmpty(emailObj.message) || !_.isString(emailObj.message)) {
- log.error('Missing \'message\' String property in request body \'content\' object', emailObj);
- return next(new errors.BadRequestError('Missing \'message\' String property in request body \'content\' object'));
+ const hasMessageField = (!_.isEmpty(emailObj.message) && _.isString(emailObj.message));
+ const hasTemplateField = (!_.isEmpty(emailObj.template) && _.isObject(emailObj.template));
+
+ if (!hasMessageField && !hasTemplateField) {
+ log.error('Missing \'message\' String or \'template\' object property in \'content\'', emailObj);
+ return next(new errors.BadRequestError('Missing \'message\' String or \'template\' object property in \'content\''));
}
return next();
diff --git a/lib/middleware/check_single_email.js b/lib/middleware/check_single_email.js
index 155c838..eeedc47 100644
--- a/lib/middleware/check_single_email.js
+++ b/lib/middleware/check_single_email.js
@@ -18,9 +18,12 @@ module.exports = function() {
return next(new errors.BadRequestError('Missing \'to\' property in parameters'));
}
- if (_.isEmpty(emailObj.html) || !_.isString(emailObj.html)) {
- log.error('Missing \'html\' String property in request body', emailObj);
- return next(new errors.BadRequestError('Missing \'html\' String property in request body'));
+ const hasHtmlField = (!_.isEmpty(emailObj.html) && _.isString(emailObj.html));
+ const hasTemplateField = (!_.isEmpty(emailObj.template) && _.isObject(emailObj.template));
+
+ if (!hasHtmlField && !hasTemplateField) {
+ log.error('Missing \'html\' String or \'template\' object property in request body', emailObj);
+ return next(new errors.BadRequestError('Missing \'html\' String or \'template\' object property in request body'));
}
return next();
diff --git a/lib/platforms/email.js b/lib/platforms/email.js
index c1461d8..f8397a2 100644
--- a/lib/platforms/email.js
+++ b/lib/platforms/email.js
@@ -1,6 +1,8 @@
'use strict';
const config = require('config');
const _ = require('lodash');
+
+const processHtmlTemplate = require('../util/process_html_template');
const log = require('../util/logger');
const transport = require('../transport/mailgun');
@@ -17,6 +19,15 @@ function sendEmailNotification(identities, body, callback) {
}));
content = body.content;
+ if (content.template) {
+ const readHtml = processHtmlTemplate(content.template);
+
+ if (readHtml.error) {
+ return callback(readHtml.error);
+ }
+ content.message = readHtml.content;
+ }
+
log.info('Sending request to send emails', emails, content);
transport.send(emails, content, function(err, response, body) {
@@ -41,7 +52,16 @@ function sendSingleEmail(body, callback) {
const emails = body.to;
const content = body;
content.subject = body.subject;
- content.message = content.html;
+
+ if (content.template) {
+ const readHtml = processHtmlTemplate(content.template);
+ if (readHtml.error) {
+ return callback(readHtml.error);
+ }
+ content.message = readHtml.content;
+ } else {
+ content.message = content.html;
+ }
log.info('Sending request to sendSingleEmail', emails, content);
diff --git a/lib/platforms/template.js b/lib/platforms/template.js
new file mode 100644
index 0000000..299ba65
--- /dev/null
+++ b/lib/platforms/template.js
@@ -0,0 +1,117 @@
+'use strict';
+const fs = require('fs');
+const path = require('path');
+const async = require('async');
+const errors = require('../util/errors');
+const fileHandler = require('../util/file_handler');
+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(ORIGINAL_TEMPLATES_BASE_PATH, function(err, files) {
+
+ if (err) {
+ return callback(err);
+ }
+
+ const fileList = files.filter(function(file) {
+ return fileHandler.getFilenameInfo(file);
+ }).map(function(file) {
+ const filenameParts = fileHandler.getFilenameInfo(file);
+
+ return {
+ name: filenameParts[1],
+ type: filenameParts[2]
+ };
+ });
+
+ return callback(null, fileList);
+ });
+}
+
+function getTemplateDetails(filename, callback) {
+
+ const fileparts = fileHandler.getFilenameInfo(filename);
+
+ if (!fileparts) {
+ return callback(new errors.BadRequestError('Invalid template filename: must have the . format'));
+ }
+
+ const type = fileparts.type;
+
+ const basepath = (type === 'html') ? HTML_TEMPLATES_BASE_PATH : ORIGINAL_TEMPLATES_BASE_PATH;
+ const data = fileHandler.readFile(path.join(basepath, filename));
+
+ 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..d745a6d
--- /dev/null
+++ b/lib/routes/template.js
@@ -0,0 +1,63 @@
+'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 next(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, fileList) {
+ if (err) {
+ return next(err);
+ }
+
+ res.send(fileList);
+ });
+};
+
+routes.getTemplateDetails = function(req, res, next) {
+
+ let templateName = req.params.templateName;
+
+ templatePlatform.getTemplateDetails(templateName, function(err, details) {
+ if (err) {
+ return next(err);
+ }
+
+ res.send(details);
+ });
+};
+
+module.exports = function(server) {
+ server.post('/api/notification/template', routes.createEmailTemplate);
+ server.get('/api/notification/template/list', routes.listHtmlTemplates);
+ server.get('/api/notification/template/details/:templateName', 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/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/file_handler.js b/lib/util/file_handler.js
new file mode 100644
index 0000000..c997ebe
--- /dev/null
+++ b/lib/util/file_handler.js
@@ -0,0 +1,27 @@
+'uhtmlse strict';
+
+const fs = require('fs');
+const errors = require('./errors');
+
+const BASENAME_AND_EXTENSION_REGEX = /(.+?)\.([^.]*$|$)/;
+
+function readFile(filepath) {
+ const data = {};
+
+ try {
+ data.content = fs.readFileSync(filepath).toString();
+ return data;
+ } catch (err) {
+ data.error = new errors.NotFoundError('Template not found');
+ return data;
+ }
+}
+
+function getFilenameInfo(filename) {
+ return filename.match(BASENAME_AND_EXTENSION_REGEX);
+}
+
+module.exports = {
+ readFile: readFile,
+ getFilenameInfo: getFilenameInfo
+};
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
new file mode 100644
index 0000000..d4823e2
--- /dev/null
+++ b/lib/util/process_html_template.js
@@ -0,0 +1,26 @@
+'use strict';
+
+const path = require('path');
+const _ = require('lodash');
+const fileHandler = require('./file_handler');
+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 = fileHandler.readFile(filepath);
+
+ if (htmlData.content) {
+ const placeholderData = templateData.placeholders;
+ const placeholderKeys = _.keys(placeholderData);
+
+ _.each(placeholderKeys, function(placeholderKey) {
+ htmlData.content = htmlData.content.replace('{{' + placeholderKey + '}}', placeholderData[placeholderKey], 'g');
+ });
+ }
+
+ return htmlData;
+};
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;
+};
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/test/middlewares/check_email.js b/test/middlewares/check_email.js
index c703c04..c8b0c7d 100755
--- a/test/middlewares/check_email.js
+++ b/test/middlewares/check_email.js
@@ -49,7 +49,7 @@ describe('Batch email middleware', function() {
checkEmail()(request, res, next);
});
- it('returns a BadRequestError for a missing \'message\' field', function(done) {
+ it('returns a BadRequestError for a missing \'message\' and \'template\' field', function(done) {
delete emailObj.content.message;
request.body = emailObj;
@@ -59,7 +59,7 @@ describe('Batch email middleware', function() {
let next = function(error) {
expect(error.statusCode).to.equal(400);
expect(error.body.code).to.equal('BadRequestError');
- expect(error.body.message).to.equal('Missing \'message\' String property in request body \'content\' object');
+ expect(error.body.message).to.equal('Missing \'message\' String or \'template\' object property in \'content\'');
done();
};
@@ -76,7 +76,7 @@ describe('Batch email middleware', function() {
let next = function(error) {
expect(error.statusCode).to.equal(400);
expect(error.body.code).to.equal('BadRequestError');
- expect(error.body.message).to.equal('Missing \'message\' String property in request body \'content\' object');
+ expect(error.body.message).to.equal('Missing \'message\' String or \'template\' object property in \'content\'');
done();
};
@@ -112,4 +112,26 @@ describe('Batch email middleware', function() {
checkEmail()(request, res, next);
});
+ it('passes validations for a well-formatted email object with template object', function(done) {
+
+ delete emailObj.content.message;
+ emailObj.content.template = {
+ filename: 'template',
+ placeholders: {
+ USER: 'Mr. Invent'
+ }
+ };
+
+ request.body = emailObj;
+
+ let res = {};
+
+ let next = function(error) {
+ expect(error).to.equal(undefined);
+ done();
+ };
+
+ checkEmail()(request, res, next);
+ });
+
});
diff --git a/test/middlewares/check_single_email.js b/test/middlewares/check_single_email.js
index 307d6a0..a5ba0a7 100644
--- a/test/middlewares/check_single_email.js
+++ b/test/middlewares/check_single_email.js
@@ -40,7 +40,7 @@ describe('Single target email middleware', function() {
let next = function(error) {
expect(error.statusCode).to.equal(400);
expect(error.body.code).to.equal('BadRequestError');
- expect(error.body.message).to.equal('Missing \'html\' String property in request body');
+ expect(error.body.message).to.equal('Missing \'html\' String or \'template\' object property in request body');
done();
};
@@ -57,7 +57,7 @@ describe('Single target email middleware', function() {
let next = function(error) {
expect(error.statusCode).to.equal(400);
expect(error.body.code).to.equal('BadRequestError');
- expect(error.body.message).to.equal('Missing \'html\' String property in request body');
+ expect(error.body.message).to.equal('Missing \'html\' String or \'template\' object property in request body');
done();
};
diff --git a/test/platforms/single_email.js b/test/platforms/single_email.js
index a904b6e..ce954da 100644
--- a/test/platforms/single_email.js
+++ b/test/platforms/single_email.js
@@ -5,24 +5,27 @@ const expect = require('chai').expect;
const emailPlatform = require('./../../lib/platforms/email');
const TEST_FILES = './../sample_files/';
const mailgun = require('./../../lib/transport/mailgun');
-let mailgunStub;
+const fileHandler = require('./../../lib/util/file_handler');
+let mailgunStub, fileHandlerStub;
describe('Single email', function() {
beforeEach(function(done) {
mailgunStub = sinon.stub(mailgun, 'send');
mailgunStub.yields(null, 'Queued', {output: 'Queued notifications' });
+ fileHandlerStub = sinon.stub(fileHandler, 'readFile');
return done();
});
afterEach(function(done) {
mailgunStub.restore();
+ fileHandlerStub.restore();
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 +34,35 @@ 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;
+ fileHandlerStub.returns({content: 'Hello {{USER}}
'});
+
+ 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;
+
+ fileHandlerStub.returns({error: {message: 'Template not found', statusCode: 404, body: { code: 'NotFoundError', message: 'Template not found'}}});
+
+ emailPlatform.sendSingleEmail(requestBody, function(error, output) {
+ expect(error).to.not.equal(null);
+ expect(output).to.equal(undefined);
+ expect(error).to.have.property('message', 'Template not found');
+ expect(error).to.have.property('statusCode', 404);
+ expect(error).to.have.property('body');
+ expect(error.body).to.have.property('code', 'NotFoundError');
+ fileHandlerStub.restore();
+ return done();
+ });
+ });
});
diff --git a/test/platforms/template.js b/test/platforms/template.js
new file mode 100644
index 0000000..ef396f3
--- /dev/null
+++ b/test/platforms/template.js
@@ -0,0 +1,165 @@
+'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 VALID_HTML = 'Hello World
';
+const INVALID_MJML = '';
+const READ_FILE_LIST = ['template-1.mjml', 'template-2.mjml', 'template-3.mjml'];
+const FILE_LIST = [{name: 'template-1', type: 'mjml'}, {name: 'template-2', type: 'mjml'}, {name: 'template-3', type: 'mjml'}];
+
+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, READ_FILE_LIST);
+ fsStub.readFileSync = sinon.stub(fs, 'readFileSync');
+ 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 MJML template', function(done) {
+
+ const templateName = 'template.mjml';
+
+ fsStub.readFileSync.returns(VALID_MJML);
+
+ templatePlatform.getTemplateDetails(templateName, 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('returns the details associated to an MJML template', function(done) {
+
+ const templateName = 'template.mjml';
+
+ fsStub.readFileSync.returns(VALID_HTML);
+
+ templatePlatform.getTemplateDetails(templateName, function(err, details) {
+ expect(err).to.equal(null);
+ expect(details).to.have.property('content');
+ expect(details.content).to.equal(VALID_HTML);
+ 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.mjml';
+
+ fsStub.readFileSync.throws(new Error('Not Found'));
+
+ templatePlatform.getTemplateDetails(templateName, function(err, details) {
+ expect(details).to.equal(undefined);
+ expect(err).to.have.property('message', '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();
+ });
+ });
+
+ it('fails because of missing extension in requested HTML template filename', function(done) {
+ const templateName = 'invalid-format';
+
+ templatePlatform.getTemplateDetails(templateName, function(err, details) {
+ expect(details).to.equal(undefined);
+ expect(err).to.have.property('message', 'Invalid template filename: must have the . format');
+ expect(err).to.have.property('statusCode', 400);
+ expect(err).to.have.property('body');
+ expect(err.body).to.have.property('code', 'BadRequestError');
+ return done();
+ });
+ });
+});
diff --git a/test/sample_files/Orchestrator.json b/test/sample_files/Orchestrator.json
index 7c2b7f4..1c44567 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",
+ "placeholders": {
+ "USER": "Mr. Invent"
+ }
+ },
+ "subject": "Testing"
+ },
+ "invalid_mjml_single_email": {
+ "to": "mac@into.sh",
+ "template": {
+ "filename": "no-file",
+ "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
new file mode 100644
index 0000000..66cfdf1
--- /dev/null
+++ b/test/sample_files/template.html
@@ -0,0 +1,5 @@
+
+
+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/file_handler.js b/test/util/file_handler.js
new file mode 100644
index 0000000..6fbd67a
--- /dev/null
+++ b/test/util/file_handler.js
@@ -0,0 +1,43 @@
+'use strict';
+const fileHandler = require('./../../lib/util/file_handler');
+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 = fileHandler.readFile(VALID_HTML_PATH);
+ 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 = fileHandler.readFile(INVALID_HTML_PATH);
+ expect(output.content).to.equal(undefined);
+ expect(output.error).to.have.property('message', '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();
+ });
+
+ it('returns a basename and an extension for a filename', function(done) {
+ const filename = 'template.erb.html';
+ const output = fileHandler.getFilenameInfo(filename);
+ expect(output).to.have.length(3);
+ expect(output[1]).to.equal('template.erb');
+ expect(output[2]).to.equal('html');
+ return done();
+ });
+
+ it('returns null for a filename without extension', function(done) {
+ const filename = 'template';
+ const output = fileHandler.getFilenameInfo(filename);
+ expect(output).to.equal(null);
+ 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..d762f94
--- /dev/null
+++ b/test/util/process_html_template.js
@@ -0,0 +1,50 @@
+'use strict';
+const processHtmlTemplate = require('./../../lib/util/process_html_template');
+const expect = require('chai').expect;
+const sinon = require('sinon');
+const fileHandler = require('../../lib/util/file_handler');
+const EXISTING_HTML_TEMPLATE = 'template.html';
+const NON_EXISTING_HTML_TEMPLATE = 'no-file.html';
+
+let fileHandlerStub;
+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'
+ }
+ };
+
+ fileHandlerStub = sinon.stub(fileHandler, 'readFile');
+ fileHandlerStub.returns({content: 'Hello {{USER}}!
'});
+
+ 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');
+ fileHandlerStub.restore();
+ 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', '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/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();
+ });
+});