From 9833f8f8b996615212e18408c67e38448090656a Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Wed, 31 Mar 2021 20:07:23 -0400 Subject: [PATCH] refactor(contact): Refactored /contact endpoint and tests --- src/lib/contact.js | 59 +++++++ src/modules/contact/controller.js | 104 ++++++------ test/e2e/automated/a04-contact.rest-e2e.js | 178 ++++++++++++++++++++ test/unit/biz-logic/a04-contact.lib-unit.js | 117 +++++++++++++ test/unit/rest-api/a04-contact.rest-api.js | 62 +++++++ 5 files changed, 465 insertions(+), 55 deletions(-) create mode 100644 src/lib/contact.js create mode 100644 test/e2e/automated/a04-contact.rest-e2e.js create mode 100644 test/unit/biz-logic/a04-contact.lib-unit.js create mode 100644 test/unit/rest-api/a04-contact.rest-api.js diff --git a/src/lib/contact.js b/src/lib/contact.js new file mode 100644 index 0000000..ccefa21 --- /dev/null +++ b/src/lib/contact.js @@ -0,0 +1,59 @@ +/* eslint-disable no-useless-escape */ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + +const config = require('../../config') + +const NodeMailer = require('../lib/nodemailer') +const nodemailer = new NodeMailer() +const wlogger = require('./wlogger') + +let _this + +class ContactLib { + constructor () { + _this = this + _this.config = config + _this.nodemailer = nodemailer + } + + async sendEmail (emailObj) { + try { + // Validate input + if (!emailObj.email || typeof emailObj.email !== 'string') { + throw new Error("Property 'email' must be a string!") + } + + if (!emailObj.formMessage || typeof emailObj.formMessage !== 'string') { + throw new Error("Property 'formMessage' must be a string!") + } + + // If an email list exists, the email will be sended to that list + // otherwhise will be sended by default to the variable "_this.config.emailUser" + let _to = [_this.config.emailUser] + + // Email list is optional + if (emailObj.emailList) { + if ( + !Array.isArray(emailObj.emailList) || + !emailObj.emailList.length > 0 + ) { + throw new Error("Property 'emailList' must be a array of emails!") + } else { + _to = emailObj.emailList + } + } + + console.log(`Trying send message to : ${_to}`) + + emailObj.subject = 'Someone wants contact with you.' + emailObj.to = _to + + const result = await _this.nodemailer.sendEmail(emailObj) + return result + } catch (err) { + wlogger.error('Error in lib/contact.js/sendEmail()') + throw err + } + } +} +module.exports = ContactLib diff --git a/src/modules/contact/controller.js b/src/modules/contact/controller.js index 8aac25b..de56c54 100644 --- a/src/modules/contact/controller.js +++ b/src/modules/contact/controller.js @@ -1,78 +1,72 @@ /* eslint-disable no-useless-escape */ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' -const config = require('../../../config') - -const NodeMailer = require('../../lib/nodemailer') -const nodemailer = new NodeMailer() +const ContactLib = require('../../lib/contact') +const contactLib = new ContactLib() let _this - -class Contact { +/** + * @api {post} /contact/email Send Email + * @apiName SendMail + * @apiGroup Contact + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X POST -d '{ "obj": { "email": "email@format.com", "formMessage": "a message" } }' localhost:5001/contact/email + * + * @apiParam {Object} obj object (required) + * @apiParam {String} obj.email Sender Email. + * @apiParam {String} obj.formMessage Message. + * + * @apiSuccessExample {json} Success-Response: + * HTTP/1.1 200 OK + * { + * + * success:true + * + * } + * + * @apiError UnprocessableEntity Missing required parameters + * + * @apiErrorExample {json} Error-Response: + * HTTP/1.1 422 Unprocessable Entity + * { + * "status": 422, + * "error": "Unprocessable Entity" + * } + */ +class ContactController { constructor () { _this = this - _this.config = config - _this.nodemailer = nodemailer + _this.contactLib = contactLib } async email (ctx) { try { const data = ctx.request.body - const emailObj = data.obj - // Validate input - if (!emailObj.email || typeof emailObj.email !== 'string') { - throw new Error("Property 'email' must be a string!") - } - const isEmail = await _this.nodemailer.validateEmail(emailObj.email) - - if (!isEmail) { - throw new Error("Property 'email' must be email format!") - } - - if (!emailObj.formMessage || typeof emailObj.formMessage !== 'string') { - throw new Error("Property 'message' must be a string!") - } - - if (!emailObj.payloadTitle || typeof emailObj.payloadTitle !== 'string') { - throw new Error("Property 'payloadTitle' must be a string!") - } - - // If an email list exists, the email will be sended to that list - // otherwhise will be sended by default to the variable "_this.config.emailUser" - let _to = [_this.config.emailUser] - - // Email list is optional - if (emailObj.emailList) { - if ( - !Array.isArray(emailObj.emailList) || - !emailObj.emailList.length > 0 - ) { - throw new Error("Property 'emailList' must be a array of emails!") - } else { - _to = emailObj.emailList - } - } - - console.log(`Trying send message to : ${_to}`) - - emailObj.subject = 'Someone wants to share a document with you.' - emailObj.to = _to - - await _this.nodemailer.sendEmail(emailObj) + await _this.contactLib.sendEmail(emailObj) ctx.body = { success: true } } catch (err) { - // ctx.body = { - // success: false - // } - // console.error(`Error: `, err) - // throw err + _this.handleError(ctx, err) + } + } + // DRY error handler + handleError (ctx, err) { + // If an HTTP status is specified by the buisiness logic, use that. + if (err.status) { + if (err.message) { + ctx.throw(err.status, err.message) + } else { + ctx.throw(err.status) + } + } else { + // By default use a 422 error if the HTTP status is not specified. ctx.throw(422, err.message) } } } -module.exports = Contact +module.exports = ContactController diff --git a/test/e2e/automated/a04-contact.rest-e2e.js b/test/e2e/automated/a04-contact.rest-e2e.js new file mode 100644 index 0000000..648b871 --- /dev/null +++ b/test/e2e/automated/a04-contact.rest-e2e.js @@ -0,0 +1,178 @@ +const config = require('../../../config') +const axios = require('axios').default +const assert = require('chai').assert +const sinon = require('sinon') + +// Mock data +// const mockData = require('./mocks/contact-mocks') + +const LOCALHOST = `http://localhost:${config.port}` + +const mockContext = require('../../unit/mocks/ctx-mock').context +const ContactController = require('../../../src/modules/contact/controller') +let uut +let sandbox + +describe('Contact', () => { + beforeEach(() => { + uut = new ContactController() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + describe('POST /contact/email', () => { + it('should throw error if email property is not provided', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/contact/email`, + data: { + obj: { + formMessage: 'message' + } + } + } + + await axios(options) + + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 422) + assert.include(err.response.data, "Property 'email' must be a string!") + } + }) + + it('should throw error if formMessage property is not provided', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/contact/email`, + data: { + obj: { + email: 'email@email.com' + } + } + } + + await axios(options) + + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 422) + assert.include( + err.response.data, + "Property 'formMessage' must be a string!" + ) + } + }) + + it('should throw error if email list provided is not a array', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/contact/email`, + data: { + obj: { + email: 'email@email.com', + formMessage: 'test message', + emailList: 1 + } + } + } + + await axios(options) + + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 422) + assert.include( + err.response.data, + "Property 'emailList' must be a array of emails!" + ) + } + }) + + it('should throw error if email list provided is a empty array', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/contact/email`, + data: { + obj: { + email: 'email@email.com', + formMessage: 'test message', + emailList: [] + } + } + } + + await axios(options) + + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 422) + assert.include( + err.response.data, + "Property 'emailList' must be a array of emails!" + ) + } + }) + + it('should send email with minimun input', async () => { + try { + // Mock live network calls. + sandbox.stub(uut.contactLib, 'sendEmail').resolves(true) + + // Mock the context object. + const ctx = mockContext() + ctx.request = { + body: { + obj: { + email: 'email@email.com', + formMessage: 'test message' + } + } + } + await uut.email(ctx) + } catch (err) { + assert(false, 'Unexpected result') + } + }) + + it('should send email with all input', async () => { + try { + // Mock live network calls. + sandbox.stub(uut.contactLib, 'sendEmail').resolves(true) + + // Mock the context object. + const ctx = mockContext() + ctx.request = { + body: { + obj: { + email: 'email@email.com', + formMessage: 'test message', + emailList: ['email@email.com'] + } + } + } + await uut.email(ctx) + } catch (err) { + assert(false, 'Unexpected result') + } + }) + }) +}) diff --git a/test/unit/biz-logic/a04-contact.lib-unit.js b/test/unit/biz-logic/a04-contact.lib-unit.js new file mode 100644 index 0000000..ef4392b --- /dev/null +++ b/test/unit/biz-logic/a04-contact.lib-unit.js @@ -0,0 +1,117 @@ +const assert = require('chai').assert +const sinon = require('sinon') + +const ContactLib = require('../../../src/lib/contact') +let uut +let sandbox + +describe('Contact', () => { + beforeEach(() => { + uut = new ContactLib() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + describe('sendEmail()', () => { + it('should throw error if email property is not provided', async () => { + try { + const data = { + formMessage: 'test msg' + } + await uut.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, "Property 'email' must be a string!") + } + }) + + it('should throw error if formMessage property is not provided', async () => { + try { + const data = { + email: 'test@email.com' + } + await uut.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, "Property 'formMessage' must be a string!") + } + }) + it('should throw error if email list provided is not a array', async () => { + try { + sandbox.stub(uut.nodemailer, 'sendEmail').resolves(true) + + const data = { + formMessage: 'test msg', + email: 'test@email.com', + emailList: 'test@email.com' + } + await uut.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, "Property 'emailList' must be a array of emails!") + } + }) + it('should throw error if email list provided is a empty array', async () => { + try { + sandbox.stub(uut.nodemailer, 'sendEmail').resolves(true) + + const data = { + formMessage: 'test msg', + email: 'test@email.com', + emailList: [] + } + await uut.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, "Property 'emailList' must be a array of emails!") + } + }) + it('should send email to default server email', async () => { + try { + sandbox.stub(uut.nodemailer, 'sendEmail').resolves(true) + + const data = { + formMessage: 'test msg', + email: 'test@email.com' + } + const result = await uut.sendEmail(data) + assert.isTrue(result) + } catch (err) { + assert(false, 'Unexpected result') + } + }) + + it('should catch and trhow nodemailer lib error', async () => { + try { + // Force an error with the database. + sandbox.stub(uut.nodemailer, 'sendEmail').throws(new Error('test error')) + + const data = { + formMessage: 'test msg', + email: 'test@email.com' + } + await uut.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + + it('should send email to specifics email list', async () => { + try { + sandbox.stub(uut.nodemailer, 'sendEmail').resolves(true) + + const data = { + formMessage: 'test msg', + email: 'test@email.com', + emailList: ['testcontact@email.com'] + } + const result = await uut.sendEmail(data) + assert.isTrue(result) + } catch (err) { + assert(false, 'Unexpected result') + } + }) + }) +}) diff --git a/test/unit/rest-api/a04-contact.rest-api.js b/test/unit/rest-api/a04-contact.rest-api.js new file mode 100644 index 0000000..71218e6 --- /dev/null +++ b/test/unit/rest-api/a04-contact.rest-api.js @@ -0,0 +1,62 @@ +/* + Unit tests for the REST API handler for the /users endpoints. +*/ + +// Public npm libraries +const assert = require('chai').assert +const sinon = require('sinon') + +const ContactController = require('../../../src/modules/contact/controller') +let uut +let sandbox +let ctx + +const mockContext = require('../../unit/mocks/ctx-mock').context + +describe('Users', () => { + before(async () => { + }) + + beforeEach(() => { + uut = new ContactController() + + sandbox = sinon.createSandbox() + + // Mock the context object. + ctx = mockContext() + }) + + afterEach(() => sandbox.restore()) + + describe('#POST /contact', () => { + it('should return 422 status on biz logic error', async () => { + try { + await uut.email(ctx) + + assert.fail('Unexpected result') + } catch (err) { + // console.log(err) + assert.equal(err.status, 422) + assert.include(err.message, 'Cannot read property') + } + }) + + it('should return 200 status on success', async () => { + sandbox.stub(uut.contactLib, 'sendEmail').resolves(true) + + ctx.request.body = { + email: 'test02@test.com', + formMessage: 'test' + } + + await uut.email(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + // Assert that expected properties exist in the returned data. + assert.property(ctx.response.body, 'success') + assert.isTrue(ctx.response.body.success) + }) + }) +})