Skip to content

Commit

Permalink
feat(member): add subscribe to listserv (#862)
Browse files Browse the repository at this point in the history
Co-authored-by: WikiRik <WikiRik@users.noreply.github.com>
  • Loading branch information
WikiRik and WikiRik authored Jan 21, 2024
1 parent 1841cc4 commit 4de5d8b
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 0 deletions.
3 changes: 3 additions & 0 deletions config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const config = {
url: 'http://mailer',
port: 4000
},
listserv_email: [
process.env.LISTSERV_EMAIL || 'listserv@example.com'
],
logger: {
silent: false,
level: process.env.LOGLEVEL || 'info'
Expand Down
1 change: 1 addition & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ services:
HOST: "${SUBDOMAIN_FRONTEND}${BASE_URL}"
CORE_LOGIN: "${CORE_LOGIN}"
CORE_PASSWORD: "${CORE_PASSWORD}"
LISTSERV_EMAIL: "${LISTSERV_EMAIL}"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8084/healthcheck"]
interval: 30s
Expand Down
8 changes: 8 additions & 0 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ function getRandomBytes(length) {
});
}

function getMailText({ user, mailinglists }) {
return `OK BEGIN
REG ${user.first_name} ${user.last_name}
SUBSCRIBE ${mailinglists.join('\nSUBSCRIBE ')}
OK END`;
}

// A helper to add data to gauge Prometheus metric.
const addGaugeData = (gauge, array) => {
// reset gauge...
Expand All @@ -209,5 +216,6 @@ module.exports = {
findBy,
whitelistObject,
getRandomBytes,
getMailText,
addGaugeData
};
1 change: 1 addition & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ MemberRouter.post('/confirm', members.confirmUser);
MemberRouter.put('/primary-body', members.setPrimaryBody);
MemberRouter.put('/email', members.triggerEmailChange);
MemberRouter.put('/password', members.setUserPassword);
MemberRouter.post('/listserv', members.subscribeListserv);
MemberRouter.get('/', members.getUser);
MemberRouter.put('/', members.updateUser);
MemberRouter.delete('/', members.deleteUser);
Expand Down
39 changes: 39 additions & 0 deletions middlewares/members.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const moment = require('moment');
const _ = require('lodash');

const { User, Body, MailChange, MailConfirmation } = require('../models');
const config = require('../config');
const constants = require('../lib/constants');
const helpers = require('../lib/helpers');
const errors = require('../lib/errors');
Expand Down Expand Up @@ -298,3 +299,41 @@ exports.confirmEmailChange = async (req, res) => {
message: 'Mail was changed successfully.'
});
};

exports.subscribeListserv = async (req, res) => {
if (!req.permissions.hasPermission('global:subscribe:listserv') && req.user.id !== req.currentUser.id) {
return errors.makeForbiddenError(res, 'Permission update:member is required, but not present.');
}

if (!req.body.mailinglists) {
return errors.makeBadRequestError(res, 'No mailinglists are provided.');
}

if (!Array.isArray(req.body.mailinglists)) {
return errors.makeBadRequestError(res, 'Mailinglists must be an array of strings.');
}

if (req.body.mailinglists.length === 0) {
return errors.makeValidationError(res, 'Mailinglists must not be empty.');
}

const mailinglists = req.body.mailinglists.map((list) => list.toUpperCase());

await mailer.sendMail({
to: config.listserv_email,
from: req.user.notification_email,
subject: `SUBSCRIBE ${req.user.notification_email}`,
template: 'custom.html',
parameters: {
body: helpers.getMailText({
user: req.user,
mailinglists
})
}
});

return res.json({
success: true,
message: `Request for subscribing to ${mailinglists.join(', ')} has been sent.`
});
};
238 changes: 238 additions & 0 deletions test/api/users-listserv.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
const { startServer, stopServer } = require('../../lib/server');
const { request } = require('../scripts/helpers');
const generator = require('../scripts/generator');
const mock = require('../scripts/mock');

describe('User subscribe listserv', () => {
beforeAll(async () => {
await startServer();
});

afterAll(async () => {
await stopServer();
});

beforeEach(async () => {
await mock.mockAll();
});

afterEach(async () => {
await generator.clearAll();
await mock.cleanAll();
});

test('should return 404 if the user is not found', async () => {
const user = await generator.createUser({ superadmin: true });
const token = await generator.createAccessToken(user);

await generator.createPermission({ scope: 'global', action: 'subscribe', object: 'listserv' });

const res = await request({
uri: '/members/1337/listserv',
method: 'POST',
headers: { 'X-Auth-Token': token.value },
body: { mailinglists: ['ANNOUNCE-L'] }
});

expect(res.statusCode).toEqual(404);
expect(res.body.success).toEqual(false);
expect(res.body).not.toHaveProperty('data');
expect(res.body).toHaveProperty('message');
});

test('should fail if mailinglists is not provided', async () => {
const user = await generator.createUser({ superadmin: true });
const token = await generator.createAccessToken(user);

await generator.createPermission({ scope: 'global', action: 'subscribe', object: 'listserv' });

const res = await request({
uri: '/members/' + user.id + '/listserv',
method: 'POST',
headers: { 'X-Auth-Token': token.value },
body: {}
});

expect(res.statusCode).toEqual(400);
expect(res.body.success).toEqual(false);
expect(res.body).not.toHaveProperty('data');
expect(res.body).toHaveProperty('message');
});

test('should fail if there mailinglists is not an array', async () => {
const user = await generator.createUser({ superadmin: true });
const token = await generator.createAccessToken(user);

await generator.createPermission({ scope: 'global', action: 'subscribe', object: 'listserv' });

const res = await request({
uri: '/members/' + user.id + '/listserv',
method: 'POST',
headers: { 'X-Auth-Token': token.value },
body: { mailinglists: 'not-valid' }
});

expect(res.statusCode).toEqual(400);
expect(res.body.success).toEqual(false);
expect(res.body).not.toHaveProperty('data');
expect(res.body).toHaveProperty('message');
});

test('should fail if there mailinglists is an empty array', async () => {
const user = await generator.createUser({ superadmin: true });
const token = await generator.createAccessToken(user);

await generator.createPermission({ scope: 'global', action: 'subscribe', object: 'listserv' });

const res = await request({
uri: '/members/' + user.id + '/listserv',
method: 'POST',
headers: { 'X-Auth-Token': token.value },
body: { mailinglists: [] }
});

expect(res.statusCode).toEqual(422);
expect(res.body.success).toEqual(false);
expect(res.body).not.toHaveProperty('data');
expect(res.body).toHaveProperty('message');
});

test('should fail if mailer fails', async () => {
mock.mockAll({ mailer: { netError: true } });

const user = await generator.createUser({ superadmin: true });
const token = await generator.createAccessToken(user);

await generator.createPermission({ scope: 'global', action: 'subscribe', object: 'listserv' });

const res = await request({
uri: '/members/' + user.id + '/listserv',
method: 'POST',
headers: { 'X-Auth-Token': token.value },
body: { mailinglists: ['ANNOUNCE-L'] }
});

expect(res.statusCode).toEqual(500);
expect(res.body.success).toEqual(false);
expect(res.body).not.toHaveProperty('data');
expect(res.body).toHaveProperty('message');
});

test('should succeed for one mailinglist if everything is okay', async () => {
const user = await generator.createUser({ superadmin: true });
const token = await generator.createAccessToken(user);

await generator.createPermission({ scope: 'global', action: 'subscribe', object: 'listserv' });

const res = await request({
uri: '/members/' + user.id + '/listserv',
method: 'POST',
headers: { 'X-Auth-Token': token.value },
body: { mailinglists: ['ANNOUNCE-L'] }
});

expect(res.statusCode).toEqual(200);
expect(res.body.success).toEqual(true);
expect(res.body).not.toHaveProperty('errors');
expect(res.body).toHaveProperty('message');
expect(res.body.message).toEqual('Request for subscribing to ANNOUNCE-L has been sent.');
});

test('should succeed for multiple mailinglists if everything is okay', async () => {
const user = await generator.createUser({ superadmin: true });
const token = await generator.createAccessToken(user);

await generator.createPermission({ scope: 'global', action: 'subscribe', object: 'listserv' });

const res = await request({
uri: '/members/' + user.id + '/listserv',
method: 'POST',
headers: { 'X-Auth-Token': token.value },
body: { mailinglists: ['announce-l, AEGEE-L, AeGeEnEwS-l'] }
});

expect(res.statusCode).toEqual(200);
expect(res.body.success).toEqual(true);
expect(res.body).not.toHaveProperty('errors');
expect(res.body).toHaveProperty('message');
expect(res.body.message).toEqual('Request for subscribing to ANNOUNCE-L, AEGEE-L, AEGEENEWS-L has been sent.');
});

test('should work for current user for /me without permission', async () => {
const user = await generator.createUser();
const token = await generator.createAccessToken(user);

const res = await request({
uri: '/members/' + user.id + '/listserv',
method: 'POST',
headers: { 'X-Auth-Token': token.value },
body: { mailinglists: ['ANNOUNCE-L'] }
});

expect(res.statusCode).toEqual(200);
expect(res.body.success).toEqual(true);
expect(res.body).not.toHaveProperty('errors');
expect(res.body).toHaveProperty('message');
});

test('should work for current user for /:user_id without permission', async () => {
const user = await generator.createUser();
const token = await generator.createAccessToken(user);

const res = await request({
uri: '/members/' + user.id + '/listserv',
method: 'POST',
headers: { 'X-Auth-Token': token.value },
body: { mailinglists: ['ANNOUNCE-L'] }
});

expect(res.statusCode).toEqual(200);
expect(res.body.success).toEqual(true);
expect(res.body).not.toHaveProperty('errors');
expect(res.body).toHaveProperty('message');
});

test('should not work with local permission', async () => {
const user = await generator.createUser();
const token = await generator.createAccessToken(user);

const otherUser = await generator.createUser();
const permission = await generator.createPermission({ scope: 'local', action: 'suscribe', object: 'listserv' });
const body = await generator.createBody();
const circle = await generator.createCircle({ body_id: body.id });
await generator.createCircleMembership(circle, user);
await generator.createCirclePermission(circle, permission);
await generator.createBodyMembership(body, otherUser);

const res = await request({
uri: '/members/' + otherUser.id + '/listserv',
method: 'POST',
headers: { 'X-Auth-Token': token.value },
body: { mailinglists: ['ANNOUNCE-L'] }
});

expect(res.statusCode).toEqual(403);
expect(res.body.success).toEqual(false);
expect(res.body).not.toHaveProperty('data');
expect(res.body).toHaveProperty('message');
});

test('should fail if no permission', async () => {
const user = await generator.createUser();
const token = await generator.createAccessToken(user);

const otherUser = await generator.createUser();

const res = await request({
uri: '/members/' + otherUser.id + '/listserv',
method: 'POST',
headers: { 'X-Auth-Token': token.value },
body: { mailinglists: ['ANNOUNCE-L'] }
});

expect(res.statusCode).toEqual(403);
expect(res.body.success).toEqual(false);
expect(res.body).not.toHaveProperty('data');
expect(res.body).toHaveProperty('message');
});
});

0 comments on commit 4de5d8b

Please sign in to comment.