From 2160a103386751143c266100cca3a3c9b18f5bf4 Mon Sep 17 00:00:00 2001 From: witzig Date: Sat, 27 May 2017 13:28:40 +0200 Subject: [PATCH 1/7] Satisfy eslint linebreak-style --- test/e2e/lib/mocha-e2e.js | 434 +++++++++++++++++++------------------- test/e2e/lib/semaphore.js | 70 +++--- 2 files changed, 252 insertions(+), 252 deletions(-) diff --git a/test/e2e/lib/mocha-e2e.js b/test/e2e/lib/mocha-e2e.js index 16aeba06..96c557a2 100644 --- a/test/e2e/lib/mocha-e2e.js +++ b/test/e2e/lib/mocha-e2e.js @@ -1,217 +1,217 @@ -'use strict'; - -const Mocha = require('mocha'); -const color = Mocha.reporters.Base.color; -const Semaphore = require('./semaphore'); -const fs = require('fs-extra'); -const config = require('./config'); -const webdriver = require('selenium-webdriver'); - -const driver = new webdriver.Builder() - .forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs') - .build(); - - -const failHandlerRunning = new Semaphore(); - - -function UseCaseReporter(runner) { - Mocha.reporters.Base.call(this, runner); - - const self = this; - let indents = 0; - - function indent () { - return Array(indents).join(' '); - } - - runner.on('start', function () { - console.log(); - }); - - runner.on('suite', suite => { - ++indents; - console.log(color('suite', '%s%s'), indent(), suite.title); - }); - - runner.on('suite end', () => { - --indents; - if (indents === 1) { - console.log(); - } - }); - - runner.on('use-case', useCase => { - ++indents; - console.log(); - console.log(color('suite', '%sUse case: %s'), indent(), useCase.title); - }); - - runner.on('use-case end', () => { - --indents; - }); - - runner.on('steps', useCase => { - ++indents; - console.log(color('pass', '%s%s'), indent(), useCase.title); - }); - - runner.on('steps end', () => { - --indents; - }); - - runner.on('step pass', step => { - console.log(indent() + color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) + color('pass', ' %s'), step.title); - }); - - runner.on('step fail', step => { - console.log(indent() + color('fail', ' %s'), step.title); - }); - - runner.on('pending', test => { - const fmt = indent() + color('pending', ' - %s'); - console.log(fmt, test.title); - }); - - runner.on('pass', test => { - let fmt; - if (test.speed === 'fast') { - fmt = indent() + - color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) + - color('pass', ' %s'); - console.log(fmt, test.title); - } else { - fmt = indent() + - color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) + - color('pass', ' %s') + - color(test.speed, ' (%dms)'); - console.log(fmt, test.title, test.duration); - } - }); - - runner.on('fail', (test, err) => { - failHandlerRunning.enter(); - (async () => { - const currentUrl = await driver.getCurrentUrl(); - const info = `URL: ${currentUrl}`; - await fs.writeFile('last-failed-e2e-test.info', info); - await fs.writeFile('last-failed-e2e-test.html', await driver.getPageSource()); - await fs.writeFile('last-failed-e2e-test.png', new Buffer(await driver.takeScreenshot(), 'base64')); - failHandlerRunning.exit(); - })(); - - console.log(indent() + color('fail', ' %s'), test.title); - console.log(); - console.log(err); - console.log(); - console.log(`Snaphot of and info about the current page are in last-failed-e2e-test.*`); - }); - - runner.on('end', () => { - const stats = self.stats; - let fmt; - - console.log(); - - // passes - fmt = color('bright pass', ' ') + color('green', ' %d passing'); - console.log(fmt, stats.passes); - - // pending - if (stats.pending) { - fmt = color('pending', ' ') + color('pending', ' %d pending'); - console.log(fmt, stats.pending); - } - - // failures - if (stats.failures) { - fmt = color('fail', ' %d failing'); - console.log(fmt, stats.failures); - } - - console.log(); - }); -} - - -const mocha = new Mocha() - .timeout(120000) - .reporter(UseCaseReporter) - .ui('tdd'); - -mocha._originalRun = mocha.run; - - -let runner; -mocha.run = fn => { - runner = mocha._originalRun(async () => { - await failHandlerRunning.waitForEmpty(); - await driver.quit(); - - fn(); - }); -}; - - -async function useCaseExec(name, asyncFn) { - runner.emit('use-case', {title: name}); - - try { - await asyncFn(); - runner.emit('use-case end'); - } catch (err) { - runner.emit('use-case end'); - throw err; - } -} - -function useCase(name, asyncFn) { - if (asyncFn) { - return test('Use case: ' + name, () => useCaseExec(name, asyncFn)); - } else { - // Pending test - return test('Use case: ' + name); - } -} - -useCase.only = (name, asyncFn) => { - return test.only('Use case: ' + name, () => useCaseExec(name, asyncFn)); -}; - -useCase.skip = (name, asyncFn) => { - return test.skip('Use case: ' + name, () => useCaseExec(name, asyncFn)); -}; - -async function step(name, asyncFn) { - try { - await asyncFn(); - runner.emit('step pass', {title: name}); - } catch (err) { - runner.emit('step fail', {title: name}); - throw err; - } -} - -async function steps(name, asyncFn) { - try { - runner.emit('steps', {title: name}); - await asyncFn(); - runner.emit('steps end'); - } catch (err) { - runner.emit('step end'); - throw err; - } -} - -async function precondition(preConditionName, useCaseName, asyncFn) { - await steps(`Including use case "${useCaseName}" to satisfy precondition "${preConditionName}"`, asyncFn); -} - -module.exports = { - mocha, - useCase, - step, - steps, - precondition, - driver -}; \ No newline at end of file +'use strict'; + +const Mocha = require('mocha'); +const color = Mocha.reporters.Base.color; +const Semaphore = require('./semaphore'); +const fs = require('fs-extra'); +const config = require('./config'); +const webdriver = require('selenium-webdriver'); + +const driver = new webdriver.Builder() + .forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs') + .build(); + + +const failHandlerRunning = new Semaphore(); + + +function UseCaseReporter(runner) { + Mocha.reporters.Base.call(this, runner); + + const self = this; + let indents = 0; + + function indent () { + return Array(indents).join(' '); + } + + runner.on('start', function () { + console.log(); + }); + + runner.on('suite', suite => { + ++indents; + console.log(color('suite', '%s%s'), indent(), suite.title); + }); + + runner.on('suite end', () => { + --indents; + if (indents === 1) { + console.log(); + } + }); + + runner.on('use-case', useCase => { + ++indents; + console.log(); + console.log(color('suite', '%sUse case: %s'), indent(), useCase.title); + }); + + runner.on('use-case end', () => { + --indents; + }); + + runner.on('steps', useCase => { + ++indents; + console.log(color('pass', '%s%s'), indent(), useCase.title); + }); + + runner.on('steps end', () => { + --indents; + }); + + runner.on('step pass', step => { + console.log(indent() + color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) + color('pass', ' %s'), step.title); + }); + + runner.on('step fail', step => { + console.log(indent() + color('fail', ' %s'), step.title); + }); + + runner.on('pending', test => { + const fmt = indent() + color('pending', ' - %s'); + console.log(fmt, test.title); + }); + + runner.on('pass', test => { + let fmt; + if (test.speed === 'fast') { + fmt = indent() + + color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) + + color('pass', ' %s'); + console.log(fmt, test.title); + } else { + fmt = indent() + + color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) + + color('pass', ' %s') + + color(test.speed, ' (%dms)'); + console.log(fmt, test.title, test.duration); + } + }); + + runner.on('fail', (test, err) => { + failHandlerRunning.enter(); + (async () => { + const currentUrl = await driver.getCurrentUrl(); + const info = `URL: ${currentUrl}`; + await fs.writeFile('last-failed-e2e-test.info', info); + await fs.writeFile('last-failed-e2e-test.html', await driver.getPageSource()); + await fs.writeFile('last-failed-e2e-test.png', new Buffer(await driver.takeScreenshot(), 'base64')); + failHandlerRunning.exit(); + })(); + + console.log(indent() + color('fail', ' %s'), test.title); + console.log(); + console.log(err); + console.log(); + console.log(`Snaphot of and info about the current page are in last-failed-e2e-test.*`); + }); + + runner.on('end', () => { + const stats = self.stats; + let fmt; + + console.log(); + + // passes + fmt = color('bright pass', ' ') + color('green', ' %d passing'); + console.log(fmt, stats.passes); + + // pending + if (stats.pending) { + fmt = color('pending', ' ') + color('pending', ' %d pending'); + console.log(fmt, stats.pending); + } + + // failures + if (stats.failures) { + fmt = color('fail', ' %d failing'); + console.log(fmt, stats.failures); + } + + console.log(); + }); +} + + +const mocha = new Mocha() + .timeout(120000) + .reporter(UseCaseReporter) + .ui('tdd'); + +mocha._originalRun = mocha.run; + + +let runner; +mocha.run = fn => { + runner = mocha._originalRun(async () => { + await failHandlerRunning.waitForEmpty(); + await driver.quit(); + + fn(); + }); +}; + + +async function useCaseExec(name, asyncFn) { + runner.emit('use-case', {title: name}); + + try { + await asyncFn(); + runner.emit('use-case end'); + } catch (err) { + runner.emit('use-case end'); + throw err; + } +} + +function useCase(name, asyncFn) { + if (asyncFn) { + return test('Use case: ' + name, () => useCaseExec(name, asyncFn)); + } else { + // Pending test + return test('Use case: ' + name); + } +} + +useCase.only = (name, asyncFn) => { + return test.only('Use case: ' + name, () => useCaseExec(name, asyncFn)); +}; + +useCase.skip = (name, asyncFn) => { + return test.skip('Use case: ' + name, () => useCaseExec(name, asyncFn)); +}; + +async function step(name, asyncFn) { + try { + await asyncFn(); + runner.emit('step pass', {title: name}); + } catch (err) { + runner.emit('step fail', {title: name}); + throw err; + } +} + +async function steps(name, asyncFn) { + try { + runner.emit('steps', {title: name}); + await asyncFn(); + runner.emit('steps end'); + } catch (err) { + runner.emit('step end'); + throw err; + } +} + +async function precondition(preConditionName, useCaseName, asyncFn) { + await steps(`Including use case "${useCaseName}" to satisfy precondition "${preConditionName}"`, asyncFn); +} + +module.exports = { + mocha, + useCase, + step, + steps, + precondition, + driver +}; diff --git a/test/e2e/lib/semaphore.js b/test/e2e/lib/semaphore.js index 7c30c900..d22b8f71 100644 --- a/test/e2e/lib/semaphore.js +++ b/test/e2e/lib/semaphore.js @@ -1,35 +1,35 @@ -'use strict'; - -const Promise = require('bluebird'); - -class Semaphore { - constructor() { - this.counter = 0; - } - - enter() { - this.counter++; - } - - exit() { - this.counter--; - } - - async waitForEmpty() { - const self = this; - - function wait(resolve) { - if (self.counter == 0) { - resolve(); - } else { - setTimeout(wait, 500, resolve); - } - } - - return new Promise(resolve => { - setTimeout(wait, 500, resolve); - }) - } -} - -module.exports = Semaphore; \ No newline at end of file +'use strict'; + +const Promise = require('bluebird'); + +class Semaphore { + constructor() { + this.counter = 0; + } + + enter() { + this.counter++; + } + + exit() { + this.counter--; + } + + async waitForEmpty() { + const self = this; + + function wait(resolve) { + if (self.counter == 0) { + resolve(); + } else { + setTimeout(wait, 500, resolve); + } + } + + return new Promise(resolve => { + setTimeout(wait, 500, resolve); + }) + } +} + +module.exports = Semaphore; From 59912e3c2992db6a45d269c51352f30df0909889 Mon Sep 17 00:00:00 2001 From: vladimir Date: Sat, 27 May 2017 14:04:49 +0200 Subject: [PATCH 2/7] Separate disable opened/clicked tracker to 2 options --- lib/models/campaigns.js | 8 ++- lib/models/links.js | 100 +++++++++++++++------------ meta.json | 2 +- setup/sql/mailtrain-test.sql | 3 +- setup/sql/mailtrain.sql | 3 +- setup/sql/upgrade-00028.sql | 13 ++++ views/campaigns/create-rss.hbs | 10 ++- views/campaigns/create-triggered.hbs | 10 ++- views/campaigns/create.hbs | 10 ++- views/campaigns/edit-rss.hbs | 10 ++- views/campaigns/edit-triggered.hbs | 10 ++- views/campaigns/edit.hbs | 10 ++- views/campaigns/view.hbs | 5 +- 13 files changed, 134 insertions(+), 60 deletions(-) create mode 100644 setup/sql/upgrade-00028.sql diff --git a/lib/models/campaigns.js b/lib/models/campaigns.js index 1291cd78..4f8aa610 100644 --- a/lib/models/campaigns.js +++ b/lib/models/campaigns.js @@ -16,7 +16,7 @@ let _ = require('../translate')._; let util = require('util'); let tableHelpers = require('../table-helpers'); -let allowedKeys = ['description', 'from', 'address', 'reply_to', 'subject', 'editor_name', 'editor_data', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'tracking_disabled']; +let allowedKeys = ['description', 'from', 'address', 'reply_to', 'subject', 'editor_name', 'editor_data', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'click_tracking_disabled', 'open_tracking_disabled']; module.exports.list = (start, limit, callback) => { tableHelpers.list('campaigns', ['*'], 'scheduled', null, start, limit, callback); @@ -370,7 +370,8 @@ module.exports.create = (campaign, opts, callback) => { campaign = tools.convertKeys(campaign); let name = (campaign.name || '').toString().trim(); - campaign.trackingDisabled = campaign.trackingDisabled ? 1 : 0; + campaign.openTrackingDisabled = campaign.openTrackingDisabled ? 1 : 0; + campaign.clickTrackingDisabled = campaign.clickTrackingDisabled ? 1 : 0; opts = opts || {}; @@ -592,7 +593,8 @@ module.exports.update = (id, updates, callback) => { let campaign = tools.convertKeys(updates); let name = (campaign.name || '').toString().trim(); - campaign.trackingDisabled = campaign.trackingDisabled ? 1 : 0; + campaign.openTrackingDisabled = campaign.openTrackingDisabled ? 1 : 0; + campaign.clickTrackingDisabled = campaign.clickTrackingDisabled ? 1 : 0; if (!name) { return callback(new Error(_('Campaign Name must be set'))); diff --git a/lib/models/links.js b/lib/models/links.js index ed348ad3..e7ce5591 100644 --- a/lib/models/links.js +++ b/lib/models/links.js @@ -42,7 +42,7 @@ module.exports.countClick = (remoteIp, useragent, campaignCid, listCid, subscrip return callback(err); } - if (!data || data.campaign.trackingDisabled) { + if (!data || data.campaign.clickTrackingDisabled) { return callback(null, false); } @@ -158,7 +158,7 @@ module.exports.countOpen = (remoteIp, useragent, campaignCid, listCid, subscript return callback(err); } - if (!data || data.campaign.trackingDisabled) { + if (!data || data.campaign.openTrackingDisabled) { return callback(null, false); } @@ -268,56 +268,64 @@ module.exports.add = (url, campaignId, callback) => { }; module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message, callback) => { - if (campaign.trackingDisabled || !message || !message.trim()) { + if ((campaign.openTrackingDisabled && campaign.clickTrackingDisabled) || !message || !message.trim()) { // tracking is disabled, do not modify the message return setImmediate(() => callback(null, message)); } - let re = /(]* href\s*=[\s"']*)(http[^"'>\s]+)/gi; - let urls = new Set(); - (message || '').replace(re, (match, prefix, url) => { - urls.add(url); - }); - - let map = new Map(); - let vals = urls.values(); - + // insert tracking image - let inserted = false; - let imgUrl = urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid))); - let img = 'mt'; - message = message.replace(/<\/body\b/i, match => { - inserted = true; - return img + match; - }); - if (!inserted) { - message = message + img; - } - - let replaceUrls = () => { - callback(null, - message.replace(re, (match, prefix, url) => - prefix + (map.has(url) ? urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid), encodeURIComponent(map.get(url)))) : url))); - }; - - let storeNext = () => { - let urlItem = vals.next(); - if (urlItem.done) { - return replaceUrls(); - } - - module.exports.add(he.decode(urlItem.value, { - isAttributeValue: true - }), campaign.id, (err, linkId, cid) => { - if (err) { - log.error('Link', err); - return storeNext(); - } - map.set(urlItem.value, cid); - return storeNext(); + if (!campaign.openTrackingDisabled) { + let inserted = false; + let imgUrl = urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid))); + let img = 'mt'; + message = message.replace(/<\/body\b/i, match => { + inserted = true; + return img + match; + }); + if (!inserted) { + message = message + img; + } + if (campaign.clickTrackingDisabled) { + return callback(null, message); + } + } + + if (!campaign.clickTrackingDisabled) { + let re = /(]* href\s*=[\s"']*)(http[^"'>\s]+)/gi; + let urls = new Set(); + (message || '').replace(re, (match, prefix, url) => { + urls.add(url); }); - }; - storeNext(); + let map = new Map(); + let vals = urls.values(); + + let replaceUrls = () => { + callback(null, + message.replace(re, (match, prefix, url) => + prefix + (map.has(url) ? urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid), encodeURIComponent(map.get(url)))) : url))); + }; + + let storeNext = () => { + let urlItem = vals.next(); + if (urlItem.done) { + return replaceUrls(); + } + + module.exports.add(he.decode(urlItem.value, { + isAttributeValue: true + }), campaign.id, (err, linkId, cid) => { + if (err) { + log.error('Link', err); + return storeNext(); + } + map.set(urlItem.value, cid); + return storeNext(); + }); + }; + + storeNext(); + } }; function getSubscriptionData(campaignCid, listCid, subscriptionCid, callback) { diff --git a/meta.json b/meta.json index 22fe97fb..aa851e29 100644 --- a/meta.json +++ b/meta.json @@ -1,3 +1,3 @@ { - "schemaVersion": 27 + "schemaVersion": 28 } diff --git a/setup/sql/mailtrain-test.sql b/setup/sql/mailtrain-test.sql index 8ae97db9..6533f3a4 100644 --- a/setup/sql/mailtrain-test.sql +++ b/setup/sql/mailtrain-test.sql @@ -69,7 +69,8 @@ CREATE TABLE `campaigns` ( `html_prepared` longtext, `text` longtext, `status` tinyint(4) unsigned NOT NULL DEFAULT '1', - `tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', + `open_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', + `click_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', `scheduled` timestamp NULL DEFAULT NULL, `status_change` timestamp NULL DEFAULT NULL, `delivered` int(11) unsigned NOT NULL DEFAULT '0', diff --git a/setup/sql/mailtrain.sql b/setup/sql/mailtrain.sql index d56c9806..02817a37 100644 --- a/setup/sql/mailtrain.sql +++ b/setup/sql/mailtrain.sql @@ -65,7 +65,8 @@ CREATE TABLE `campaigns` ( `html_prepared` longtext, `text` longtext, `status` tinyint(4) unsigned NOT NULL DEFAULT '1', - `tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', + `click_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', + `open_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', `scheduled` timestamp NULL DEFAULT NULL, `status_change` timestamp NULL DEFAULT NULL, `delivered` int(11) unsigned NOT NULL DEFAULT '0', diff --git a/setup/sql/upgrade-00028.sql b/setup/sql/upgrade-00028.sql new file mode 100644 index 00000000..47212891 --- /dev/null +++ b/setup/sql/upgrade-00028.sql @@ -0,0 +1,13 @@ +# Header section +# Define incrementing schema version number +SET @schema_version = '28'; + +# Rename column tracking_disabled +ALTER TABLE `campaigns` ADD COLUMN `open_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL, ADD COLUMN `click_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL; +UPDATE `campaigns` SET `open_tracking_disabled` = `tracking_disabled`, `click_tracking_disabled` = `tracking_disabled`; +ALTER TABLE `campaigns` DROP COLUMN `tracking_disabled`; + +# Footer section +LOCK TABLES `settings` WRITE; +INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version; +UNLOCK TABLES; diff --git a/views/campaigns/create-rss.hbs b/views/campaigns/create-rss.hbs index 131a4a5d..709c1df9 100644 --- a/views/campaigns/create-rss.hbs +++ b/views/campaigns/create-rss.hbs @@ -84,7 +84,15 @@
+
+
+ +
+
+
diff --git a/views/campaigns/create-triggered.hbs b/views/campaigns/create-triggered.hbs index 8fdafbe5..f2711dc7 100644 --- a/views/campaigns/create-triggered.hbs +++ b/views/campaigns/create-triggered.hbs @@ -104,7 +104,15 @@
+
+
+ +
+
+
diff --git a/views/campaigns/create.hbs b/views/campaigns/create.hbs index b5869898..24c74fc1 100644 --- a/views/campaigns/create.hbs +++ b/views/campaigns/create.hbs @@ -110,7 +110,15 @@
+
+
+ +
+
+
diff --git a/views/campaigns/edit-rss.hbs b/views/campaigns/edit-rss.hbs index 3c13c859..967094bd 100644 --- a/views/campaigns/edit-rss.hbs +++ b/views/campaigns/edit-rss.hbs @@ -111,7 +111,15 @@
+
+
+ +
+
+
diff --git a/views/campaigns/edit-triggered.hbs b/views/campaigns/edit-triggered.hbs index cdefe5fc..b94c2b0d 100644 --- a/views/campaigns/edit-triggered.hbs +++ b/views/campaigns/edit-triggered.hbs @@ -103,7 +103,15 @@
+
+
+ +
+
+
diff --git a/views/campaigns/edit.hbs b/views/campaigns/edit.hbs index 098b502a..faa0053c 100644 --- a/views/campaigns/edit.hbs +++ b/views/campaigns/edit.hbs @@ -121,7 +121,15 @@
+
+
+ +
+
+
diff --git a/views/campaigns/view.hbs b/views/campaigns/view.hbs index a8e981b3..dcf278b1 100644 --- a/views/campaigns/view.hbs +++ b/views/campaigns/view.hbs @@ -164,7 +164,7 @@ - {{#unless trackingDisabled}} + {{#unless openTrackingDisabled}}
{{#translate}}Opened{{/translate}}
@@ -174,7 +174,8 @@
- + {{/unless}} + {{#unless clickTrackingDisabled}}
{{#translate}}Clicked{{/translate}}
From 1e3fe9682552cfe6de38aa7eeb94320b93366e72 Mon Sep 17 00:00:00 2001 From: vladimir Date: Sat, 27 May 2017 14:14:32 +0200 Subject: [PATCH 3/7] [bugfix] Blacklisted emails counter not set to 0 on campaign reset --- lib/models/campaigns.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/campaigns.js b/lib/models/campaigns.js index 7ef662c6..0fb4323d 100644 --- a/lib/models/campaigns.js +++ b/lib/models/campaigns.js @@ -829,7 +829,7 @@ module.exports.reset = (id, callback) => { return callback(err); } - connection.query('UPDATE campaigns SET `status`=1, `status_change`=NULL, `delivered`=0, `opened`=0, `clicks`=0, `bounced`=0, `complained`=0, `unsubscribed`=0 WHERE id=? LIMIT 1', [id], err => { + connection.query('UPDATE campaigns SET `status`=1, `status_change`=NULL, `delivered`=0, `opened`=0, `clicks`=0, `bounced`=0, `complained`=0, `unsubscribed`=0, `blacklisted`=0 WHERE id=? LIMIT 1', [id], err => { if (err) { connection.release(); return callback(err); From a9285e11811f9aacd0f52844ec06c37708418c98 Mon Sep 17 00:00:00 2001 From: witzig Date: Sat, 27 May 2017 14:24:08 +0200 Subject: [PATCH 4/7] Fixed eslint errors --- test/e2e/.eslintrc | 5 +++-- test/e2e/index.js | 4 +--- test/e2e/lib/mail.js | 2 +- test/e2e/lib/mocha-e2e.js | 4 ++-- test/e2e/lib/page.js | 5 ++--- test/e2e/lib/semaphore.js | 4 ++-- test/e2e/lib/web.js | 2 +- test/e2e/page-objects/user.js | 2 +- test/e2e/tests/login.js | 2 +- test/e2e/tests/subscription.js | 2 +- 10 files changed, 15 insertions(+), 17 deletions(-) diff --git a/test/e2e/.eslintrc b/test/e2e/.eslintrc index 836bac9a..a5157382 100644 --- a/test/e2e/.eslintrc +++ b/test/e2e/.eslintrc @@ -2,8 +2,9 @@ "parser": "babel-eslint", "rules": { "strict": 0, - "no-invalid-this": 0, - "no-unused-expressions": 0 + "no-console": 0, + "comma-dangle": 0, + "arrow-body-style": 0 }, "env": { "mocha": true diff --git a/test/e2e/index.js b/test/e2e/index.js index 5dd54d00..dce40875 100644 --- a/test/e2e/index.js +++ b/test/e2e/index.js @@ -1,11 +1,9 @@ 'use strict'; require('./lib/exit-unless-test'); -const { mocha, driver } = require('./lib/mocha-e2e'); +const mocha = require('./lib/mocha-e2e').mocha; const path = require('path'); -global.USE_SHARED_DRIVER = true; - const only = 'only'; const skip = 'skip'; diff --git a/test/e2e/lib/mail.js b/test/e2e/lib/mail.js index b0f41872..47d5a631 100644 --- a/test/e2e/lib/mail.js +++ b/test/e2e/lib/mail.js @@ -12,7 +12,7 @@ module.exports = (...extras) => page({ await this.waitUntilVisible(); }, - async ensureUrl(path) { + async ensureUrl(path) { // eslint-disable-line no-unused-vars throw new Error('Unsupported method.'); }, diff --git a/test/e2e/lib/mocha-e2e.js b/test/e2e/lib/mocha-e2e.js index 96c557a2..4ea1f66d 100644 --- a/test/e2e/lib/mocha-e2e.js +++ b/test/e2e/lib/mocha-e2e.js @@ -25,7 +25,7 @@ function UseCaseReporter(runner) { return Array(indents).join(' '); } - runner.on('start', function () { + runner.on('start', () => { console.log(); }); @@ -104,7 +104,7 @@ function UseCaseReporter(runner) { console.log(); console.log(err); console.log(); - console.log(`Snaphot of and info about the current page are in last-failed-e2e-test.*`); + console.log('Snaphot of and info about the current page are in last-failed-e2e-test.*'); }); runner.on('end', () => { diff --git a/test/e2e/lib/page.js b/test/e2e/lib/page.js index 07064af7..3026f671 100644 --- a/test/e2e/lib/page.js +++ b/test/e2e/lib/page.js @@ -1,6 +1,5 @@ 'use strict'; -const config = require('./config'); const webdriver = require('selenium-webdriver'); const By = webdriver.By; const until = webdriver.until; @@ -45,7 +44,7 @@ module.exports = (...extras) => Object.assign({ } for (const text of (this.textsToWaitFor || [])) { - await driver.wait(new webdriver.Condition(`for text "${text}"`, async (driver) => { + await driver.wait(new webdriver.Condition(`for text "${text}"`, async () => { return await this.containsText(text); }), waitTimeout); } @@ -58,7 +57,7 @@ module.exports = (...extras) => Object.assign({ }, async waitUntilVisibleAfterRefresh(selector) { - await driver.wait(new webdriver.Condition('for refresh', async (driver) => { + await driver.wait(new webdriver.Condition('for refresh', async () => { const val = await driver.executeScript('return document.mailTrainRefreshAcknowledged;'); return !val; }), waitTimeout); diff --git a/test/e2e/lib/semaphore.js b/test/e2e/lib/semaphore.js index d22b8f71..89366b3e 100644 --- a/test/e2e/lib/semaphore.js +++ b/test/e2e/lib/semaphore.js @@ -19,7 +19,7 @@ class Semaphore { const self = this; function wait(resolve) { - if (self.counter == 0) { + if (self.counter === 0) { resolve(); } else { setTimeout(wait, 500, resolve); @@ -28,7 +28,7 @@ class Semaphore { return new Promise(resolve => { setTimeout(wait, 500, resolve); - }) + }); } } diff --git a/test/e2e/lib/web.js b/test/e2e/lib/web.js index 692c0214..9e345219 100644 --- a/test/e2e/lib/web.js +++ b/test/e2e/lib/web.js @@ -15,7 +15,7 @@ module.exports = (...extras) => page({ path = pathOrParams; } else { const urlPattern = new UrlPattern(this.requestUrl || this.url); - path = urlPattern.stringify(pathOrParams) + path = urlPattern.stringify(pathOrParams); } const parsedUrl = url.parse(path); diff --git a/test/e2e/page-objects/user.js b/test/e2e/page-objects/user.js index 52f8377f..2facc345 100644 --- a/test/e2e/page-objects/user.js +++ b/test/e2e/page-objects/user.js @@ -22,7 +22,7 @@ module.exports = { url: '/users/account', elementsToWaitFor: ['form'], elements: { - form: `form[action="/users/account"]`, + form: 'form[action="/users/account"]', emailInput: 'form[action="/users/account"] input[name="email"]' } }), diff --git a/test/e2e/tests/login.js b/test/e2e/tests/login.js index 571389be..c4f47d35 100644 --- a/test/e2e/tests/login.js +++ b/test/e2e/tests/login.js @@ -7,7 +7,7 @@ const expect = require('chai').expect; const page = require('../page-objects/user'); const home = require('../page-objects/home'); -suite('Login use-cases', function() { +suite('Login use-cases', () => { before(() => driver.manage().deleteAllCookies()); test('User can access home page', async () => { diff --git a/test/e2e/tests/subscription.js b/test/e2e/tests/subscription.js index fec05937..dabdc914 100644 --- a/test/e2e/tests/subscription.js +++ b/test/e2e/tests/subscription.js @@ -67,7 +67,7 @@ async function subscriptionExistsPrecondition(subscription) { return subscription; } -suite('Subscription use-cases', function() { +suite('Subscription use-cases', () => { before(() => driver.manage().deleteAllCookies()); useCase('Subscription to a public list (main scenario)', async () => { From b1eebd980490fcefcec14b51425f5d708221aa04 Mon Sep 17 00:00:00 2001 From: witzig Date: Sat, 27 May 2017 14:29:16 +0200 Subject: [PATCH 5/7] Fixed waitUntilVisible should wait for selector, if present. This is used by waitForFlash. --- test/e2e/lib/page.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/e2e/lib/page.js b/test/e2e/lib/page.js index 3026f671..d455d895 100644 --- a/test/e2e/lib/page.js +++ b/test/e2e/lib/page.js @@ -35,6 +35,10 @@ module.exports = (...extras) => Object.assign({ async waitUntilVisible(selector) { await driver.wait(until.elementLocated(By.css('body')), waitTimeout); + if (selector) { + await driver.wait(until.elementLocated(By.css(selector)), waitTimeout); + } + for (const elem of (this.elementsToWaitFor || [])) { const sel = this.elements[elem]; if (!sel) { From 2c930c60d2f9ec29d0f685dbd6784c3f7682a49d Mon Sep 17 00:00:00 2001 From: witzig Date: Sat, 27 May 2017 20:15:01 +0200 Subject: [PATCH 6/7] Fixed ER_DUP_FIELDNAME during sqlinit --- setup/sql/mailtrain-test.sql | 3 +-- setup/sql/mailtrain.sql | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/setup/sql/mailtrain-test.sql b/setup/sql/mailtrain-test.sql index 6533f3a4..8ae97db9 100644 --- a/setup/sql/mailtrain-test.sql +++ b/setup/sql/mailtrain-test.sql @@ -69,8 +69,7 @@ CREATE TABLE `campaigns` ( `html_prepared` longtext, `text` longtext, `status` tinyint(4) unsigned NOT NULL DEFAULT '1', - `open_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', - `click_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', + `tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', `scheduled` timestamp NULL DEFAULT NULL, `status_change` timestamp NULL DEFAULT NULL, `delivered` int(11) unsigned NOT NULL DEFAULT '0', diff --git a/setup/sql/mailtrain.sql b/setup/sql/mailtrain.sql index 02817a37..d56c9806 100644 --- a/setup/sql/mailtrain.sql +++ b/setup/sql/mailtrain.sql @@ -65,8 +65,7 @@ CREATE TABLE `campaigns` ( `html_prepared` longtext, `text` longtext, `status` tinyint(4) unsigned NOT NULL DEFAULT '1', - `click_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', - `open_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', + `tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', `scheduled` timestamp NULL DEFAULT NULL, `status_change` timestamp NULL DEFAULT NULL, `delivered` int(11) unsigned NOT NULL DEFAULT '0', From 19a563bc6a82c1102411bdc37431abed287f9cd1 Mon Sep 17 00:00:00 2001 From: witzig Date: Sun, 28 May 2017 00:11:30 +0200 Subject: [PATCH 7/7] Remaining e2e subscription tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @bures, I’m not too happy with switchToList(). Suggestions? If we keep it, it should probably be added to every useCase. --- setup/sql/mailtrain-test.sql | 173 +++++++++++++++++++++-- test/e2e/lib/config.js | 37 ++++- test/e2e/page-objects/subscription.js | 32 ++++- test/e2e/tests/subscription.js | 195 +++++++++++++++++++++++++- 4 files changed, 410 insertions(+), 27 deletions(-) diff --git a/setup/sql/mailtrain-test.sql b/setup/sql/mailtrain-test.sql index 8ae97db9..11dc6681 100644 --- a/setup/sql/mailtrain-test.sql +++ b/setup/sql/mailtrain-test.sql @@ -69,7 +69,6 @@ CREATE TABLE `campaigns` ( `html_prepared` longtext, `text` longtext, `status` tinyint(4) unsigned NOT NULL DEFAULT '1', - `tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', `scheduled` timestamp NULL DEFAULT NULL, `status_change` timestamp NULL DEFAULT NULL, `delivered` int(11) unsigned NOT NULL DEFAULT '0', @@ -80,6 +79,8 @@ CREATE TABLE `campaigns` ( `bounced` int(1) unsigned NOT NULL DEFAULT '0', `complained` int(1) unsigned NOT NULL DEFAULT '0', `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `open_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', + `click_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `cid` (`cid`), KEY `name` (`name`(191)), @@ -93,8 +94,8 @@ CREATE TABLE `confirmations` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `cid` varchar(255) CHARACTER SET ascii NOT NULL, `list` int(11) unsigned NOT NULL, - `email` varchar(255) NOT NULL, - `opt_in_ip` varchar(100) DEFAULT NULL, + `action` varchar(100) NOT NULL, + `ip` varchar(100) DEFAULT NULL, `data` text NOT NULL, `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), @@ -193,11 +194,17 @@ CREATE TABLE `lists` ( `subscribers` int(11) unsigned DEFAULT '0', `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `public_subscribe` tinyint(1) unsigned NOT NULL DEFAULT '1', + `unsubscription_mode` int(11) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `cid` (`cid`), KEY `name` (`name`(191)) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; -INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`) VALUES (1,'Hkj1vCoJb',NULL,'01 Testlist - Public Subscribe','',0,NOW(),1); +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4; +INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (1,'Hkj1vCoJb',0,'#1 (one-step, no form)','',0,NOW(),1,0); +INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (2,'SktV4HDZ-',NULL,'#2 (one-step, with form)','',0,NOW(),1,1); +INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (3,'BkdvNBw-W',NULL,'#3 (two-step, no form)','',0,NOW(),1,2); +INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (4,'rJMKVrDZ-',NULL,'#4 (two-step, with form)','',0,NOW(),1,3); +INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (5,'SJgoNSw-W',NULL,'#5 (manual unsubscribe)','',0,NOW(),1,4); +INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (6,'HyveEPvWW',NULL,'#6 non-public','',0,NOW(),0,0); CREATE TABLE `queued` ( `campaign` int(11) unsigned NOT NULL, `list` int(11) unsigned NOT NULL, @@ -269,7 +276,7 @@ CREATE TABLE `settings` ( `value` text NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `key` (`key`) -) ENGINE=InnoDB AUTO_INCREMENT=112 DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB AUTO_INCREMENT=114 DEFAULT CHARSET=utf8mb4; INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','localhost'); INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','5587'); INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','NONE'); @@ -286,7 +293,7 @@ INSERT INTO `settings` (`id`, `key`, `value`) VALUES (13,'default_from','My Awes INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com'); INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message'); INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','https://mailtrain.org'); -INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','27'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','29'); INSERT INTO `settings` (`id`, `key`, `value`) VALUES (46,'ua_code',''); INSERT INTO `settings` (`id`, `key`, `value`) VALUES (47,'shoutout',''); INSERT INTO `settings` (`id`, `key`, `value`) VALUES (54,'mail_transport','smtp'); @@ -361,6 +368,146 @@ CREATE TABLE `subscription__1` ( KEY `latest_click` (`latest_click`), KEY `created` (`created`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `subscription__2` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` varchar(255) CHARACTER SET ascii NOT NULL, + `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', + `opt_in_ip` varchar(100) DEFAULT NULL, + `opt_in_country` varchar(2) DEFAULT NULL, + `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, + `imported` int(11) unsigned DEFAULT NULL, + `status` tinyint(4) unsigned NOT NULL DEFAULT '1', + `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', + `status_change` timestamp NULL DEFAULT NULL, + `latest_open` timestamp NULL DEFAULT NULL, + `latest_click` timestamp NULL DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `first_name` varchar(255) DEFAULT NULL, + `last_name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + UNIQUE KEY `cid` (`cid`), + KEY `status` (`status`), + KEY `first_name` (`first_name`(191)), + KEY `last_name` (`last_name`(191)), + KEY `subscriber_tz` (`tz`), + KEY `is_test` (`is_test`), + KEY `latest_open` (`latest_open`), + KEY `latest_click` (`latest_click`), + KEY `created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `subscription__3` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` varchar(255) CHARACTER SET ascii NOT NULL, + `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', + `opt_in_ip` varchar(100) DEFAULT NULL, + `opt_in_country` varchar(2) DEFAULT NULL, + `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, + `imported` int(11) unsigned DEFAULT NULL, + `status` tinyint(4) unsigned NOT NULL DEFAULT '1', + `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', + `status_change` timestamp NULL DEFAULT NULL, + `latest_open` timestamp NULL DEFAULT NULL, + `latest_click` timestamp NULL DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `first_name` varchar(255) DEFAULT NULL, + `last_name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + UNIQUE KEY `cid` (`cid`), + KEY `status` (`status`), + KEY `first_name` (`first_name`(191)), + KEY `last_name` (`last_name`(191)), + KEY `subscriber_tz` (`tz`), + KEY `is_test` (`is_test`), + KEY `latest_open` (`latest_open`), + KEY `latest_click` (`latest_click`), + KEY `created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `subscription__4` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` varchar(255) CHARACTER SET ascii NOT NULL, + `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', + `opt_in_ip` varchar(100) DEFAULT NULL, + `opt_in_country` varchar(2) DEFAULT NULL, + `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, + `imported` int(11) unsigned DEFAULT NULL, + `status` tinyint(4) unsigned NOT NULL DEFAULT '1', + `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', + `status_change` timestamp NULL DEFAULT NULL, + `latest_open` timestamp NULL DEFAULT NULL, + `latest_click` timestamp NULL DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `first_name` varchar(255) DEFAULT NULL, + `last_name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + UNIQUE KEY `cid` (`cid`), + KEY `status` (`status`), + KEY `first_name` (`first_name`(191)), + KEY `last_name` (`last_name`(191)), + KEY `subscriber_tz` (`tz`), + KEY `is_test` (`is_test`), + KEY `latest_open` (`latest_open`), + KEY `latest_click` (`latest_click`), + KEY `created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `subscription__5` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` varchar(255) CHARACTER SET ascii NOT NULL, + `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', + `opt_in_ip` varchar(100) DEFAULT NULL, + `opt_in_country` varchar(2) DEFAULT NULL, + `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, + `imported` int(11) unsigned DEFAULT NULL, + `status` tinyint(4) unsigned NOT NULL DEFAULT '1', + `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', + `status_change` timestamp NULL DEFAULT NULL, + `latest_open` timestamp NULL DEFAULT NULL, + `latest_click` timestamp NULL DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `first_name` varchar(255) DEFAULT NULL, + `last_name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + UNIQUE KEY `cid` (`cid`), + KEY `status` (`status`), + KEY `first_name` (`first_name`(191)), + KEY `last_name` (`last_name`(191)), + KEY `subscriber_tz` (`tz`), + KEY `is_test` (`is_test`), + KEY `latest_open` (`latest_open`), + KEY `latest_click` (`latest_click`), + KEY `created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `subscription__6` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` varchar(255) CHARACTER SET ascii NOT NULL, + `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', + `opt_in_ip` varchar(100) DEFAULT NULL, + `opt_in_country` varchar(2) DEFAULT NULL, + `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, + `imported` int(11) unsigned DEFAULT NULL, + `status` tinyint(4) unsigned NOT NULL DEFAULT '1', + `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', + `status_change` timestamp NULL DEFAULT NULL, + `latest_open` timestamp NULL DEFAULT NULL, + `latest_click` timestamp NULL DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `first_name` varchar(255) DEFAULT NULL, + `last_name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + UNIQUE KEY `cid` (`cid`), + KEY `status` (`status`), + KEY `first_name` (`first_name`(191)), + KEY `last_name` (`last_name`(191)), + KEY `subscriber_tz` (`tz`), + KEY `is_test` (`is_test`), + KEY `latest_open` (`latest_open`), + KEY `latest_click` (`latest_click`), + KEY `created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `templates` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL DEFAULT '', @@ -422,14 +569,14 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/blantyre',120); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/brazzaville',60); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bujumbura',120); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/cairo',120); -INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/casablanca',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/casablanca',0); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/ceuta',120); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/conakry',0); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dakar',0); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dar_es_salaam',180); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/djibouti',180); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/douala',60); -INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/el_aaiun',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/el_aaiun',0); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/freetown',0); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/gaborone',120); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/harare',120); @@ -603,7 +750,7 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rio_branco',-300); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rosario',-180); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santarem',-180); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santa_isabel',-420); -INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santiago',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santiago',-240); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santo_domingo',-240); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/sao_paulo',-180); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/scoresbysund',0); @@ -788,8 +935,8 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/pacific',-420); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/saskatchewan',-360); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/yukon',-420); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cet',120); -INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/continental',-180); -INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/easterisland',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/continental',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/easterisland',-360); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cst6cdt',-300); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cuba',-240); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('eet',180); @@ -936,7 +1083,7 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/auckland',720); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/bougainville',660); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chatham',765); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chuuk',600); -INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/easter',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/easter',-360); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/efate',660); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/enderbury',780); INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/fakaofo',780); diff --git a/test/e2e/lib/config.js b/test/e2e/lib/config.js index a9d191be..223d2103 100644 --- a/test/e2e/lib/config.js +++ b/test/e2e/lib/config.js @@ -13,15 +13,46 @@ module.exports = { } }, lists: { - one: { + l1: { id: 1, cid: 'Hkj1vCoJb', publicSubscribe: 1, - unsubscriptionMode: 0 + unsubscriptionMode: 0, // (one-step, no form) + }, + l2: { + id: 2, + cid: 'SktV4HDZ-', + publicSubscribe: 1, + unsubscriptionMode: 1, // (one-step, with form) + }, + l3: { + id: 3, + cid: 'BkdvNBw-W', + publicSubscribe: 1, + unsubscriptionMode: 2, // (two-step, no form) + }, + l4: { + id: 4, + cid: 'rJMKVrDZ-', + publicSubscribe: 1, + unsubscriptionMode: 3, // (two-step, with form) + }, + l5: { + id: 5, + cid: 'SJgoNSw-W', + publicSubscribe: 1, + unsubscriptionMode: 4, // (manual unsubscribe) + }, + l6: { + id: 6, + cid: 'HyveEPvWW', + publicSubscribe: 0, + unsubscriptionMode: 0, // (one-step, no form) } }, settings: { - 'service-url' : 'http://localhost:' + config.www.port + '/', + 'service-url': 'http://localhost:' + config.www.port + '/', + 'admin-email': 'admin@example.com', 'default-homepage': 'https://mailtrain.org', 'smtp-hostname': config.testserver.host, 'smtp-port': config.testserver.port, diff --git a/test/e2e/page-objects/subscription.js b/test/e2e/page-objects/subscription.js index 1d431b49..6d3d98d9 100644 --- a/test/e2e/page-objects/subscription.js +++ b/test/e2e/page-objects/subscription.js @@ -19,6 +19,11 @@ module.exports = list => ({ } }), + webSubscribeNonPublic: web({ + url: `/subscription/${list.cid}`, + textsToWaitFor: ['The list does not allow public subscriptions'], + }), + webConfirmSubscriptionNotice: web({ url: `/subscription/${list.cid}/confirm-subscription-notice`, textsToWaitFor: ['We need to confirm your email address'] @@ -118,15 +123,34 @@ module.exports = list => ({ } }), - /* - webUnsubscribe: web({ // FIXME + webUnsubscribe: web({ elementsToWaitFor: ['submitButton'], + textsToWaitFor: ['Unsubscribe'], elements: { submitButton: 'a[href="#submit"]' } }), -*/ + webConfirmUnsubscriptionNotice: web({ + url: `/subscription/${list.cid}/confirm-unsubscription-notice`, + textsToWaitFor: ['We need to confirm your email address'] + }), + + mailConfirmUnsubscription: mail({ + elementsToWaitFor: ['confirmLink'], + textsToWaitFor: ['Please Confirm Unsubscription'], + elements: { + confirmLink: `a[href^="${config.settings['service-url']}subscription/confirm/unsubscribe/"]` + } + }), + + webManualUnsubscribeNotice: web({ + url: `/subscription/${list.cid}/manual-unsubscribe-notice`, + elementsToWaitFor: ['contactLink'], + textsToWaitFor: ['Online Unsubscription Is Not Possible', config.settings['admin-email']], + elements: { + contactLink: `a[href^="mailto:${config.settings['admin-email']}"]` + } + }), }); - diff --git a/test/e2e/tests/subscription.js b/test/e2e/tests/subscription.js index dabdc914..7e2ab262 100644 --- a/test/e2e/tests/subscription.js +++ b/test/e2e/tests/subscription.js @@ -4,8 +4,13 @@ const config = require('../lib/config'); const { useCase, step, precondition, driver } = require('../lib/mocha-e2e'); const shortid = require('shortid'); const expect = require('chai').expect; +const createPage = require('../page-objects/subscription'); -const page = require('../page-objects/subscription')(config.lists.one); +let page = createPage(config.lists.l1); + +function switchToList(list) { + page = createPage(config.lists[list]); +} function generateEmail() { return 'keep.' + shortid.generate() + '@mailtrain.org'; @@ -116,9 +121,17 @@ suite('Subscription use-cases', () => { }); - useCase('Subscription to a non-public list'); + useCase('Subscription to a non-public list', async () => { + switchToList('l6'); + + await step('User navigates to list subscription page and sees message that this list does not allow public subscriptions.', async () => { + await page.webSubscribeNonPublic.navigate(); + }); + }); useCase('Change profile info', async () => { + switchToList('l1'); + const subscription = await subscriptionExistsPrecondition({ email: generateEmail(), firstName: 'John', @@ -236,13 +249,181 @@ suite('Subscription use-cases', () => { }); }); - useCase('Unsubscription from list #2 (one-step, with form).'); + useCase('Unsubscription from list #2 (one-step, with form).', async () => { + switchToList('l2'); - useCase('Unsubscription from list #3 (two-step, no form).'); + const subscription = await subscriptionExistsPrecondition({ + email: generateEmail() + }); - useCase('Unsubscription from list #4 (two-step, with form).'); + await step('User clicks the unsubscribe button.', async () => { + await page.mailSubscriptionConfirmed.click('unsubscribeLink'); + }); - useCase('Unsubscription from list #5 (manual unsubscribe).'); + await step('Systems shows a form to unsubscribe.', async () => { + await page.webUnsubscribe.waitUntilVisibleAfterRefresh(); + }); + + await step('User confirms unsubscribe and clicks the unsubscribe button.', async () => { + await page.webUnsubscribe.submit(); + }); + + await step('System shows a notice that confirms unsubscription.', async () => { + await page.webUnsubscribedNotice.waitUntilVisibleAfterRefresh(); + }); + + await step('System sends an email that confirms unsubscription.', async () => { + await page.mailUnsubscriptionConfirmed.fetchMail(subscription.email); + }); + }); + + useCase('Unsubscription from list #3 (two-step, no form).', async () => { + switchToList('l3'); + + const subscription = await subscriptionExistsPrecondition({ + email: generateEmail() + }); + + await step('User clicks the unsubscribe button.', async () => { + await page.mailSubscriptionConfirmed.click('unsubscribeLink'); + }); + + await step('System shows a notice that further instructions are in the email.', async () => { + await page.webConfirmUnsubscriptionNotice.waitUntilVisibleAfterRefresh(); + }); + + await step('System sends an email with a link to confirm unsubscription.', async () => { + await page.mailConfirmUnsubscription.fetchMail(subscription.email); + }); + + await step('User clicks the confirm unsubscribe button in the email.', async () => { + await page.mailConfirmUnsubscription.click('confirmLink'); + }); + + await step('System shows a notice that confirms unsubscription.', async () => { + await page.webUnsubscribedNotice.waitUntilVisibleAfterRefresh(); + }); + + await step('System sends an email that confirms unsubscription.', async () => { + await page.mailUnsubscriptionConfirmed.fetchMail(subscription.email); + }); + }); + + useCase('Unsubscription from list #4 (two-step, with form).', async () => { + switchToList('l4'); + + const subscription = await subscriptionExistsPrecondition({ + email: generateEmail() + }); + + await step('User clicks the unsubscribe button.', async () => { + await page.mailSubscriptionConfirmed.click('unsubscribeLink'); + }); + + await step('Systems shows a form to unsubscribe.', async () => { + await page.webUnsubscribe.waitUntilVisibleAfterRefresh(); + }); + + await step('User confirms unsubscribe and clicks the unsubscribe button.', async () => { + await page.webUnsubscribe.submit(); + }); + + await step('System shows a notice that further instructions are in the email.', async () => { + await page.webConfirmUnsubscriptionNotice.waitUntilVisibleAfterRefresh(); + }); + + await step('System sends an email with a link to confirm unsubscription.', async () => { + await page.mailConfirmUnsubscription.fetchMail(subscription.email); + }); + + await step('User clicks the confirm unsubscribe button in the email.', async () => { + await page.mailConfirmUnsubscription.click('confirmLink'); + }); + + await step('System shows a notice that confirms unsubscription.', async () => { + await page.webUnsubscribedNotice.waitUntilVisibleAfterRefresh(); + }); + + await step('System sends an email that confirms unsubscription.', async () => { + await page.mailUnsubscriptionConfirmed.fetchMail(subscription.email); + }); + }); + + useCase('Unsubscription from list #5 (manual unsubscribe).', async () => { + switchToList('l5'); + + await subscriptionExistsPrecondition({ + email: generateEmail() + }); + + await step('User clicks the unsubscribe button.', async () => { + await page.mailSubscriptionConfirmed.click('unsubscribeLink'); + }); + + await step('Systems shows a notice that online unsubscription is not possible.', async () => { + await page.webManualUnsubscribeNotice.waitUntilVisibleAfterRefresh(); + }); + }); + + useCase('Resubscription.', async () => { + switchToList('l1'); + + const subscription = await subscriptionExistsPrecondition({ + email: generateEmail(), + firstName: 'John', + lastName: 'Doe' + }); + + await step('User clicks the unsubscribe button.', async () => { + await page.mailSubscriptionConfirmed.click('unsubscribeLink'); + }); + + await step('System shows a notice that confirms unsubscription.', async () => { + await page.webUnsubscribedNotice.waitUntilVisibleAfterRefresh(); + }); + + await step('System sends an email that confirms unsubscription.', async () => { + await page.mailUnsubscriptionConfirmed.fetchMail(subscription.email); + }); + + await step('User clicks the resubscribe button.', async () => { + await page.mailUnsubscriptionConfirmed.click('resubscribeLink'); + }); + + await step('Systems shows the subscription form. The form contains data entered during initial subscription.', async () => { + await page.webSubscribe.waitUntilVisibleAfterRefresh(); + expect(await page.webSubscribe.getValue('emailInput')).to.equal(subscription.email); + expect(await page.webSubscribe.getValue('firstNameInput')).to.equal(subscription.firstName); + expect(await page.webSubscribe.getValue('lastNameInput')).to.equal(subscription.lastName); + }); + + await step('User submits the subscription form.', async () => { + await page.webSubscribe.submit(); + }); + + await step('System shows a notice that further instructions are in the email.', async () => { + await page.webConfirmSubscriptionNotice.waitUntilVisibleAfterRefresh(); + }); + + await step('System sends an email with a link to confirm the subscription.', async () => { + await page.mailConfirmSubscription.fetchMail(subscription.email); + }); + + await step('User clicks confirm subscription in the email', async () => { + await page.mailConfirmSubscription.click('confirmLink'); + }); + + await step('System shows a notice that subscription has been confirmed.', async () => { + await page.webSubscribedNotice.waitUntilVisibleAfterRefresh(); + }); + + await step('System sends an email with subscription confirmation. The manage and unsubscribe links are identical with the initial subscription.', async () => { + await page.mailSubscriptionConfirmed.fetchMail(subscription.email); + const unsubscribeLink = await page.mailSubscriptionConfirmed.getHref('unsubscribeLink'); + const manageLink = await page.mailSubscriptionConfirmed.getHref('manageLink'); + expect(subscription.unsubscribeLink).to.equal(unsubscribeLink); + expect(subscription.manageLink).to.equal(manageLink); + }); + }); - useCase('Resubscription.'); // This one is supposed to check that values pre-filled in resubscription (i.e. the re-subscribe link in unsubscription confirmation) are the same as the ones used before. });