Subscription tests now pass.

This commit is contained in:
Tomas Bures 2018-05-21 19:41:10 +02:00
parent 1f5566ca9b
commit 4943b22a51
6 changed files with 87 additions and 75 deletions

View file

@ -65,17 +65,15 @@ hbs.registerPartials(__dirname + '/views/subscription/partials/');
* and the message is never displayed * and the message is never displayed
*/ */
hbs.registerHelper('flash_messages', function () { // eslint-disable-line prefer-arrow-callback hbs.registerHelper('flash_messages', function () { // eslint-disable-line prefer-arrow-callback
console.log(this.flash);
if (typeof this.flash !== 'function') { // eslint-disable-line no-invalid-this if (typeof this.flash !== 'function') { // eslint-disable-line no-invalid-this
return ''; return '';
} }
let messages = this.flash(); // eslint-disable-line no-invalid-this const messages = this.flash(); // eslint-disable-line no-invalid-this
console.log(messages); const response = [];
let response = [];
// group messages by type // group messages by type
Object.keys(messages).forEach(key => { for (const key in messages) {
let el = '<div class="alert alert-' + key + ' alert-dismissible" role="alert"><button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>'; let el = '<div class="alert alert-' + key + ' alert-dismissible" role="alert"><button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>';
if (key === 'danger') { if (key === 'danger') {
@ -84,11 +82,9 @@ hbs.registerHelper('flash_messages', function () { // eslint-disable-line prefer
let rows = []; let rows = [];
messages[key].forEach(message => { for (const message of messages[key]) {
message = hbs.handlebars.escapeExpression(message); rows.push(hbs.handlebars.escapeExpression(message).replace(/(\r\n|\n|\r)/gm, '<br>'));
message = message.replace(/(\r\n|\n|\r)/gm, '<br>'); }
rows.push(message);
});
if (rows.length > 1) { if (rows.length > 1) {
el += '<p>' + rows.join('</p>\n<p>') + '</p>'; el += '<p>' + rows.join('</p>\n<p>') + '</p>';
@ -99,7 +95,7 @@ hbs.registerHelper('flash_messages', function () { // eslint-disable-line prefer
el += '</div>'; el += '</div>';
response.push(el); response.push(el);
}); }
return new hbs.handlebars.SafeString( return new hbs.handlebars.SafeString(
response.join('\n') response.join('\n')
@ -375,7 +371,7 @@ function createApp(trusted) {
data: [] data: []
}; };
return status(err.status || 500).json(resp); return res.status(err.status || 500).json(resp);
} else { } else {
// TODO: Render interoperable errors using a special client that does internationalization of the error message // TODO: Render interoperable errors using a special client that does internationalization of the error message

View file

@ -583,11 +583,12 @@ async function forHbs(context, listId, subscription) { // assumes grouped subscr
}; };
if (!type.grouped && !type.enumerated) { if (!type.grouped && !type.enumerated) {
entry.value = subscription ? type.forHbs(fld, subscription[fldKey]) : ''; // subscription[fldKey] may not exists because we are getting the data from "fromPost"
entry.value = (subscription ? type.forHbs(fld, subscription[fldKey]) : null) || '';
} else if (type.grouped) { } else if (type.grouped) {
const options = []; const options = [];
const value = subscription ? subscription[fldKey] : (type.cardinality === Cardinality.SINGLE ? null : []); const value = (subscription ? subscription[fldKey] : null) || (type.cardinality === Cardinality.SINGLE ? null : []);
for (const optCol in fld.groupedOptions) { for (const optCol in fld.groupedOptions) {
const opt = fld.groupedOptions[optCol]; const opt = fld.groupedOptions[optCol];
@ -610,7 +611,7 @@ async function forHbs(context, listId, subscription) { // assumes grouped subscr
} else if (type.enumerated) { } else if (type.enumerated) {
const options = []; const options = [];
const value = subscription ? subscription[fldKey] : null; const value = (subscription ? subscription[fldKey] : null) || null;
for (const opt of fld.settings.options) { for (const opt of fld.settings.options) {
options.push({ options.push({
@ -631,7 +632,8 @@ async function forHbs(context, listId, subscription) { // assumes grouped subscr
} }
// Converts subscription data received via POST request from subscription form or via subscribe request to API v1 to subscription structure supported by subscriptions model. // Converts subscription data received via POST request from subscription form or via subscribe request to API v1 to subscription structure supported by subscriptions model.
async function fromPost(context, listId, data) { // assumes grouped subscription // If a field is not specified in the POST data, it omits it also in the returned subscription
async function fromPost(context, listId, data, partial) { // assumes grouped subscription
// This is to handle option values from API v1 // This is to handle option values from API v1
function isSelected(value) { function isSelected(value) {
@ -643,43 +645,45 @@ async function fromPost(context, listId, data) { // assumes grouped subscription
const subscription = {}; const subscription = {};
for (const fld of flds) { for (const fld of flds) {
const type = fieldTypes[fld.type]; if (fld.key in data) {
const fldKey = getFieldKey(fld); const type = fieldTypes[fld.type];
const fldKey = getFieldKey(fld);
let value = null; let value = null;
if (!type.grouped && !type.enumerated) { if (!type.grouped && !type.enumerated) {
value = type.parsePostValue(fld, cleanupFromPost(data[fld.key])); value = type.parsePostValue(fld, cleanupFromPost(data[fld.key]));
} else if (type.grouped) { } else if (type.grouped) {
if (type.cardinality === Cardinality.SINGLE) { if (type.cardinality === Cardinality.SINGLE) {
for (const optCol in fld.groupedOptions) { for (const optCol in fld.groupedOptions) {
const opt = fld.groupedOptions[optCol]; const opt = fld.groupedOptions[optCol];
// This handles two different formats for grouped dropdowns and radios. // This handles two different formats for grouped dropdowns and radios.
// The first part of the condition handles the POST requests from the subscription form, while the // The first part of the condition handles the POST requests from the subscription form, while the
// second part handles the subscribe request to API v1 // second part handles the subscribe request to API v1
if (data[fld.key] === opt.key || isSelected(data[opt.key])) { if (data[fld.key] === opt.key || isSelected(data[opt.key])) {
value = opt.column value = opt.column
}
}
} else {
value = [];
for (const optCol in fld.groupedOptions) {
const opt = fld.groupedOptions[optCol];
if (isSelected(data[opt.key])) {
value.push(opt.column);
}
} }
} }
} else {
value = [];
for (const optCol in fld.groupedOptions) { } else if (type.enumerated) {
const opt = fld.groupedOptions[optCol]; value = data[fld.key];
if (isSelected(data[opt.key])) {
value.push(opt.column);
}
}
} }
} else if (type.enumerated) { subscription[fldKey] = value;
value = data[fld.key];
} }
subscription[fldKey] = value;
} }
return subscription; return subscription;

View file

@ -533,6 +533,7 @@ async function enforceEntityPermission(context, entityTypeId, entityId, required
await knex.transaction(async tx => { await knex.transaction(async tx => {
const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations); const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
if (!result) { if (!result) {
log.info(`Denying permission ${entityTypeId}.${entityId} ${requiredOperations}`);
throwPermissionDenied(); throwPermissionDenied();
} }
}); });
@ -544,6 +545,7 @@ async function enforceEntityPermissionTx(tx, context, entityTypeId, entityId, re
} }
const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations); const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
if (!result) { if (!result) {
log.info(`Denying permission ${entityTypeId}.${entityId} ${requiredOperations}`);
throwPermissionDenied(); throwPermissionDenied();
} }
} }
@ -552,6 +554,7 @@ async function enforceTypePermission(context, entityTypeId, requiredOperations)
await knex.transaction(async tx => { await knex.transaction(async tx => {
const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations); const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
if (!result) { if (!result) {
log.info(`Denying permission ${entityTypeId} ${requiredOperations}`);
throwPermissionDenied(); throwPermissionDenied();
} }
}); });
@ -560,6 +563,7 @@ async function enforceTypePermission(context, entityTypeId, requiredOperations)
async function enforceTypePermissionTx(tx, context, entityTypeId, requiredOperations) { async function enforceTypePermissionTx(tx, context, entityTypeId, requiredOperations) {
const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations); const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
if (!result) { if (!result) {
log.info(`Denying permission ${entityTypeId} ${requiredOperations}`);
throwPermissionDenied(); throwPermissionDenied();
} }
} }

View file

@ -400,12 +400,18 @@ async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, meta
} }
const existingWithKey = await existingWithKeyQuery.first(); const existingWithKey = await existingWithKeyQuery.first();
if (existingWithKey) { if (existingWithKey) {
if (meta && (meta.updateAllowed || meta.updateOfUnsubscribedAllowed && existingWithKey.status === SubscriptionStatus.UNSUBSCRIBED)) { if (meta && (meta.updateAllowed || (meta.updateOfUnsubscribedAllowed && existingWithKey.status === SubscriptionStatus.UNSUBSCRIBED))) {
meta.update = true; meta.update = true;
meta.existing = existingWithKey; meta.existing = existingWithKey;
} else { } else {
throw new interoperableErrors.DuplicitEmailError(); throw new interoperableErrors.DuplicitEmailError();
} }
} else {
// This is here because of the API, which allows one to send subscriptions without caring about whether they already exist, what their status is, etc.
// In the case, the subscription is existing, we should not change the status. If it does not exist, we are fine with changing the status
if (meta.subscribeIfNoExisting && !entity.status) {
entity.status = SubscriptionStatus.SUBSCRIBED;
}
} }
if ((isCreate && !(meta && meta.update)) || 'status' in entity) { if ((isCreate && !(meta && meta.update)) || 'status' in entity) {
@ -455,8 +461,8 @@ async function _create(tx, listId, filteredEntity) {
/* /*
Adds a new subscription. Returns error if a subscription with the same email address is already present and is not unsubscribed. Adds a new subscription. Returns error if a subscription with the same email address is already present and is not unsubscribed.
If it is unsubscribed, the existing subscription is changed based on the provided data. If it is unsubscribed and meta.updateOfUnsubscribedAllowed, the existing subscription is changed based on the provided data.
If meta.partial is true, it updates even an active subscription. If meta.updateAllowed is true, it updates even an active subscription.
*/ */
async function create(context, listId, entity, meta /* meta is provided when called from /confirm/subscribe/:cid */) { async function create(context, listId, entity, meta /* meta is provided when called from /confirm/subscribe/:cid */) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {

View file

@ -14,6 +14,7 @@ const interoperableErrors = require('../shared/interoperable-errors');
const contextHelpers = require('../lib/context-helpers'); const contextHelpers = require('../lib/context-helpers');
const shares = require('../models/shares'); const shares = require('../models/shares');
const slugify = require('slugify'); const slugify = require('slugify');
const passport = require('../lib/passport');
class APIError extends Error { class APIError extends Error {
constructor(msg, status) { constructor(msg, status) {
@ -23,8 +24,9 @@ class APIError extends Error {
} }
router.postAsync('/subscribe/:listId', async (req, res) => { router.postAsync('/subscribe/:listCid', passport.loggedIn, async (req, res) => {
const listId = req.params.listId; const list = await lists.getByCid(req.context, req.params.listCid);
await shares.enforceEntityPermission(req.context, 'list', list.id, 'manageSubscriptions');
const input = {}; const input = {};
Object.keys(req.body).forEach(key => { Object.keys(req.body).forEach(key => {
@ -37,31 +39,28 @@ router.postAsync('/subscribe/:listId', async (req, res) => {
const emailErr = await tools.validateEmail(input.EMAIL, false); const emailErr = await tools.validateEmail(input.EMAIL, false);
if (emailErr) { if (emailErr) {
const errMsg = tools.validateEmailGetMessage(emailErr, email); const errMsg = tools.validateEmailGetMessage(emailErr, input.email);
log.error('API', errMsg); log.error('API', errMsg);
throw new APIError(errMsg, 400); throw new APIError(errMsg, 400);
} }
const subscription = await fields.fromPost(req.context, list.id, input, true);
if (input.TIMEZONE) { if (input.TIMEZONE) {
subscription.tz = (input.TIMEZONE || '').toString().trim(); subscription.tz = (input.TIMEZONE || '').toString().trim();
} }
const subscription = await fields.fromPost(req.context, listId);
if (/^(yes|true|1)$/i.test(input.FORCE_SUBSCRIBE)) { if (/^(yes|true|1)$/i.test(input.FORCE_SUBSCRIBE)) {
subscription.status = SubscriptionStatus.SUBSCRIBED; subscription.status = SubscriptionStatus.SUBSCRIBED;
} }
if (/^(yes|true|1)$/i.test(input.REQUIRE_CONFIRMATION)) { // if REQUIRE_CONFIRMATION is set, we assume that the user is not subscribed and will be subscribed if (/^(yes|true|1)$/i.test(input.REQUIRE_CONFIRMATION)) { // if REQUIRE_CONFIRMATION is set, we assume that the user is not subscribed and will be subscribed
const list = await lists.getByCid(contextHelpers.getAdminContext(), listId);
await shares.enforceEntityPermission(req.context, 'list', listId, 'manageSubscriptions');
const data = { const data = {
email, email: input.EMAIL,
subscriptionData: subscription subscriptionData: subscription
}; };
const confirmCid = await confirmations.addConfirmation(listId, 'subscribe', req.ip, data); const confirmCid = await confirmations.addConfirmation(list.id, 'subscribe', req.ip, data);
await mailHelpers.sendConfirmSubscription(list, input.EMAIL, confirmCid, subscription); await mailHelpers.sendConfirmSubscription(list, input.EMAIL, confirmCid, subscription);
res.status(200); res.status(200);
@ -74,10 +73,11 @@ router.postAsync('/subscribe/:listId', async (req, res) => {
subscription.email = input.EMAIL; subscription.email = input.EMAIL;
const meta = { const meta = {
updateAllowed: true updateAllowed: true,
subscribeIfNoExisting: true
}; };
await subscriptions.create(req.context, listId, subscription, meta); await subscriptions.create(req.context, list.id, subscription, meta);
res.status(200); res.status(200);
res.json({ res.json({
@ -89,7 +89,8 @@ router.postAsync('/subscribe/:listId', async (req, res) => {
}); });
router.postAsync('/unsubscribe/:listId', async (req, res) => { router.postAsync('/unsubscribe/:listCid', passport.loggedIn, async (req, res) => {
const list = await lists.getByCid(req.context, req.params.listCid);
const input = {}; const input = {};
Object.keys(req.body).forEach(key => { Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim(); input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
@ -99,7 +100,7 @@ router.postAsync('/unsubscribe/:listId', async (req, res) => {
throw new APIError('Missing EMAIL', 400); throw new APIError('Missing EMAIL', 400);
} }
const subscription = await subscriptions.unsubscribeByEmailAndGet(req.context, req.params.listId, input.EMAIL); const subscription = await subscriptions.unsubscribeByEmailAndGet(req.context, list.id, input.EMAIL);
res.status(200); res.status(200);
res.json({ res.json({
@ -111,7 +112,8 @@ router.postAsync('/unsubscribe/:listId', async (req, res) => {
}); });
router.postAsync('/delete/:listId', async (req, res) => { router.postAsync('/delete/:listCid', passport.loggedIn, async (req, res) => {
const list = await lists.getByCid(req.context, req.params.listCid);
const input = {}; const input = {};
Object.keys(req.body).forEach(key => { Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim(); input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
@ -121,7 +123,7 @@ router.postAsync('/delete/:listId', async (req, res) => {
throw new APIError('Missing EMAIL', 400); throw new APIError('Missing EMAIL', 400);
} }
const subscription = await subscriptions.removeByEmailAndGet(req.context, req.params.listId, input.EMAIL); const subscription = await subscriptions.removeByEmailAndGet(req.context, list.id, input.EMAIL);
res.status(200); res.status(200);
res.json({ res.json({
@ -133,11 +135,12 @@ router.postAsync('/delete/:listId', async (req, res) => {
}); });
router.getAsync('/subscriptions/:listId', async (req, res) => { router.getAsync('/subscriptions/:listCid', passport.loggedIn, async (req, res) => {
const list = await lists.getByCid(req.context, req.params.listCid);
const start = parseInt(req.query.start || 0, 10); const start = parseInt(req.query.start || 0, 10);
const limit = parseInt(req.query.limit || 10000, 10); const limit = parseInt(req.query.limit || 10000, 10);
const { subscriptions, total } = await subscriptions.list(req.params.listId, false, start, limit); const { subscriptions, total } = await subscriptions.list(list.id, false, start, limit);
res.status(200); res.status(200);
res.json({ res.json({
@ -150,7 +153,7 @@ router.getAsync('/subscriptions/:listId', async (req, res) => {
}); });
}); });
router.getAsync('/lists/:email', async (req, res) => { router.getAsync('/lists/:email', passport.loggedIn, async (req, res) => {
const lists = await subscriptions.getListsWithEmail(req.context, req.params.email); const lists = await subscriptions.getListsWithEmail(req.context, req.params.email);
res.status(200); res.status(200);
@ -160,7 +163,8 @@ router.getAsync('/lists/:email', async (req, res) => {
}); });
router.postAsync('/field/:listId', async (req, res) => { router.postAsync('/field/:listCid', passport.loggedIn, async (req, res) => {
const list = await lists.getByCid(req.context, req.params.listCid);
const input = {}; const input = {};
Object.keys(req.body).forEach(key => { Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim(); input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
@ -211,7 +215,7 @@ router.postAsync('/field/:listId', async (req, res) => {
orderManageBefore: visible ? 'end' : 'none' orderManageBefore: visible ? 'end' : 'none'
}; };
const id = await fields.create(req.context, req.params.listId, field); const id = await fields.create(req.context, list.id, field);
res.status(200); res.status(200);
res.json({ res.json({
@ -223,7 +227,7 @@ router.postAsync('/field/:listId', async (req, res) => {
}); });
router.postAsync('/blacklist/add', async (req, res) => { router.postAsync('/blacklist/add', passport.loggedIn, async (req, res) => {
let input = {}; let input = {};
Object.keys(req.body).forEach(key => { Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim(); input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
@ -240,7 +244,7 @@ router.postAsync('/blacklist/add', async (req, res) => {
}); });
router.postAsync('/blacklist/delete', async (req, res) => { router.postAsync('/blacklist/delete', passport.loggedIn, async (req, res) => {
let input = {}; let input = {};
Object.keys(req.body).forEach(key => { Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim(); input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
@ -257,7 +261,7 @@ router.postAsync('/blacklist/delete', async (req, res) => {
}); });
router.getAsync('/blacklist/get', async (req, res) => { router.getAsync('/blacklist/get', passport.loggedIn, async (req, res) => {
let start = parseInt(req.query.start || 0, 10); let start = parseInt(req.query.start || 0, 10);
let limit = parseInt(req.query.limit || 10000, 10); let limit = parseInt(req.query.limit || 10000, 10);
let search = req.query.search || ''; let search = req.query.search || '';

View file

@ -67,7 +67,6 @@ function changeSubscriptionData(listConf, subscription) {
const data = generateSubscriptionData(listConf); const data = generateSubscriptionData(listConf);
delete data.EMAIL; delete data.EMAIL;
const changedSubscription = Object.assign({}, subscription, data); const changedSubscription = Object.assign({}, subscription, data);
// TODO: Make sure values have actually changed.
return changedSubscription; return changedSubscription;
} }
@ -543,7 +542,6 @@ suite('Subscription use-cases', () => {
}); });
async function apiSubscribe(listConf, subscription) { async function apiSubscribe(listConf, subscription) {
await step('Add subscription via API call.', async () => { await step('Add subscription via API call.', async () => {
const response = await request({ const response = await request({
@ -596,8 +594,8 @@ suite('API Subscription use-cases', () => {
await page.mailSubscriptionConfirmed.fetchMail(subscription.EMAIL); await page.mailSubscriptionConfirmed.fetchMail(subscription.EMAIL);
}); });
await step('User navigates to manage subscription.', async () => { await step('User clicks the manage subscription button.', async () => {
await page.webManage.navigate({ ucid: subscription.ucid }); await page.mailSubscriptionConfirmed.click('manageLink');
}); });
await step('Systems shows a form containing the data submitted with the API call.', async () => { await step('Systems shows a form containing the data submitted with the API call.', async () => {