diff --git a/lib/antenna_criteria.js b/lib/antenna_criteria.js index 9127aa72..cda8a374 100644 --- a/lib/antenna_criteria.js +++ b/lib/antenna_criteria.js @@ -1,5 +1,6 @@ const { AntennaCriterion } = require('../models'); const errors = require('./errors'); +const mailer = require('./mailer'); exports.listCriteria = async (req, res) => { if (!req.permissions.manage_antenna_criteria) { @@ -40,3 +41,23 @@ exports.setCriterion = async (req, res) => { data: result[0] }); }; + +exports.sendFulfilmentMail = async (req, res) => { + if (!req.permissions.send_mails) { + return errors.makeForbiddenError(res, 'You are not allowed to send Antenna Criteria fulfilment mails.'); + } + + await mailer.sendMail({ + from: req.body.from, + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + template: req.body.template, + reply_to: req.body.reply_to + }); + + return res.json({ + success: true, + message: 'Successfully sent Antenna Criteria fulfilment mail', + }); +}; diff --git a/lib/helpers.js b/lib/helpers.js index afdad87b..12a9e5e1 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -50,7 +50,9 @@ function getBodiesListFromPermissions(result) { exports.getPermissions = (user, corePermissions, managePermissions) => { const permissions = { view_board: hasPermission(corePermissions, 'view:board'), - manage_antenna_criteria: hasPermission(corePermissions, 'global:manage_network:antenna_criteria') + manage_antenna_criteria: hasPermission(corePermissions, 'global:manage_network:antenna_criteria'), + manage_netcom_assignment: hasPermission(corePermissions, 'global:manage_network:netcom_assignment'), + send_mails: hasPermission(corePermissions, 'global:manage_network:fulfilment_email') }; permissions.manage_boards = { diff --git a/lib/mail_component.js b/lib/mail_component.js new file mode 100644 index 00000000..ba17eb24 --- /dev/null +++ b/lib/mail_component.js @@ -0,0 +1,34 @@ +const { MailComponent } = require('../models'); +const errors = require('./errors'); + +exports.listMailComponents = async (req, res) => { + if (!req.permissions.send_mails) { + return errors.makeForbiddenError(res, 'You are not allowed to list mail components.'); + } + + const components = await MailComponent.findAll({ + where: { agora_id: Number(req.params.agora_id) } + }); + + return res.json({ + success: true, + data: components + }); +}; + +exports.setMailComponent = async (req, res) => { + if (!req.permissions.send_mails) { + return errors.makeForbiddenError(res, 'You are not allowed to set mail components.'); + } + + if (!['introduction', 'communication', 'board election', 'members list', 'membership fee', 'events', 'agora attendance', 'development plan', 'fulfilment report', 'closing'].includes(req.body.mail_component)) { + return errors.makeValidationError(res, 'This is not a valid mail component.'); + } + + const result = await MailComponent.upsert(req.body); + + return res.json({ + success: true, + data: result[0] + }); +}; diff --git a/lib/mailer.js b/lib/mailer.js index ad712d2c..e781fc74 100644 --- a/lib/mailer.js +++ b/lib/mailer.js @@ -11,9 +11,11 @@ module.exports.sendMail = async (options) => { body: { from: options.from, to: options.to, + cc: options.cc, subject: options.subject, template: options.template, - parameters: options.parameters + parameters: options.parameters, + reply_to: options.reply_to } }); diff --git a/lib/netcom.js b/lib/netcom.js new file mode 100644 index 00000000..f625c99b --- /dev/null +++ b/lib/netcom.js @@ -0,0 +1,47 @@ +const { Netcom } = require('../models'); +const errors = require('./errors'); + +exports.listNetcomAssignment = async (req, res) => { + if (!req.permissions.manage_netcom_assignment) { + return errors.makeForbiddenError(res, 'You are not allowed to list NetCom assignment.'); + } + + const netcom = await Netcom.findAll(); + + return res.json({ + success: true, + data: netcom + }); +}; + +exports.setNetcomAssignment = async (req, res) => { + if (!req.permissions.manage_netcom_assignment) { + return errors.makeForbiddenError(res, 'You are not allowed to set NetCom assignment.'); + } + + const result = await Netcom.upsert(req.body); + + return res.json({ + success: true, + data: result[0] + }); +}; + +exports.removeNetcomAssignment = async (req, res) => { + if (!req.permissions.manage_netcom_assignment) { + return errors.makeForbiddenError(res, 'You are not allowed to remove NetCom assignment.'); + } + + const assignment = await Netcom.findOne({ + where: { + body_id: req.params.body_id + } + }); + + await assignment.destroy(); + + return res.json({ + success: true, + message: 'NetCom assignment was deleted.' + }); +}; diff --git a/lib/server.js b/lib/server.js index fbaaa4ff..13c7e67c 100644 --- a/lib/server.js +++ b/lib/server.js @@ -9,6 +9,8 @@ const morgan = require('./morgan'); const middlewares = require('./middlewares'); const boards = require('./boards'); const antennaCriteria = require('./antenna_criteria'); +const netcom = require('./netcom'); +const mailComponent = require('./mail_component'); const metrics = require('./metrics'); const endpointsMetrics = require('./endpoints_metrics'); const db = require('./sequelize'); @@ -44,6 +46,14 @@ GeneralRouter.delete('/bodies/:body_id/boards/:board_id', boards.findBoard, boar GeneralRouter.get('/antennaCriteria/:agora_id', antennaCriteria.listCriteria); GeneralRouter.put('/antennaCriteria', antennaCriteria.setCriterion); +GeneralRouter.post('/antennaCriteria/sendFulfilmentMail', antennaCriteria.sendFulfilmentMail); + +GeneralRouter.get('/netcom', netcom.listNetcomAssignment); +GeneralRouter.put('/netcom', netcom.setNetcomAssignment); +GeneralRouter.delete('/netcom/:body_id', netcom.removeNetcomAssignment); + +GeneralRouter.get('/mailComponent/:agora_id', mailComponent.listMailComponents); +GeneralRouter.put('/mailComponent', mailComponent.setMailComponent); server.use(endpointsMetrics.addEndpointMetrics); server.use('/', GeneralRouter); diff --git a/migrations/20240923115900-create-netcom-assignment.js b/migrations/20240923115900-create-netcom-assignment.js new file mode 100644 index 00000000..133c9af5 --- /dev/null +++ b/migrations/20240923115900-create-netcom-assignment.js @@ -0,0 +1,22 @@ +module.exports = { + up: (queryInterface, Sequelize) => queryInterface.createTable('netcom', { + body_id: { + allowNull: false, + type: Sequelize.INTEGER, + primaryKey: true + }, + netcom_id: { + allowNull: false, + type: Sequelize.INTEGER + }, + created_at: { + allowNull: false, + type: Sequelize.DATE + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE + } + }), + down: (queryInterface) => queryInterface.dropTable('netcom') +}; diff --git a/migrations/20240924202000-create-mail-component.js b/migrations/20240924202000-create-mail-component.js new file mode 100644 index 00000000..e5fc1e06 --- /dev/null +++ b/migrations/20240924202000-create-mail-component.js @@ -0,0 +1,27 @@ +module.exports = { + up: (queryInterface, Sequelize) => queryInterface.createTable('mailComponent', { + agora_id: { + allowNull: false, + type: Sequelize.INTEGER, + primaryKey: true + }, + mail_component: { + allowNull: false, + type: Sequelize.ENUM('introduction', 'communication', 'board election', 'members list', 'membership fee', 'events', 'agora attendance', 'development plan', 'fulfilment report', 'closing'), + primaryKey: true + }, + text: { + allowNull: false, + type: Sequelize.TEXT + }, + created_at: { + allowNull: false, + type: Sequelize.DATE + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE + } + }), + down: (queryInterface) => queryInterface.dropTable('mailComponent') +}; diff --git a/models/MailComponent.js b/models/MailComponent.js new file mode 100644 index 00000000..f399661c --- /dev/null +++ b/models/MailComponent.js @@ -0,0 +1,36 @@ +const { Sequelize, sequelize } = require('../lib/sequelize'); + +const MailComponent = sequelize.define('mailComponent', { + agora_id: { + allowNull: false, + primaryKey: true, + type: Sequelize.INTEGER, + validate: { + notEmpty: { msg: 'Agora should be set.' }, + isInt: { msg: 'Agora ID should be a number.' } + } + }, + mail_component: { + allowNull: false, + primaryKey: true, + type: Sequelize.ENUM('introduction', 'communication', 'board election', 'members list', 'membership fee', 'events', 'agora attendance', 'development plan', 'fulfilment report', 'closing'), + validate: { + isIn: { + args: [['introduction', 'communication', 'board election', 'members list', 'membership fee', 'events', 'agora attendance', 'development plan', 'fulfilment report', 'closing']], + msg: 'Message component must be one of these: "introduction", "communication", "board election", "members list", "membership fee", "events", "agora attendance", "development plan", "fulfilment report", "closing".' + } + } + }, + text: { + allowNull: false, + type: Sequelize.TEXT + } +}, { + underscored: true, + tableName: 'mailComponent', + createdAt: 'created_at', + updatedAt: 'updated_at', + primaryKey: ['agora_id', 'mail_component'] +}); + +module.exports = MailComponent; diff --git a/models/Netcom.js b/models/Netcom.js new file mode 100644 index 00000000..03d546e7 --- /dev/null +++ b/models/Netcom.js @@ -0,0 +1,27 @@ +const { Sequelize, sequelize } = require('../lib/sequelize'); + +const Netcom = sequelize.define('netcom', { + body_id: { + allowNull: false, + primaryKey: true, + type: Sequelize.INTEGER, + validate: { + notEmpty: { msg: 'Body should be set.' }, + isInt: { msg: 'Body ID should be a number.' } + } + }, + netcom_id: { + allowNull: false, + type: Sequelize.INTEGER, + validate: { + isInt: { msg: 'Netcom ID should be a number.' } + } + } +}, { + underscored: true, + tableName: 'netcom', + createdAt: 'created_at', + updatedAt: 'updated_at' +}); + +module.exports = Netcom; diff --git a/models/index.js b/models/index.js index d7e15a4e..116c332f 100644 --- a/models/index.js +++ b/models/index.js @@ -1,4 +1,6 @@ const Board = require('./Board'); const AntennaCriterion = require('./AntennaCriterion'); +const Netcom = require('./Netcom'); +const MailComponent = require('./MailComponent'); -module.exports = { Board, AntennaCriterion }; +module.exports = { Board, AntennaCriterion, Netcom, MailComponent }; diff --git a/test/api/mail-component.test.js b/test/api/mail-component.test.js new file mode 100644 index 00000000..80036833 --- /dev/null +++ b/test/api/mail-component.test.js @@ -0,0 +1,193 @@ +const { startServer, stopServer } = require('../../lib/server'); +const { request } = require('../scripts/helpers'); +const mock = require('../scripts/mock'); +const generator = require('../scripts/generator'); + +describe('MailComponent', () => { + beforeAll(async () => { + await startServer(); + }); + + afterAll(async () => { + await stopServer(); + }); + + beforeEach(async () => { + mock.mockAll(); + }); + + afterEach(async () => { + mock.cleanAll(); + await generator.clearAll(); + }); + + test('should fail listing MailComponents if no permission', async () => { + mock.mockAll({ mainPermissions: { noPermissions: true } }); + + const res = await request({ + uri: '/mailComponent/1', + method: 'GET', + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(403); + expect(res.body.success).toEqual(false); + }); + + test('should list MailComponents of the selected Agora', async () => { + await generator.createMailComponent({ agora_id: 1, mail_component: 'introduction' }); + await generator.createMailComponent({ agora_id: 1, mail_component: 'communication' }); + + const res = await request({ + uri: '/mailComponent/1', + method: 'GET', + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).toHaveProperty('data'); + expect(res.body.data.length).toEqual(2); + }); + + test('should only list MailComponents of the selected Agora', async () => { + await generator.createMailComponent({ agora_id: 1, mail_component: 'introduction' }); + await generator.createMailComponent({ agora_id: 1, mail_component: 'communication' }); + await generator.createMailComponent({ agora_id: 2, mail_component: 'board election' }); + + const res = await request({ + uri: '/mailComponent/1', + method: 'GET', + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).toHaveProperty('data'); + expect(res.body.data.length).toEqual(2); + }); + + test('should fail creating new MailComponent if no permission', async () => { + mock.mockAll({ mainPermissions: { noPermissions: true } }); + + const component = generator.generateMailComponent(); + + const res = await request({ + uri: '/mailComponent', + method: 'PUT', + body: component, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(403); + expect(res.body.success).toEqual(false); + }); + + test('should fail creating new MailComponent if agora_id is not set', async () => { + const component = generator.generateMailComponent({ agora_id: null }); + + const res = await request({ + uri: '/mailComponent', + method: 'PUT', + body: component, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(422); + expect(res.body.success).toEqual(false); + expect(res.body).toHaveProperty('errors'); + expect(res.body).not.toHaveProperty('data'); + expect(res.body.errors).toHaveProperty('agora_id'); + }); + + test('should fail creating new MailComponent if mail_component is not set', async () => { + const component = generator.generateMailComponent({ mail_component: null }); + + const res = await request({ + uri: '/mailComponent', + method: 'PUT', + body: component, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(422); + expect(res.body.success).toEqual(false); + expect(res.body).toHaveProperty('errors'); + expect(res.body).not.toHaveProperty('data'); + expect(res.body.errors).toHaveProperty('mail_component'); + }); + + test('should fail creating new MailComponent if mail_component is not correct', async () => { + const component = generator.generateMailComponent({ mail_component: 'blabla' }); + + const res = await request({ + uri: '/mailComponent', + method: 'PUT', + body: component, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(422); + expect(res.body.success).toEqual(false); + expect(res.body).not.toHaveProperty('data'); + }); + + test('should fail creating new MailComponent if mail_component is not set', async () => { + const component = generator.generateMailComponent({ text: null }); + + const res = await request({ + uri: '/mailComponent', + method: 'PUT', + body: component, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(422); + expect(res.body.success).toEqual(false); + expect(res.body).toHaveProperty('errors'); + expect(res.body).not.toHaveProperty('data'); + expect(res.body.errors).toHaveProperty('text'); + }); + + test('should create new MailComponent if everything is okay', async () => { + const component = generator.generateMailComponent(); + + const res = await request({ + uri: '/mailComponent', + method: 'PUT', + body: component, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).not.toHaveProperty('errors'); + expect(res.body).toHaveProperty('data'); + expect(res.body.data).toHaveProperty('agora_id'); + expect(res.body.data).toHaveProperty('mail_component'); + expect(res.body.data).toHaveProperty('text'); + }); + + test('should update existing MailComponent if everything is okay', async () => { + await generator.createMailComponent({ agora_id: 1, mail_component: 'introduction', text: 'Hello!' }); + const component = generator.generateMailComponent({ agora_id: 1, mail_component: 'introduction', text: 'Goodbye!' }); + + const res = await request({ + uri: '/mailComponent', + method: 'PUT', + body: component, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).not.toHaveProperty('errors'); + expect(res.body).toHaveProperty('data'); + expect(res.body.data).toHaveProperty('agora_id'); + expect(res.body.data.agora_id).toEqual(1); + expect(res.body.data).toHaveProperty('mail_component'); + expect(res.body.data.mail_component).toEqual('introduction'); + expect(res.body.data).toHaveProperty('text'); + expect(res.body.data.text).toEqual('Goodbye!'); + }); +}); diff --git a/test/api/netcom.test.js b/test/api/netcom.test.js new file mode 100644 index 00000000..8efd1948 --- /dev/null +++ b/test/api/netcom.test.js @@ -0,0 +1,175 @@ +const { startServer, stopServer } = require('../../lib/server'); +const { request } = require('../scripts/helpers'); +const mock = require('../scripts/mock'); +const generator = require('../scripts/generator'); +const { Netcom } = require('../../models'); + +describe('Netcom', () => { + beforeAll(async () => { + await startServer(); + }); + + afterAll(async () => { + await stopServer(); + }); + + beforeEach(async () => { + mock.mockAll(); + }); + + afterEach(async () => { + mock.cleanAll(); + await generator.clearAll(); + }); + + test('should fail listing Netcom assignment if no permission', async () => { + mock.mockAll({ mainPermissions: { noPermissions: true } }); + + const res = await request({ + uri: '/netcom', + method: 'GET', + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(403); + expect(res.body.success).toEqual(false); + }); + + test('should list Netcom assignment if everything is okay', async () => { + await generator.createNetcom(); + + const res = await request({ + uri: '/netcom', + method: 'GET', + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).toHaveProperty('data'); + expect(res.body.data.length).toEqual(1); + }); + + test('should fail creating Netcom assignment if no permission', async () => { + mock.mockAll({ mainPermissions: { noPermissions: true } }); + + const netcom = generator.generateNetcom(); + + const res = await request({ + uri: '/netcom', + method: 'PUT', + body: netcom, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(403); + expect(res.body.success).toEqual(false); + }); + + test('should fail creating Netcom assignment if body_id is not set', async () => { + const netcom = generator.generateNetcom({ body_id: null }); + + const res = await request({ + uri: '/netcom', + method: 'PUT', + body: netcom, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(422); + expect(res.body.success).toEqual(false); + expect(res.body).toHaveProperty('errors'); + expect(res.body).not.toHaveProperty('data'); + expect(res.body.errors).toHaveProperty('body_id'); + }); + + test('should fail creating Netcom assignment if netcom_id is not set', async () => { + const netcom = generator.generateNetcom({ netcom_id: null }); + + const res = await request({ + uri: '/netcom', + method: 'PUT', + body: netcom, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(422); + expect(res.body.success).toEqual(false); + expect(res.body).toHaveProperty('errors'); + expect(res.body).not.toHaveProperty('data'); + expect(res.body.errors).toHaveProperty('netcom_id'); + }); + + test('should create new Netcom assignment if everything is okay', async () => { + const netcom = generator.generateNetcom(); + + const res = await request({ + uri: '/netcom', + method: 'PUT', + body: netcom, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).not.toHaveProperty('errors'); + expect(res.body).toHaveProperty('data'); + expect(res.body.data).toHaveProperty('body_id'); + expect(res.body.data).toHaveProperty('netcom_id'); + }); + + test('should update existing Netcom assignment if everything is okay', async () => { + await generator.createNetcom({ body_id: 1, netcom_id: 2 }); + const netcom = generator.generateNetcom({ body_id: 1, netcom_id: 3 }); + + const res = await request({ + uri: '/netcom', + method: 'PUT', + body: netcom, + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).not.toHaveProperty('errors'); + expect(res.body).toHaveProperty('data'); + expect(res.body.data).toHaveProperty('body_id'); + expect(res.body.data.body_id).toEqual(1); + expect(res.body.data).toHaveProperty('netcom_id'); + expect(res.body.data.netcom_id).toEqual(3); + }); + + test('should fail deleting Netcom assignment if no permission', async () => { + mock.mockAll({ mainPermissions: { noPermissions: true } }); + + const netcom = await generator.createNetcom(); + + const res = await request({ + uri: '/netcom/' + netcom.body_id, + method: 'DELETE', + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(403); + expect(res.body.success).toEqual(false); + }); + + test('should delete Netcom assignment if everything is okay', async () => { + const netcom = await generator.createNetcom(); + + const res = await request({ + uri: '/netcom/' + netcom.body_id, + method: 'DELETE', + headers: { 'X-Auth-Token': 'blablabla' } + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.success).toEqual(true); + expect(res.body).not.toHaveProperty('errors'); + expect(res.body).toHaveProperty('data'); + + const netcomFromDB = await Netcom.findByPk(netcom.body_id); + + expect(netcomFromDB).toEqual(null); + }); +}); diff --git a/test/assets/core-permissions-full.json b/test/assets/core-permissions-full.json index fcaa31c2..56d6530a 100644 --- a/test/assets/core-permissions-full.json +++ b/test/assets/core-permissions-full.json @@ -49,5 +49,11 @@ }, { "combined": "global:manage_network:fulfilment_report" + }, + { + "combined": "global:manage_network:netcom_assignment" + }, + { + "combined": "global:manage_network:fulfilment_email" }] } diff --git a/test/scripts/generator.js b/test/scripts/generator.js index 0101cca4..f24cd84a 100644 --- a/test/scripts/generator.js +++ b/test/scripts/generator.js @@ -1,6 +1,6 @@ const { faker } = require('@faker-js/faker'); -const { Board, AntennaCriterion } = require('../../models'); +const { Board, AntennaCriterion, Netcom, MailComponent } = require('../../models'); const notSet = (field) => typeof field === 'undefined'; @@ -31,7 +31,32 @@ exports.createAntennaCriterion = (options = {}) => { return AntennaCriterion.create(exports.generateAntennaCriterion(options)); }; +exports.generateNetcom = (options = {}) => { + if (notSet(options.body_id)) options.body_id = faker.number.int(100); + if (notSet(options.netcom_id)) options.netcom_id = faker.number.int(100); + + return options; +}; + +exports.createNetcom = (options = {}) => { + return Netcom.create(exports.generateNetcom(options)); +}; + +exports.generateMailComponent = (options = {}) => { + if (notSet(options.agora_id)) options.agora_id = faker.number.int(100); + if (notSet(options.mail_component)) options.mail_component = faker.helpers.arrayElement(['introduction', 'communication', 'board election', 'members list', 'membership fee', 'events', 'agora attendance', 'development plan', 'fulfilment report', 'closing']); + if (notSet(options.text)) options.text = faker.string.alphanumeric(16); + + return options; +}; + +exports.createMailComponent = (options = {}) => { + return MailComponent.create(exports.generateMailComponent(options)); +}; + exports.clearAll = async () => { await Board.destroy({ where: {}, truncate: { cascade: true } }); await AntennaCriterion.destroy({ where: {}, truncate: { cascade: true } }); + await Netcom.destroy({ where: {}, truncate: { cascade: true } }); + await MailComponent.destroy({ where: {}, truncate: { cascade: true } }); };