feat(email): reinstate account verification reminder emails
This commit is contained in:
Родитель
57f58917e1
Коммит
7bd920e7e4
|
@ -876,7 +876,47 @@ const conf = convict({
|
||||||
env: 'RECOVERY_CODE_NOTIFY_LOW_COUNT'
|
env: 'RECOVERY_CODE_NOTIFY_LOW_COUNT'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
verificationReminders: {
|
||||||
|
rolloutRate: {
|
||||||
|
doc: 'Rollout rate for verification reminder emails, in the range 0 .. 1',
|
||||||
|
default: 1,
|
||||||
|
env: 'VERIFICATION_REMINDERS_ROLLOUT_RATE',
|
||||||
|
format: Number,
|
||||||
|
},
|
||||||
|
firstInterval: {
|
||||||
|
doc: 'Time since account creation after which the first reminder is sent',
|
||||||
|
default: '1 day',
|
||||||
|
env: 'VERIFICATION_REMINDERS_FIRST_INTERVAL',
|
||||||
|
format: 'duration',
|
||||||
|
},
|
||||||
|
secondInterval: {
|
||||||
|
doc: 'Time since account creation after which the second reminder is sent',
|
||||||
|
default: '5 days',
|
||||||
|
env: 'VERIFICATION_REMINDERS_SECOND_INTERVAL',
|
||||||
|
format: 'duration',
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
prefix: {
|
||||||
|
default: 'verificationReminders:',
|
||||||
|
doc: 'Key prefix for the verification reminders Redis pool',
|
||||||
|
env: 'VERIFICATION_REMINDERS_REDIS_PREFIX',
|
||||||
|
format: String,
|
||||||
|
},
|
||||||
|
maxConnections: {
|
||||||
|
default: 10,
|
||||||
|
doc: 'Maximum connection count for the verification reminders Redis pool',
|
||||||
|
env: 'VERIFICATION_REMINDERS_REDIS_MAX_CONNECTIONS',
|
||||||
|
format: 'nat',
|
||||||
|
},
|
||||||
|
minConnections: {
|
||||||
|
default: 1,
|
||||||
|
doc: 'Minimum connection count for the verification reminders Redis pool',
|
||||||
|
env: 'VERIFICATION_REMINDERS_REDIS_MIN_CONNECTIONS',
|
||||||
|
format: 'nat',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle configuration files. you can specify a CSV list of configuration
|
// handle configuration files. you can specify a CSV list of configuration
|
||||||
|
|
|
@ -1785,10 +1785,10 @@ not just the one being attached to the Firefox account.
|
||||||
Opaque alphanumeric token to be included in verification links.
|
Opaque alphanumeric token to be included in verification links.
|
||||||
<!--end-request-body-post-recovery_emailverify_code-service-->
|
<!--end-request-body-post-recovery_emailverify_code-service-->
|
||||||
|
|
||||||
* `reminder`: *string, max(32), alphanum, optional*
|
* `reminder`: *string, regex(/^(?:first|second)$/), optional*
|
||||||
|
|
||||||
<!--begin-request-body-post-recovery_emailverify_code-reminder-->
|
<!--begin-request-body-post-recovery_emailverify_code-reminder-->
|
||||||
Deprecated.
|
Indicates that verification originates from a reminder email.
|
||||||
<!--end-request-body-post-recovery_emailverify_code-reminder-->
|
<!--end-request-body-post-recovery_emailverify_code-reminder-->
|
||||||
|
|
||||||
* `type`: *string, max(32), alphanum, optional*
|
* `type`: *string, max(32), alphanum, optional*
|
||||||
|
|
|
@ -168,6 +168,7 @@ in a sign-in or sign-up flow:
|
||||||
|`signinCode.consumed`|A sign-in code has been consumed on the server.|
|
|`signinCode.consumed`|A sign-in code has been consumed on the server.|
|
||||||
|`account.confirmed`|Sign-in to an existing account has been confirmed via email.|
|
|`account.confirmed`|Sign-in to an existing account has been confirmed via email.|
|
||||||
|`account.verified`|A new account has been verified via email.|
|
|`account.verified`|A new account has been verified via email.|
|
||||||
|
|`account.reminder.${reminder}`|A new account has been verified via a reminder email.|
|
||||||
|`account.keyfetch`|Sync encryption keys have been fetched.|
|
|`account.keyfetch`|Sync encryption keys have been fetched.|
|
||||||
|`account.signed`|A certificate has been signed.|
|
|`account.signed`|A certificate has been signed.|
|
||||||
|`account.reset`|An account has been reset.|
|
|`account.reset`|An account has been reset.|
|
||||||
|
|
|
@ -105,6 +105,11 @@ module.exports = (log, config) => {
|
||||||
throw new TypeError('Missing argument');
|
throw new TypeError('Missing argument');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verificationReminders = require('../verification-reminders')(log, config);
|
||||||
|
verificationReminders.keys.forEach(key => {
|
||||||
|
EMAIL_TYPES[`verificationReminder${key[0].toUpperCase()}${key.substr(1)}Email`] = 'registration';
|
||||||
|
});
|
||||||
|
|
||||||
const transformEvent = initialize(config.oauth.clientIds, EVENTS, FUZZY_EVENTS);
|
const transformEvent = initialize(config.oauth.clientIds, EVENTS, FUZZY_EVENTS);
|
||||||
|
|
||||||
return receiveEvent;
|
return receiveEvent;
|
||||||
|
|
|
@ -23,7 +23,7 @@ const MS_ONE_DAY = MS_ONE_HOUR * 24;
|
||||||
const MS_ONE_WEEK = MS_ONE_DAY * 7;
|
const MS_ONE_WEEK = MS_ONE_DAY * 7;
|
||||||
const MS_ONE_MONTH = MS_ONE_DAY * 30;
|
const MS_ONE_MONTH = MS_ONE_DAY * 30;
|
||||||
|
|
||||||
module.exports = (log, db, mailer, Password, config, customs, signinUtils, push) => {
|
module.exports = (log, db, mailer, Password, config, customs, signinUtils, push, verificationReminders) => {
|
||||||
const tokenCodeConfig = config.signinConfirmation.tokenVerificationCode;
|
const tokenCodeConfig = config.signinConfirmation.tokenVerificationCode;
|
||||||
const tokenCodeLifetime = tokenCodeConfig && tokenCodeConfig.codeLifetime || MS_ONE_HOUR;
|
const tokenCodeLifetime = tokenCodeConfig && tokenCodeConfig.codeLifetime || MS_ONE_HOUR;
|
||||||
const tokenCodeLength = tokenCodeConfig && tokenCodeConfig.codeLength || 8;
|
const tokenCodeLength = tokenCodeConfig && tokenCodeConfig.codeLength || 8;
|
||||||
|
@ -279,6 +279,8 @@ module.exports = (log, db, mailer, Password, config, customs, signinUtils, push)
|
||||||
tokenVerificationId: tokenVerificationId
|
tokenVerificationId: tokenVerificationId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return verificationReminders.create(account.uid);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
log.error('mailer.sendVerifyCode.1', { err: err});
|
log.error('mailer.sendVerifyCode.1', { err: err});
|
||||||
|
|
|
@ -14,7 +14,9 @@ const validators = require('./validators');
|
||||||
|
|
||||||
const HEX_STRING = validators.HEX_STRING;
|
const HEX_STRING = validators.HEX_STRING;
|
||||||
|
|
||||||
module.exports = (log, db, mailer, config, customs, push) => {
|
module.exports = (log, db, mailer, config, customs, push, verificationReminders) => {
|
||||||
|
const REMINDER_PATTERN = new RegExp(`^(?:${verificationReminders.keys.join('|')})$`);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
@ -271,8 +273,7 @@ module.exports = (log, db, mailer, config, customs, push) => {
|
||||||
uid: isA.string().max(32).regex(HEX_STRING).required(),
|
uid: isA.string().max(32).regex(HEX_STRING).required(),
|
||||||
code: isA.string().min(32).max(32).regex(HEX_STRING).required(),
|
code: isA.string().min(32).max(32).regex(HEX_STRING).required(),
|
||||||
service: validators.service,
|
service: validators.service,
|
||||||
// TODO: drop this param once it is no longer sent by clients
|
reminder: isA.string().regex(REMINDER_PATTERN).optional(),
|
||||||
reminder: isA.string().max(32).alphanum().optional(),
|
|
||||||
type: isA.string().max(32).alphanum().optional(),
|
type: isA.string().max(32).alphanum().optional(),
|
||||||
marketingOptIn: isA.boolean()
|
marketingOptIn: isA.boolean()
|
||||||
}
|
}
|
||||||
|
@ -281,7 +282,7 @@ module.exports = (log, db, mailer, config, customs, push) => {
|
||||||
handler: async function (request) {
|
handler: async function (request) {
|
||||||
log.begin('Account.RecoveryEmailVerify', request);
|
log.begin('Account.RecoveryEmailVerify', request);
|
||||||
|
|
||||||
const { code, marketingOptIn, service, type, uid } = request.payload;
|
const { code, marketingOptIn, reminder, service, type, uid } = request.payload;
|
||||||
|
|
||||||
// verify_code because we don't know what type this is yet, but
|
// verify_code because we don't know what type this is yet, but
|
||||||
// we want to record right away before anything could fail, so
|
// we want to record right away before anything could fail, so
|
||||||
|
@ -429,7 +430,9 @@ module.exports = (log, db, mailer, config, customs, push) => {
|
||||||
// Force it so that we emit the appropriate newsletter state.
|
// Force it so that we emit the appropriate newsletter state.
|
||||||
marketingOptIn: marketingOptIn || false,
|
marketingOptIn: marketingOptIn || false,
|
||||||
uid
|
uid
|
||||||
})
|
}),
|
||||||
|
reminder ? request.emitMetricsEvent(`account.reminder.${reminder}`, { uid }) : null,
|
||||||
|
verificationReminders.delete(uid),
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
@ -23,6 +23,7 @@ module.exports = function (
|
||||||
const pushbox = require('../pushbox')(log, config);
|
const pushbox = require('../pushbox')(log, config);
|
||||||
const devicesImpl = require('../devices')(log, db, push);
|
const devicesImpl = require('../devices')(log, db, push);
|
||||||
const signinUtils = require('./utils/signin')(log, config, customs, db, mailer);
|
const signinUtils = require('./utils/signin')(log, config, customs, db, mailer);
|
||||||
|
const verificationReminders = require('../verification-reminders')(log, config);
|
||||||
// The routing modules themselves.
|
// The routing modules themselves.
|
||||||
const defaults = require('./defaults')(log, db);
|
const defaults = require('./defaults')(log, db);
|
||||||
const idp = require('./idp')(log, serverPublicKeys);
|
const idp = require('./idp')(log, serverPublicKeys);
|
||||||
|
@ -34,11 +35,12 @@ module.exports = function (
|
||||||
config,
|
config,
|
||||||
customs,
|
customs,
|
||||||
signinUtils,
|
signinUtils,
|
||||||
push
|
push,
|
||||||
|
verificationReminders,
|
||||||
);
|
);
|
||||||
const oauth = require('./oauth')(log, config, oauthdb);
|
const oauth = require('./oauth')(log, config, oauthdb);
|
||||||
const devicesSessions = require('./devices-and-sessions')(log, db, config, customs, push, pushbox, devicesImpl, oauthdb);
|
const devicesSessions = require('./devices-and-sessions')(log, db, config, customs, push, pushbox, devicesImpl, oauthdb);
|
||||||
const emails = require('./emails')(log, db, mailer, config, customs, push);
|
const emails = require('./emails')(log, db, mailer, config, customs, push, verificationReminders);
|
||||||
const password = require('./password')(
|
const password = require('./password')(
|
||||||
log,
|
log,
|
||||||
db,
|
db,
|
||||||
|
|
|
@ -38,6 +38,7 @@ module.exports = function (log, config, oauthdb) {
|
||||||
// Fallback to a stub implementation if redis is disabled
|
// Fallback to a stub implementation if redis is disabled
|
||||||
get: () => P.resolve()
|
get: () => P.resolve()
|
||||||
};
|
};
|
||||||
|
const verificationReminders = require('../verification-reminders')(log, config);
|
||||||
|
|
||||||
// Email template to UTM campaign map, each of these should be unique and
|
// Email template to UTM campaign map, each of these should be unique and
|
||||||
// map to exactly one email template.
|
// map to exactly one email template.
|
||||||
|
@ -589,6 +590,44 @@ module.exports = function (log, config, oauthdb) {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
verificationReminders.keys.forEach(key => {
|
||||||
|
// Template names are generated in the form `verificationReminderFirstEmail`,
|
||||||
|
// where `First` is the key derived from config, with an initial capital letter.
|
||||||
|
const template = `verificationReminder${key[0].toUpperCase()}${key.substr(1)}Email`;
|
||||||
|
const subject = key === 'first' ? gettext('Hello again') : gettext('Still there?');
|
||||||
|
|
||||||
|
templateNameToCampaignMap[template] = `${key}-verification-reminder`;
|
||||||
|
templateNameToContentMap[template] = 'activate';
|
||||||
|
|
||||||
|
Mailer.prototype[template] = async function (message) {
|
||||||
|
const { code, email, uid } = message;
|
||||||
|
|
||||||
|
log.trace(`mailer.${template}`, { code, email, uid });
|
||||||
|
|
||||||
|
const query = { code, reminder: key, uid };
|
||||||
|
const links = this._generateLinks(this.verificationUrl, email, query, template);
|
||||||
|
const headers = {
|
||||||
|
'X-Link': links.link,
|
||||||
|
'X-Verify-Code': message.code
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.send(Object.assign({}, message, {
|
||||||
|
headers,
|
||||||
|
subject,
|
||||||
|
template,
|
||||||
|
templateValues: {
|
||||||
|
email,
|
||||||
|
link: links.link,
|
||||||
|
oneClickLink: links.oneClickLink,
|
||||||
|
privacyUrl: links.privacyUrl,
|
||||||
|
supportUrl: links.supportUrl,
|
||||||
|
supportLinkAttributes: links.supportLinkAttributes,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
Mailer.prototype.unblockCodeEmail = function (message) {
|
Mailer.prototype.unblockCodeEmail = function (message) {
|
||||||
log.trace('mailer.unblockCodeEmail', { email: message.email, uid: message.uid });
|
log.trace('mailer.unblockCodeEmail', { email: message.email, uid: message.uid });
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
"recoveryEmail": 1,
|
"recoveryEmail": 1,
|
||||||
"sms.installFirefox": 1,
|
"sms.installFirefox": 1,
|
||||||
"unblockCodeEmail": 1,
|
"unblockCodeEmail": 1,
|
||||||
|
"verificationReminderFirstEmail": 2,
|
||||||
|
"verificationReminderSecondEmail": 2,
|
||||||
"verifyEmail": 2,
|
"verifyEmail": 2,
|
||||||
"verifyPrimaryEmail": 3,
|
"verifyPrimaryEmail": 3,
|
||||||
"verifyLoginEmail": 1,
|
"verifyLoginEmail": 1,
|
||||||
|
|
|
@ -79,6 +79,8 @@ module.exports = {
|
||||||
'recovery',
|
'recovery',
|
||||||
'sms.installFirefox',
|
'sms.installFirefox',
|
||||||
'unblock_code',
|
'unblock_code',
|
||||||
|
'verification_reminder_first',
|
||||||
|
'verification_reminder_second',
|
||||||
'verify',
|
'verify',
|
||||||
'verify_login',
|
'verify_login',
|
||||||
'verify_login_code',
|
'verify_login_code',
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>{{t "Firefox Accounts"}}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="-ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; margin: 0; padding: 0;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" width="310" style="-webkit-text-size-adjust: 100%; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 310px; margin: 0 auto;">
|
||||||
|
|
||||||
|
<!--Logo-->
|
||||||
|
<tr style="page-break-before: always">
|
||||||
|
<td align="center" id="firefox-logo" style="padding: 20px 0;">
|
||||||
|
{{^if sync}}
|
||||||
|
<img src="https://image.e.mozilla.org/lib/fe9915707361037e75/m/3/firefox57-logo.png" height="88" width="85" alt="" style="-ms-interpolation-mode: bicubic;" />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if sync}}
|
||||||
|
<img src="https://image.e.mozilla.org/lib/fe9915707361037e75/m/3/fxa_july2017_v2.png" height="137" width="270" alt="" style="-ms-interpolation-mode: bicubic;" />
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<!--Header Area-->
|
||||||
|
<tr style="page-break-before: always">
|
||||||
|
<td valign="top">
|
||||||
|
<h1 style="font-family: sans-serif; font-size: 21px; line-height: 29px; font-weight: normal; margin: 0 0 11px 0; text-align: center;">{{t "Hello again."}}</h1>
|
||||||
|
<p class="primary" style="font-family: sans-serif; font-size: 14px; line-height: 21px; font-weight: normal; margin: 0 0 21px 0; text-align: center;">{{{t "A few days ago you created a Firefox Account, but never verified it. A verified account lets you access your tabs, bookmarks, passwords and history on any device connected to it. Simply confirm this email address to activate your account."}}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!--Button Area-->
|
||||||
|
<tr height="50">
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="email-button" style="-webkit-text-size-adjust: 100%; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #0a84ff; border-radius: 4px; height: 50px; width: 310px !important;">
|
||||||
|
<tr style="page-break-before: always">
|
||||||
|
<td align="center" valign="middle" id="button-content" style="font-family: sans-serif; font-weight: normal; text-align: center; margin: 0; color: #ffffff; font-size: 20px; line-height: 100%;">
|
||||||
|
<!--[if mso]>
|
||||||
|
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{{link}}}" style="width:280px;height:40px;v-text-anchor:middle;" arcsize="10%" stroke="f" fillcolor="#0a84ff">
|
||||||
|
<w:anchorlock/>
|
||||||
|
<center>
|
||||||
|
<![endif]-->
|
||||||
|
<a href="{{{link}}}" id="button-link" style="font-family:sans-serif; color: #fff; display: block; padding: 15px; text-decoration: none; width: 280px; font-size: 18px; line-height: 26px;">{{t "Activate now"}}</a>
|
||||||
|
<!--[if mso]>
|
||||||
|
</center>
|
||||||
|
</v:roundrect>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!--Button Area-->
|
||||||
|
<tr style="page-break-before: always">
|
||||||
|
<td border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||||
|
<br/>
|
||||||
|
<p class="secondary" style="font-family: sans-serif; font-weight: normal; margin: 0 0 12px 0; text-align: center; color: #737373; font-size: 11px; line-height: 18px; width: 310px !important; word-wrap:break-word">{{t "This is an automated email; if you received it in error, no action is required."}} {{{t "For more information, please visit <a %(supportLinkAttributes)s>Mozilla Support</a>."}}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<tr style="page-break-before: always">
|
||||||
|
<td valign="top">
|
||||||
|
<p style="font-family: sans-serif; font-weight: normal; margin: 0; text-align: center; color: #737373; font-size: 11px; line-height: 18px; width: 310px !important; word-wrap:break-word">Mozilla. 331 E Evelyn Ave, Mountain View, CA 94041
|
||||||
|
<br />
|
||||||
|
<a href="{{{privacyUrl}}}" style="color: #0a84ff; text-decoration: none; font-family: sans-serif; font-size: 11px; line-height: 18px;">{{t "Mozilla Privacy Policy" }}</a></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div itemscope itemtype="https://schema.org/EmailMessage">
|
||||||
|
<div itemprop="potentialAction" itemscope itemtype="https://schema.org/ViewAction">
|
||||||
|
<link itemprop="target" href="{{{oneClickLink}}}"/>
|
||||||
|
<meta itemprop="name" content="{{t 'Verify Email'}}"/>
|
||||||
|
<meta itemprop="url" content="{{{oneClickLink}}}"/>
|
||||||
|
</div>
|
||||||
|
<meta itemprop="description" content="{{t 'Verify your email to finish your Firefox Account registration'}}"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,9 @@
|
||||||
|
{{t "Hello again."}}
|
||||||
|
|
||||||
|
{{t "A few days ago you created a Firefox Account, but never verified it. A verified account lets you access your tabs, bookmarks, passwords and history on any device connected to it. Simply confirm this email address to activate your account."}}
|
||||||
|
{{t "Activate now:"}} {{{link}}}
|
||||||
|
|
||||||
|
{{t "This is an automated email; if you received it in error, no action is required."}} {{{t "For more information, please visit %(supportUrl)s"}}}
|
||||||
|
|
||||||
|
Mozilla. 331 E Evelyn Ave, Mountain View, CA 94041
|
||||||
|
{{t "Mozilla Privacy Policy" }} {{{privacyUrl}}}
|
|
@ -0,0 +1,83 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>{{t "Firefox Accounts"}}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="-ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; margin: 0; padding: 0;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" width="310" style="-webkit-text-size-adjust: 100%; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 310px; margin: 0 auto;">
|
||||||
|
|
||||||
|
<!--Logo-->
|
||||||
|
<tr style="page-break-before: always">
|
||||||
|
<td align="center" id="firefox-logo" style="padding: 20px 0;">
|
||||||
|
{{^if sync}}
|
||||||
|
<img src="https://image.e.mozilla.org/lib/fe9915707361037e75/m/3/firefox57-logo.png" height="88" width="85" alt="" style="-ms-interpolation-mode: bicubic;" />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if sync}}
|
||||||
|
<img src="https://image.e.mozilla.org/lib/fe9915707361037e75/m/3/fxa_july2017_v2.png" height="137" width="270" alt="" style="-ms-interpolation-mode: bicubic;" />
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<!--Header Area-->
|
||||||
|
<tr style="page-break-before: always">
|
||||||
|
<td valign="top">
|
||||||
|
<h1 style="font-family: sans-serif; font-size: 21px; line-height: 29px; font-weight: normal; margin: 0 0 11px 0; text-align: center;">{{t "Still there?"}}</h1>
|
||||||
|
<p class="primary" style="font-family: sans-serif; font-size: 14px; line-height: 21px; font-weight: normal; margin: 0 0 21px 0; text-align: center;">{{{t "A week ago you created a Firefox Account, but never verified it. We’re worried about you. Confirm this email address to activate your account and let us know you're okay."}}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!--Button Area-->
|
||||||
|
<tr height="50">
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="email-button" style="-webkit-text-size-adjust: 100%; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #0a84ff; border-radius: 4px; height: 50px; width: 310px !important;">
|
||||||
|
<tr style="page-break-before: always">
|
||||||
|
<td align="center" valign="middle" id="button-content" style="font-family: sans-serif; font-weight: normal; text-align: center; margin: 0; color: #ffffff; font-size: 20px; line-height: 100%;">
|
||||||
|
<!--[if mso]>
|
||||||
|
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{{link}}}" style="width:280px;height:40px;v-text-anchor:middle;" arcsize="10%" stroke="f" fillcolor="#0a84ff">
|
||||||
|
<w:anchorlock/>
|
||||||
|
<center>
|
||||||
|
<![endif]-->
|
||||||
|
<a href="{{{link}}}" id="button-link" style="font-family:sans-serif; color: #fff; display: block; padding: 15px; text-decoration: none; width: 280px; font-size: 18px; line-height: 26px;">{{t "Activate now"}}</a>
|
||||||
|
<!--[if mso]>
|
||||||
|
</center>
|
||||||
|
</v:roundrect>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!--Button Area-->
|
||||||
|
<tr style="page-break-before: always">
|
||||||
|
<td border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||||
|
<br/>
|
||||||
|
<p class="secondary" style="font-family: sans-serif; font-weight: normal; margin: 0 0 12px 0; text-align: center; color: #737373; font-size: 11px; line-height: 18px; width: 310px !important; word-wrap:break-word">{{t "This is an automated email; if you received it in error, no action is required."}} {{{t "For more information, please visit <a %(supportLinkAttributes)s>Mozilla Support</a>."}}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<tr style="page-break-before: always">
|
||||||
|
<td valign="top">
|
||||||
|
<p style="font-family: sans-serif; font-weight: normal; margin: 0; text-align: center; color: #737373; font-size: 11px; line-height: 18px; width: 310px !important; word-wrap:break-word">Mozilla. 331 E Evelyn Ave, Mountain View, CA 94041
|
||||||
|
<br />
|
||||||
|
<a href="{{{privacyUrl}}}" style="color: #0a84ff; text-decoration: none; font-family: sans-serif; font-size: 11px; line-height: 18px;">{{t "Mozilla Privacy Policy" }}</a></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div itemscope itemtype="https://schema.org/EmailMessage">
|
||||||
|
<div itemprop="potentialAction" itemscope itemtype="https://schema.org/ViewAction">
|
||||||
|
<link itemprop="target" href="{{{oneClickLink}}}"/>
|
||||||
|
<meta itemprop="name" content="{{t 'Verify Email'}}"/>
|
||||||
|
<meta itemprop="url" content="{{{oneClickLink}}}"/>
|
||||||
|
</div>
|
||||||
|
<meta itemprop="description" content="{{t 'Verify your email to finish your Firefox Account registration'}}"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,9 @@
|
||||||
|
{{t "Still there?"}}
|
||||||
|
|
||||||
|
{{t "A week ago you created a Firefox Account, but never verified it. We’re worried about you. Confirm this email address to activate your account and let us know you're okay."}}
|
||||||
|
{{t "Activate now:"}} {{{link}}}
|
||||||
|
|
||||||
|
{{t "This is an automated email; if you received it in error, no action is required."}} {{{t "For more information, please visit %(supportUrl)s"}}}
|
||||||
|
|
||||||
|
Mozilla. 331 E Evelyn Ave, Mountain View, CA 94041
|
||||||
|
{{t "Mozilla Privacy Policy" }} {{{privacyUrl}}}
|
|
@ -0,0 +1,143 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
// This module implements logic for managing account verification reminders.
|
||||||
|
//
|
||||||
|
// Reminder records are stored in Redis sorted sets on account creation and
|
||||||
|
// removed when an acount is verified. A separate script, running on the
|
||||||
|
// fxa-admin box, processes reminder records in a cron job by pulling the
|
||||||
|
// records that have ticked passed an expiry limit set in config and sending
|
||||||
|
// the appropriate reminder email to the address associated with each account.
|
||||||
|
//
|
||||||
|
// Right now, config determines how many reminder emails are sent and what
|
||||||
|
// the expiry intervals for them are. Ultimately though, that might be a good
|
||||||
|
// candidate to control with feature flags.
|
||||||
|
//
|
||||||
|
// More detail on sorted sets:
|
||||||
|
//
|
||||||
|
// * https://redis.io/topics/data-types#sorted-sets
|
||||||
|
// * https://redis.io/topics/data-types-intro#redis-sorted-sets
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const P = require('./promise');
|
||||||
|
|
||||||
|
const INTERVAL_PATTERN = /^([a-z]+)Interval$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the verification reminders module.
|
||||||
|
*
|
||||||
|
* @param {Object} log
|
||||||
|
* @param {Object} config
|
||||||
|
* @returns {VerificationReminders}
|
||||||
|
*/
|
||||||
|
module.exports = (log, config) => {
|
||||||
|
const redis = require('fxa-shared/redis')({
|
||||||
|
...config.redis,
|
||||||
|
...config.verificationReminders.redis,
|
||||||
|
enabled: true,
|
||||||
|
}, log);
|
||||||
|
|
||||||
|
const { rolloutRate } = config.verificationReminders;
|
||||||
|
|
||||||
|
const { keys, intervals } = Object.entries(config.verificationReminders).reduce(({ keys, intervals }, [ key, value ]) => {
|
||||||
|
const matches = INTERVAL_PATTERN.exec(key);
|
||||||
|
if (matches && matches.length === 2) {
|
||||||
|
const key = matches[1];
|
||||||
|
keys.push(key);
|
||||||
|
intervals[key] = value;
|
||||||
|
}
|
||||||
|
return { keys, intervals };
|
||||||
|
}, { keys: [], intervals: {} });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} VerificationReminders
|
||||||
|
* @property {Array} keys
|
||||||
|
* @property {Function} create
|
||||||
|
* @property {Function} delete
|
||||||
|
* @property {Function} process
|
||||||
|
*
|
||||||
|
* Each method below returns a promise that resolves to an object,
|
||||||
|
* the shape of which is determined by config. If config has settings
|
||||||
|
* for `firstInterval` and `secondInterval` (as at time of writing),
|
||||||
|
* the shape of those objects would be `{ first, second }`.
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
keys: keys.slice(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create verification reminder records for an account.
|
||||||
|
*
|
||||||
|
* @param {String} uid
|
||||||
|
* @returns {Promise} - Each property on the resolved object will be the number
|
||||||
|
* of elements added to that sorted set, i.e. the result of
|
||||||
|
* [`redis.zadd`](https://redis.io/commands/zadd).
|
||||||
|
*/
|
||||||
|
async create (uid) {
|
||||||
|
try {
|
||||||
|
if (rolloutRate <= 1 && Math.random() < rolloutRate) {
|
||||||
|
const now = Date.now();
|
||||||
|
const result = await P.props(keys.reduce((result, key) => {
|
||||||
|
result[key] = redis.zadd(key, now, uid);
|
||||||
|
return result;
|
||||||
|
}, {}));
|
||||||
|
log.info('verificationReminders.create', { uid });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.error('verificationReminders.create.error', { err, uid });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete verification reminder records for an account.
|
||||||
|
*
|
||||||
|
* @param {String} uid
|
||||||
|
* @returns {Promise} - Each property on the resolved object will be the number of
|
||||||
|
* elements removed from that sorted set, i.e. the result of
|
||||||
|
* [`redis.zrem`](https://redis.io/commands/zrem).
|
||||||
|
*/
|
||||||
|
async delete (uid) {
|
||||||
|
try {
|
||||||
|
const result = await P.props(keys.reduce((result, key) => {
|
||||||
|
result[key] = redis.zrem(key, uid);
|
||||||
|
return result;
|
||||||
|
}, {}));
|
||||||
|
log.info('verificationReminders.delete', { uid });
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
log.error('verificationReminders.delete.error', { err, uid });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read and remove all verification reminders that have
|
||||||
|
* ticked past the expiry intervals set in config.
|
||||||
|
*
|
||||||
|
* @returns {Promise} - Each property on the resolved object will be an array of uids that
|
||||||
|
* were found to have ticked past the relevant expiry interval, i.e.
|
||||||
|
* the result of [`redis.zrangebyscore`](https://redis.io/commands/zrangebyscore).
|
||||||
|
*/
|
||||||
|
async process () {
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
return await P.props(keys.reduce((result, key) => {
|
||||||
|
const cutoff = now - intervals[key];
|
||||||
|
result[key] = redis.zrangebyscore(key, 0, cutoff);
|
||||||
|
setImmediate(async () => {
|
||||||
|
await result[key];
|
||||||
|
redis.zremrangebyscore(key, 0, cutoff);
|
||||||
|
});
|
||||||
|
log.info('verificationReminders.process', { key, now, cutoff });
|
||||||
|
return result;
|
||||||
|
}, {}));
|
||||||
|
} catch (err) {
|
||||||
|
log.error('verificationReminders.process.error', { err });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -8447,17 +8447,29 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fxa-shared": {
|
"fxa-shared": {
|
||||||
"version": "1.0.19",
|
"version": "1.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/fxa-shared/-/fxa-shared-1.0.19.tgz",
|
"resolved": "https://registry.npmjs.org/fxa-shared/-/fxa-shared-1.0.21.tgz",
|
||||||
"integrity": "sha512-WuKS50Z/Il+bjnlp6qHqc3qr6mS7q1sYQ8rRcFx+4R39m2FjbxwSG8f1BnF74q3rdb8nCH0BsmSNFsJ1QNaN8w==",
|
"integrity": "sha512-jIvoGmulWWDa6i5MjfxBjIZrk3WiBU/tZziKN8VQtptnfKGEGOFRPtbnpWX3lElLmf/46+yOXcSyKkIoA2WzTg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"accept-language": "2.0.17",
|
"accept-language": "2.0.17",
|
||||||
|
"ajv": "6.10.0",
|
||||||
"bluebird": "3.5.3",
|
"bluebird": "3.5.3",
|
||||||
"generic-pool": "3.6.1",
|
"generic-pool": "3.6.1",
|
||||||
"moment": "2.20.1",
|
"moment": "2.20.1",
|
||||||
"redis": "2.8.0"
|
"redis": "2.8.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": {
|
||||||
|
"version": "6.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
|
||||||
|
"integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^2.0.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"bluebird": {
|
"bluebird": {
|
||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
|
||||||
|
@ -9749,9 +9761,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ieee754": {
|
"ieee754": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
|
||||||
"integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA=="
|
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
|
||||||
},
|
},
|
||||||
"ignore": {
|
"ignore": {
|
||||||
"version": "3.3.10",
|
"version": "3.3.10",
|
||||||
|
@ -11213,9 +11225,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nan": {
|
"nan": {
|
||||||
"version": "2.13.1",
|
"version": "2.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
|
||||||
"integrity": "sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA==",
|
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"nanomatch": {
|
"nanomatch": {
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
"fxa-geodb": "1.0.4",
|
"fxa-geodb": "1.0.4",
|
||||||
"fxa-jwtool": "0.7.2",
|
"fxa-jwtool": "0.7.2",
|
||||||
"fxa-notifier-aws": "1.0.0",
|
"fxa-notifier-aws": "1.0.0",
|
||||||
"fxa-shared": "1.0.19",
|
"fxa-shared": "1.0.21",
|
||||||
"generic-pool": "3.2.0",
|
"generic-pool": "3.2.0",
|
||||||
"google-libphonenumber": "2.0.10",
|
"google-libphonenumber": "2.0.10",
|
||||||
"grunt-nunjucks-2-html": "3.1.0",
|
"grunt-nunjucks-2-html": "3.1.0",
|
||||||
|
|
|
@ -270,8 +270,6 @@ describe('lib/devices:', () => {
|
||||||
is_placeholder: false
|
is_placeholder: false
|
||||||
}, 'event data was correct');
|
}, 'event data was correct');
|
||||||
|
|
||||||
assert.equal(log.info.callCount, 0, 'log.info was not called');
|
|
||||||
|
|
||||||
assert.equal(log.notifyAttachedServices.callCount, 1, 'log.notifyAttachedServices was called once');
|
assert.equal(log.notifyAttachedServices.callCount, 1, 'log.notifyAttachedServices was called once');
|
||||||
args = log.notifyAttachedServices.args[0];
|
args = log.notifyAttachedServices.args[0];
|
||||||
assert.equal(args.length, 3, 'log.notifyAttachedServices was passed three arguments');
|
assert.equal(args.length, 3, 'log.notifyAttachedServices was passed three arguments');
|
||||||
|
@ -314,10 +312,10 @@ describe('lib/devices:', () => {
|
||||||
assert.equal(log.activityEvent.callCount, 1, 'log.activityEvent was called once');
|
assert.equal(log.activityEvent.callCount, 1, 'log.activityEvent was called once');
|
||||||
assert.equal(log.activityEvent.args[0][0].is_placeholder, true, 'is_placeholder was correct');
|
assert.equal(log.activityEvent.args[0][0].is_placeholder, true, 'is_placeholder was correct');
|
||||||
|
|
||||||
assert.equal(log.info.callCount, 1, 'log.info was called once');
|
assert.equal(log.info.callCount, 2);
|
||||||
assert.equal(log.info.args[0].length, 2);
|
assert.equal(log.info.args[1].length, 2);
|
||||||
assert.equal(log.info.args[0][0], 'device:createPlaceholder');
|
assert.equal(log.info.args[1][0], 'device:createPlaceholder');
|
||||||
assert.deepEqual(log.info.args[0][1], {
|
assert.deepEqual(log.info.args[1][1], {
|
||||||
uid: credentials.uid,
|
uid: credentials.uid,
|
||||||
id: result.id
|
id: result.id
|
||||||
}, 'argument was event data');
|
}, 'argument was event data');
|
||||||
|
@ -368,8 +366,6 @@ describe('lib/devices:', () => {
|
||||||
is_placeholder: false
|
is_placeholder: false
|
||||||
}, 'event data was correct');
|
}, 'event data was correct');
|
||||||
|
|
||||||
assert.equal(log.info.callCount, 0, 'log.info was not called');
|
|
||||||
|
|
||||||
assert.equal(log.notifyAttachedServices.callCount, 0, 'log.notifyAttachedServices was not called');
|
assert.equal(log.notifyAttachedServices.callCount, 0, 'log.notifyAttachedServices was not called');
|
||||||
|
|
||||||
assert.equal(push.notifyDeviceConnected.callCount, 0, 'push.notifyDeviceConnected was not called');
|
assert.equal(push.notifyDeviceConnected.callCount, 0, 'push.notifyDeviceConnected was not called');
|
||||||
|
@ -424,8 +420,6 @@ describe('lib/devices:', () => {
|
||||||
is_placeholder: false
|
is_placeholder: false
|
||||||
}, 'event data was correct');
|
}, 'event data was correct');
|
||||||
|
|
||||||
assert.equal(log.info.callCount, 0, 'log.info was not called');
|
|
||||||
|
|
||||||
assert.equal(log.notifyAttachedServices.callCount, 1, 'log.notifyAttachedServices was called once');
|
assert.equal(log.notifyAttachedServices.callCount, 1, 'log.notifyAttachedServices was called once');
|
||||||
args = log.notifyAttachedServices.args[0];
|
args = log.notifyAttachedServices.args[0];
|
||||||
assert.equal(args.length, 3, 'log.notifyAttachedServices was passed three arguments');
|
assert.equal(args.length, 3, 'log.notifyAttachedServices was passed three arguments');
|
||||||
|
@ -458,10 +452,10 @@ describe('lib/devices:', () => {
|
||||||
assert.equal(log.activityEvent.callCount, 1, 'log.activityEvent was called once');
|
assert.equal(log.activityEvent.callCount, 1, 'log.activityEvent was called once');
|
||||||
assert.equal(log.activityEvent.args[0][0].is_placeholder, true, 'is_placeholder was correct');
|
assert.equal(log.activityEvent.args[0][0].is_placeholder, true, 'is_placeholder was correct');
|
||||||
|
|
||||||
assert.equal(log.info.callCount, 1, 'log.info was called once');
|
assert.equal(log.info.callCount, 2);
|
||||||
assert.equal(log.info.args[0].length, 2);
|
assert.equal(log.info.args[1].length, 2);
|
||||||
assert.equal(log.info.args[0][0], 'device:createPlaceholder');
|
assert.equal(log.info.args[1][0], 'device:createPlaceholder');
|
||||||
assert.deepEqual(log.info.args[0][1], {
|
assert.deepEqual(log.info.args[1][1], {
|
||||||
uid: credentials.uid,
|
uid: credentials.uid,
|
||||||
id: result.id
|
id: result.id
|
||||||
}, 'argument was event data');
|
}, 'argument was event data');
|
||||||
|
@ -512,8 +506,6 @@ describe('lib/devices:', () => {
|
||||||
is_placeholder: false
|
is_placeholder: false
|
||||||
}, 'event data was correct');
|
}, 'event data was correct');
|
||||||
|
|
||||||
assert.equal(log.info.callCount, 0, 'log.info was not called');
|
|
||||||
|
|
||||||
assert.equal(log.notifyAttachedServices.callCount, 0, 'log.notifyAttachedServices was not called');
|
assert.equal(log.notifyAttachedServices.callCount, 0, 'log.notifyAttachedServices was not called');
|
||||||
|
|
||||||
assert.equal(push.notifyDeviceConnected.callCount, 0, 'push.notifyDeviceConnected was not called');
|
assert.equal(push.notifyDeviceConnected.callCount, 0, 'push.notifyDeviceConnected was not called');
|
||||||
|
|
|
@ -93,9 +93,9 @@ describe('bounce messages', () => {
|
||||||
assert.equal(mockDB.deleteAccount.callCount, 2);
|
assert.equal(mockDB.deleteAccount.callCount, 2);
|
||||||
assert.equal(mockDB.accountRecord.args[0][0], 'test@example.com');
|
assert.equal(mockDB.accountRecord.args[0][0], 'test@example.com');
|
||||||
assert.equal(mockDB.accountRecord.args[1][0], 'foobar@example.com');
|
assert.equal(mockDB.accountRecord.args[1][0], 'foobar@example.com');
|
||||||
assert.equal(log.info.callCount, 6);
|
assert.equal(log.info.callCount, 7);
|
||||||
assert.equal(log.info.args[5][0], 'accountDeleted');
|
assert.equal(log.info.args[6][0], 'accountDeleted');
|
||||||
assert.equal(log.info.args[5][1].email, 'foobar@example.com');
|
assert.equal(log.info.args[6][1].email, 'foobar@example.com');
|
||||||
assert.equal(mockMsg.del.callCount, 1);
|
assert.equal(mockMsg.del.callCount, 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,7 +43,7 @@ function makeRoutes (options = {}, requireMocks) {
|
||||||
customs,
|
customs,
|
||||||
signinUtils,
|
signinUtils,
|
||||||
mocks.mockPush(),
|
mocks.mockPush(),
|
||||||
mocks.mockDevices()
|
mocks.mockVerificationReminders(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,11 @@ describe('metrics/amplitude', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if log argument is missing', () => {
|
it('throws if log argument is missing', () => {
|
||||||
assert.throws(() => amplitudeModule(null, { oauth: { clientIds: {} } }));
|
assert.throws(() => amplitudeModule(null, { oauth: { clientIds: {} }, verificationReminders: {} }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if config argument is missing', () => {
|
it('throws if config argument is missing', () => {
|
||||||
assert.throws(() => amplitudeModule({}, { oauth: { clientIds: null } }));
|
assert.throws(() => amplitudeModule({}, { oauth: { clientIds: null }, verificationReminders: {} }));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('instantiate', () => {
|
describe('instantiate', () => {
|
||||||
|
@ -37,7 +37,12 @@ describe('metrics/amplitude', () => {
|
||||||
0: 'amo',
|
0: 'amo',
|
||||||
1: 'pocket'
|
1: 'pocket'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
verificationReminders: {
|
||||||
|
firstInterval: 1000,
|
||||||
|
secondInterval: 2000,
|
||||||
|
thirdInterval: 3000,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -826,6 +831,131 @@ describe('metrics/amplitude', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('email.verificationReminderFirstEmail.bounced', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return amplitude('email.verificationReminderFirstEmail.bounced', mocks.mockRequest({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('did not call log.error', () => {
|
||||||
|
assert.equal(log.error.callCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('called log.amplitudeEvent correctly', () => {
|
||||||
|
assert.equal(log.amplitudeEvent.callCount, 1);
|
||||||
|
const args = log.amplitudeEvent.args[0];
|
||||||
|
assert.equal(args[0].event_type, 'fxa_email - bounced');
|
||||||
|
assert.equal(args[0].event_properties.email_type, 'registration');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('email.verificationReminderFirstEmail.sent', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return amplitude('email.verificationReminderFirstEmail.sent', mocks.mockRequest({}), {
|
||||||
|
templateVersion: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('did not call log.error', () => {
|
||||||
|
assert.equal(log.error.callCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('called log.amplitudeEvent correctly', () => {
|
||||||
|
assert.equal(log.amplitudeEvent.callCount, 1);
|
||||||
|
const args = log.amplitudeEvent.args[0];
|
||||||
|
assert.equal(args[0].event_type, 'fxa_email - sent');
|
||||||
|
assert.equal(args[0].event_properties.email_type, 'registration');
|
||||||
|
assert.equal(args[0].event_properties.email_template, 'verificationReminderFirstEmail');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('email.verificationReminderSecondEmail.bounced', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return amplitude('email.verificationReminderSecondEmail.bounced', mocks.mockRequest({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('did not call log.error', () => {
|
||||||
|
assert.equal(log.error.callCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('called log.amplitudeEvent correctly', () => {
|
||||||
|
assert.equal(log.amplitudeEvent.callCount, 1);
|
||||||
|
const args = log.amplitudeEvent.args[0];
|
||||||
|
assert.equal(args[0].event_type, 'fxa_email - bounced');
|
||||||
|
assert.equal(args[0].event_properties.email_type, 'registration');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('email.verificationReminderSecondEmail.sent', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return amplitude('email.verificationReminderSecondEmail.sent', mocks.mockRequest({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('did not call log.error', () => {
|
||||||
|
assert.equal(log.error.callCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('called log.amplitudeEvent correctly', () => {
|
||||||
|
assert.equal(log.amplitudeEvent.callCount, 1);
|
||||||
|
const args = log.amplitudeEvent.args[0];
|
||||||
|
assert.equal(args[0].event_type, 'fxa_email - sent');
|
||||||
|
assert.equal(args[0].event_properties.email_type, 'registration');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('email.verificationReminderThirdEmail.bounced', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return amplitude('email.verificationReminderThirdEmail.bounced', mocks.mockRequest({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('did not call log.error', () => {
|
||||||
|
assert.equal(log.error.callCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('called log.amplitudeEvent correctly', () => {
|
||||||
|
assert.equal(log.amplitudeEvent.callCount, 1);
|
||||||
|
const args = log.amplitudeEvent.args[0];
|
||||||
|
assert.equal(args[0].event_type, 'fxa_email - bounced');
|
||||||
|
assert.equal(args[0].event_properties.email_type, 'registration');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('email.verificationReminderThirdEmail.sent', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return amplitude('email.verificationReminderThirdEmail.sent', mocks.mockRequest({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('did not call log.error', () => {
|
||||||
|
assert.equal(log.error.callCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('called log.amplitudeEvent correctly', () => {
|
||||||
|
assert.equal(log.amplitudeEvent.callCount, 1);
|
||||||
|
const args = log.amplitudeEvent.args[0];
|
||||||
|
assert.equal(args[0].event_type, 'fxa_email - sent');
|
||||||
|
assert.equal(args[0].event_properties.email_type, 'registration');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('email.verificationReminderFourthEmail.bounced', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return amplitude('email.verificationReminderFourthEmail.bounced', mocks.mockRequest({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('did not call log.amplitudeEvent', () => {
|
||||||
|
assert.equal(log.amplitudeEvent.callCount, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('email.verificationReminderFourthEmail.sent', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return amplitude('email.verificationReminderFourthEmail.sent', mocks.mockRequest({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('did not call log.amplitudeEvent', () => {
|
||||||
|
assert.equal(log.amplitudeEvent.callCount, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('email.verifyEmail.bounced', () => {
|
describe('email.verifyEmail.bounced', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
return amplitude('email.verifyEmail.bounced', mocks.mockRequest({}));
|
return amplitude('email.verifyEmail.bounced', mocks.mockRequest({}));
|
||||||
|
|
|
@ -10,12 +10,14 @@ const log = {
|
||||||
activityEvent: sinon.spy(),
|
activityEvent: sinon.spy(),
|
||||||
amplitudeEvent: sinon.spy(),
|
amplitudeEvent: sinon.spy(),
|
||||||
error: sinon.spy(),
|
error: sinon.spy(),
|
||||||
flowEvent: sinon.spy()
|
flowEvent: sinon.spy(),
|
||||||
|
info: sinon.spy(),
|
||||||
};
|
};
|
||||||
const events = require('../../../lib/metrics/events')(log, {
|
const events = require('../../../lib/metrics/events')(log, {
|
||||||
oauth: {
|
oauth: {
|
||||||
clientIds: {}
|
clientIds: {}
|
||||||
}
|
},
|
||||||
|
verificationReminders: {},
|
||||||
});
|
});
|
||||||
const mocks = require('../../mocks');
|
const mocks = require('../../mocks');
|
||||||
const P = require('../../../lib/promise');
|
const P = require('../../../lib/promise');
|
||||||
|
|
|
@ -51,6 +51,7 @@ const makeRoutes = function (options = {}, requireMocks) {
|
||||||
signinUtils.checkPassword = options.checkPassword;
|
signinUtils.checkPassword = options.checkPassword;
|
||||||
}
|
}
|
||||||
const push = options.push || require('../../../lib/push')(log, db, {});
|
const push = options.push || require('../../../lib/push')(log, db, {});
|
||||||
|
const verificationReminders = options.verificationReminders || mocks.mockVerificationReminders();
|
||||||
return proxyquire('../../../lib/routes/account', requireMocks || {})(
|
return proxyquire('../../../lib/routes/account', requireMocks || {})(
|
||||||
log,
|
log,
|
||||||
db,
|
db,
|
||||||
|
@ -59,7 +60,8 @@ const makeRoutes = function (options = {}, requireMocks) {
|
||||||
config,
|
config,
|
||||||
customs,
|
customs,
|
||||||
signinUtils,
|
signinUtils,
|
||||||
push
|
push,
|
||||||
|
verificationReminders,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -384,6 +386,7 @@ describe('/account/create', () => {
|
||||||
});
|
});
|
||||||
const mockMailer = mocks.mockMailer();
|
const mockMailer = mocks.mockMailer();
|
||||||
const mockPush = mocks.mockPush();
|
const mockPush = mocks.mockPush();
|
||||||
|
const verificationReminders = mocks.mockVerificationReminders();
|
||||||
const accountRoutes = makeRoutes({
|
const accountRoutes = makeRoutes({
|
||||||
config: {
|
config: {
|
||||||
securityHistory: {
|
securityHistory: {
|
||||||
|
@ -403,7 +406,8 @@ describe('/account/create', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
push: mockPush
|
push: mockPush,
|
||||||
|
verificationReminders,
|
||||||
});
|
});
|
||||||
const route = getRoute(accountRoutes, '/account/create');
|
const route = getRoute(accountRoutes, '/account/create');
|
||||||
|
|
||||||
|
@ -418,23 +422,26 @@ describe('/account/create', () => {
|
||||||
mockRequest,
|
mockRequest,
|
||||||
route,
|
route,
|
||||||
sessionTokenId,
|
sessionTokenId,
|
||||||
uid
|
uid,
|
||||||
|
verificationReminders,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should create a sync account', () => {
|
it('should create a sync account', () => {
|
||||||
const mocked = setup();
|
const {
|
||||||
const clientAddress = mocked.clientAddress;
|
clientAddress,
|
||||||
const emailCode = mocked.emailCode;
|
emailCode,
|
||||||
const keyFetchTokenId = mocked.keyFetchTokenId;
|
keyFetchTokenId,
|
||||||
const mockDB = mocked.mockDB;
|
mockDB,
|
||||||
const mockLog = mocked.mockLog;
|
mockLog,
|
||||||
const mockMailer = mocked.mockMailer;
|
mockMailer,
|
||||||
const mockMetricsContext = mocked.mockMetricsContext;
|
mockMetricsContext,
|
||||||
const mockRequest = mocked.mockRequest;
|
mockRequest,
|
||||||
const route = mocked.route;
|
route,
|
||||||
const sessionTokenId = mocked.sessionTokenId;
|
sessionTokenId,
|
||||||
const uid = mocked.uid;
|
uid,
|
||||||
|
verificationReminders,
|
||||||
|
} = setup();
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
sinon.stub(Date, 'now').callsFake(() => now);
|
sinon.stub(Date, 'now').callsFake(() => now);
|
||||||
|
@ -560,17 +567,24 @@ describe('/account/create', () => {
|
||||||
assert.equal(args[2].service, 'sync');
|
assert.equal(args[2].service, 'sync');
|
||||||
assert.equal(args[2].uid, uid);
|
assert.equal(args[2].uid, uid);
|
||||||
|
|
||||||
|
assert.equal(verificationReminders.create.callCount, 1);
|
||||||
|
args = verificationReminders.create.args[0];
|
||||||
|
assert.lengthOf(args, 1);
|
||||||
|
assert.equal(args[0], uid);
|
||||||
|
|
||||||
assert.equal(mockLog.error.callCount, 0);
|
assert.equal(mockLog.error.callCount, 0);
|
||||||
}).finally(() => Date.now.restore());
|
}).finally(() => Date.now.restore());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a non-sync account', () => {
|
it('should create a non-sync account', () => {
|
||||||
const mocked = setup();
|
const {
|
||||||
const mockLog = mocked.mockLog;
|
mockLog,
|
||||||
const mockMailer = mocked.mockMailer;
|
mockMailer,
|
||||||
const mockRequest = mocked.mockRequest;
|
mockRequest,
|
||||||
const route = mocked.route;
|
route,
|
||||||
const uid = mocked.uid;
|
uid,
|
||||||
|
verificationReminders,
|
||||||
|
} = setup();
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
sinon.stub(Date, 'now').callsFake(() => now);
|
sinon.stub(Date, 'now').callsFake(() => now);
|
||||||
|
@ -598,15 +612,20 @@ describe('/account/create', () => {
|
||||||
assert.equal(mockMailer.sendVerifyCode.callCount, 1, 'mailer.sendVerifyCode was called');
|
assert.equal(mockMailer.sendVerifyCode.callCount, 1, 'mailer.sendVerifyCode was called');
|
||||||
args = mockMailer.sendVerifyCode.args[0];
|
args = mockMailer.sendVerifyCode.args[0];
|
||||||
assert.equal(args[2].service, 'foo');
|
assert.equal(args[2].service, 'foo');
|
||||||
|
|
||||||
|
assert.equal(verificationReminders.create.callCount, 1);
|
||||||
|
|
||||||
assert.equal(mockLog.error.callCount, 0);
|
assert.equal(mockLog.error.callCount, 0);
|
||||||
}).finally(() => Date.now.restore());
|
}).finally(() => Date.now.restore());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an error if email fails to send', () => {
|
it('should return an error if email fails to send', () => {
|
||||||
const mocked = setup();
|
const {
|
||||||
const mockMailer = mocked.mockMailer;
|
mockMailer,
|
||||||
const mockRequest = mocked.mockRequest;
|
mockRequest,
|
||||||
const route = mocked.route;
|
route,
|
||||||
|
verificationReminders,
|
||||||
|
} = setup();
|
||||||
|
|
||||||
mockMailer.sendVerifyCode = sinon.spy(() => P.reject());
|
mockMailer.sendVerifyCode = sinon.spy(() => P.reject());
|
||||||
|
|
||||||
|
@ -615,14 +634,18 @@ describe('/account/create', () => {
|
||||||
assert.equal(err.output.payload.code, 422);
|
assert.equal(err.output.payload.code, 422);
|
||||||
assert.equal(err.output.payload.errno, 151);
|
assert.equal(err.output.payload.errno, 151);
|
||||||
assert.equal(err.output.payload.error, 'Unprocessable Entity');
|
assert.equal(err.output.payload.error, 'Unprocessable Entity');
|
||||||
|
|
||||||
|
assert.equal(verificationReminders.create.callCount, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a bounce error if send fails with one', () => {
|
it('should return a bounce error if send fails with one', () => {
|
||||||
const mocked = setup();
|
const {
|
||||||
const mockMailer = mocked.mockMailer;
|
mockMailer,
|
||||||
const mockRequest = mocked.mockRequest;
|
mockRequest,
|
||||||
const route = mocked.route;
|
route,
|
||||||
|
verificationReminders,
|
||||||
|
} = setup();
|
||||||
|
|
||||||
mockMailer.sendVerifyCode = sinon.spy(() => P.reject(error.emailBouncedHard(42)));
|
mockMailer.sendVerifyCode = sinon.spy(() => P.reject(error.emailBouncedHard(42)));
|
||||||
|
|
||||||
|
@ -631,6 +654,8 @@ describe('/account/create', () => {
|
||||||
assert.equal(err.output.payload.code, 400);
|
assert.equal(err.output.payload.code, 400);
|
||||||
assert.equal(err.output.payload.errno, 134);
|
assert.equal(err.output.payload.errno, 134);
|
||||||
assert.equal(err.output.payload.error, 'Bad Request');
|
assert.equal(err.output.payload.error, 'Bad Request');
|
||||||
|
|
||||||
|
assert.equal(verificationReminders.create.callCount, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1612,8 +1637,8 @@ describe('/account/destroy', () => {
|
||||||
assert.equal(args[0].email, email, 'db.deleteAccount was passed email record');
|
assert.equal(args[0].email, email, 'db.deleteAccount was passed email record');
|
||||||
assert.deepEqual(args[0].uid, uid, 'email record had correct uid');
|
assert.deepEqual(args[0].uid, uid, 'email record had correct uid');
|
||||||
|
|
||||||
assert.equal(mockLog.info.callCount, 1);
|
assert.equal(mockLog.info.callCount, 2);
|
||||||
args = mockLog.info.args[0];
|
args = mockLog.info.args[1];
|
||||||
assert.lengthOf(args, 2);
|
assert.lengthOf(args, 2);
|
||||||
assert.equal(args[0], 'accountDeleted.byRequest');
|
assert.equal(args[0], 'accountDeleted.byRequest');
|
||||||
assert.equal(args[1].email, email);
|
assert.equal(args[1].email, email);
|
||||||
|
|
|
@ -52,13 +52,15 @@ const makeRoutes = function (options = {}, requireMocks) {
|
||||||
check: function () { return P.resolve(true); }
|
check: function () { return P.resolve(true); }
|
||||||
};
|
};
|
||||||
const push = options.push || require('../../../lib/push')(log, db, {});
|
const push = options.push || require('../../../lib/push')(log, db, {});
|
||||||
|
const verificationReminders = options.verificationReminders || mocks.mockVerificationReminders();
|
||||||
return proxyquire('../../../lib/routes/emails', requireMocks || {})(
|
return proxyquire('../../../lib/routes/emails', requireMocks || {})(
|
||||||
log,
|
log,
|
||||||
db,
|
db,
|
||||||
options.mailer || {},
|
options.mailer || {},
|
||||||
config,
|
config,
|
||||||
customs,
|
customs,
|
||||||
push
|
push,
|
||||||
|
verificationReminders,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -453,6 +455,7 @@ describe('/recovery_email/verify_code', () => {
|
||||||
const mockMailer = mocks.mockMailer();
|
const mockMailer = mocks.mockMailer();
|
||||||
const mockPush = mocks.mockPush();
|
const mockPush = mocks.mockPush();
|
||||||
const mockCustoms = mocks.mockCustoms();
|
const mockCustoms = mocks.mockCustoms();
|
||||||
|
const verificationReminders = mocks.mockVerificationReminders();
|
||||||
const accountRoutes = makeRoutes({
|
const accountRoutes = makeRoutes({
|
||||||
checkPassword: function () {
|
checkPassword: function () {
|
||||||
return P.resolve(true);
|
return P.resolve(true);
|
||||||
|
@ -462,9 +465,22 @@ describe('/recovery_email/verify_code', () => {
|
||||||
db: mockDB,
|
db: mockDB,
|
||||||
log: mockLog,
|
log: mockLog,
|
||||||
mailer: mockMailer,
|
mailer: mockMailer,
|
||||||
push: mockPush
|
push: mockPush,
|
||||||
|
verificationReminders,
|
||||||
});
|
});
|
||||||
const route = getRoute(accountRoutes, '/recovery_email/verify_code');
|
const route = getRoute(accountRoutes, '/recovery_email/verify_code');
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockDB.verifyTokens.resetHistory();
|
||||||
|
mockDB.verifyEmail.resetHistory();
|
||||||
|
mockLog.activityEvent.resetHistory();
|
||||||
|
mockLog.flowEvent.resetHistory();
|
||||||
|
mockLog.notifyAttachedServices.resetHistory();
|
||||||
|
mockMailer.sendPostVerifyEmail.resetHistory();
|
||||||
|
mockPush.notifyAccountUpdated.resetHistory();
|
||||||
|
verificationReminders.delete.resetHistory();
|
||||||
|
});
|
||||||
|
|
||||||
describe('verifyTokens rejects with INVALID_VERIFICATION_CODE', () => {
|
describe('verifyTokens rejects with INVALID_VERIFICATION_CODE', () => {
|
||||||
|
|
||||||
it('without a reminder payload', () => {
|
it('without a reminder payload', () => {
|
||||||
|
@ -513,16 +529,12 @@ describe('/recovery_email/verify_code', () => {
|
||||||
assert.ok(Array.isArray(args[1]), 'second argument should have been devices array');
|
assert.ok(Array.isArray(args[1]), 'second argument should have been devices array');
|
||||||
assert.equal(args[2], 'accountVerify', 'third argument should have been reason');
|
assert.equal(args[2], 'accountVerify', 'third argument should have been reason');
|
||||||
|
|
||||||
|
assert.equal(verificationReminders.delete.callCount, 1);
|
||||||
|
args = verificationReminders.delete.args[0];
|
||||||
|
assert.lengthOf(args, 1);
|
||||||
|
assert.equal(args[0], uid);
|
||||||
|
|
||||||
assert.equal(JSON.stringify(response), '{}');
|
assert.equal(JSON.stringify(response), '{}');
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
mockDB.verifyTokens.resetHistory();
|
|
||||||
mockDB.verifyEmail.resetHistory();
|
|
||||||
mockLog.activityEvent.resetHistory();
|
|
||||||
mockLog.flowEvent.resetHistory();
|
|
||||||
mockLog.notifyAttachedServices.resetHistory();
|
|
||||||
mockMailer.sendPostVerifyEmail.resetHistory();
|
|
||||||
mockPush.notifyAccountUpdated.resetHistory();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -542,16 +554,6 @@ describe('/recovery_email/verify_code', () => {
|
||||||
assert.equal(args[0].user_properties.newsletter_state, 'subscribed', 'newsletter_state was correct');
|
assert.equal(args[0].user_properties.newsletter_state, 'subscribed', 'newsletter_state was correct');
|
||||||
|
|
||||||
assert.equal(JSON.stringify(response), '{}');
|
assert.equal(JSON.stringify(response), '{}');
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
delete mockRequest.payload.marketingOptIn;
|
|
||||||
mockDB.verifyTokens.resetHistory();
|
|
||||||
mockDB.verifyEmail.resetHistory();
|
|
||||||
mockLog.activityEvent.resetHistory();
|
|
||||||
mockLog.flowEvent.resetHistory();
|
|
||||||
mockLog.notifyAttachedServices.resetHistory();
|
|
||||||
mockMailer.sendPostVerifyEmail.resetHistory();
|
|
||||||
mockPush.notifyAccountUpdated.resetHistory();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -559,25 +561,18 @@ describe('/recovery_email/verify_code', () => {
|
||||||
mockRequest.payload.reminder = 'second';
|
mockRequest.payload.reminder = 'second';
|
||||||
|
|
||||||
return runTest(route, mockRequest, (response) => {
|
return runTest(route, mockRequest, (response) => {
|
||||||
assert.equal(mockLog.activityEvent.callCount, 1, 'activityEvent was called once');
|
assert.equal(mockLog.activityEvent.callCount, 1);
|
||||||
|
|
||||||
assert.equal(mockLog.flowEvent.callCount, 2, 'flowEvent was called twice');
|
assert.equal(mockLog.flowEvent.callCount, 3);
|
||||||
assert.equal(mockLog.flowEvent.args[0][0].event, 'email.verify_code.clicked', 'first event was email.verify_code.clicked');
|
assert.equal(mockLog.flowEvent.args[0][0].event, 'email.verify_code.clicked');
|
||||||
assert.equal(mockLog.flowEvent.args[1][0].event, 'account.verified', 'second event was account.verified');
|
assert.equal(mockLog.flowEvent.args[1][0].event, 'account.verified');
|
||||||
|
assert.equal(mockLog.flowEvent.args[2][0].event, 'account.reminder.second');
|
||||||
|
|
||||||
assert.equal(mockMailer.sendPostVerifyEmail.callCount, 1, 'sendPostVerifyEmail was called once');
|
assert.equal(verificationReminders.delete.callCount, 1);
|
||||||
assert.equal(mockPush.notifyAccountUpdated.callCount, 1, 'mockPush.notifyAccountUpdated should have been called once');
|
assert.equal(mockMailer.sendPostVerifyEmail.callCount, 1);
|
||||||
|
assert.equal(mockPush.notifyAccountUpdated.callCount, 1);
|
||||||
|
|
||||||
assert.equal(JSON.stringify(response), '{}');
|
assert.equal(JSON.stringify(response), '{}');
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
mockDB.verifyTokens.resetHistory();
|
|
||||||
mockDB.verifyEmail.resetHistory();
|
|
||||||
mockLog.activityEvent.resetHistory();
|
|
||||||
mockLog.flowEvent.resetHistory();
|
|
||||||
mockLog.notifyAttachedServices.resetHistory();
|
|
||||||
mockMailer.sendPostVerifyEmail.resetHistory();
|
|
||||||
mockPush.notifyAccountUpdated.resetHistory();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -597,9 +592,6 @@ describe('/recovery_email/verify_code', () => {
|
||||||
assert.equal(mockLog.activityEvent.callCount, 0, 'log.activityEvent was not called');
|
assert.equal(mockLog.activityEvent.callCount, 0, 'log.activityEvent was not called');
|
||||||
assert.equal(mockPush.notifyAccountUpdated.callCount, 0, 'mockPush.notifyAccountUpdated should not have been called');
|
assert.equal(mockPush.notifyAccountUpdated.callCount, 0, 'mockPush.notifyAccountUpdated should not have been called');
|
||||||
assert.equal(mockPush.notifyDeviceConnected.callCount, 0, 'mockPush.notifyDeviceConnected should not have been called (no devices)');
|
assert.equal(mockPush.notifyDeviceConnected.callCount, 0, 'mockPush.notifyDeviceConnected should not have been called (no devices)');
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
mockDB.verifyTokens.resetHistory();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -618,9 +610,6 @@ describe('/recovery_email/verify_code', () => {
|
||||||
assert.equal(mockLog.activityEvent.callCount, 0, 'log.activityEvent was not called');
|
assert.equal(mockLog.activityEvent.callCount, 0, 'log.activityEvent was not called');
|
||||||
assert.equal(mockPush.notifyAccountUpdated.callCount, 0, 'mockPush.notifyAccountUpdated should not have been called');
|
assert.equal(mockPush.notifyAccountUpdated.callCount, 0, 'mockPush.notifyAccountUpdated should not have been called');
|
||||||
assert.equal(mockPush.notifyDeviceConnected.callCount, 1, 'mockPush.notifyDeviceConnected should have been called');
|
assert.equal(mockPush.notifyDeviceConnected.callCount, 1, 'mockPush.notifyDeviceConnected should have been called');
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
mockDB.verifyTokens.resetHistory();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -650,11 +639,6 @@ describe('/recovery_email/verify_code', () => {
|
||||||
assert.equal(args[0].toString('hex'), uid, 'first argument should have been uid');
|
assert.equal(args[0].toString('hex'), uid, 'first argument should have been uid');
|
||||||
assert.ok(Array.isArray(args[1]), 'second argument should have been devices array');
|
assert.ok(Array.isArray(args[1]), 'second argument should have been devices array');
|
||||||
assert.equal(args[2], 'accountConfirm', 'third argument should have been reason');
|
assert.equal(args[2], 'accountConfirm', 'third argument should have been reason');
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
mockDB.verifyTokens.resetHistory();
|
|
||||||
mockLog.activityEvent.resetHistory();
|
|
||||||
mockPush.notifyAccountUpdated.resetHistory();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -678,12 +662,6 @@ describe('/recovery_email/verify_code', () => {
|
||||||
assert.equal(args[2].secondaryEmail, dbData.secondEmail, 'correct secondary email was passed');
|
assert.equal(args[2].secondaryEmail, dbData.secondEmail, 'correct secondary email was passed');
|
||||||
assert.equal(args[2].service, mockRequest.payload.service);
|
assert.equal(args[2].service, mockRequest.payload.service);
|
||||||
assert.equal(args[2].uid, uid);
|
assert.equal(args[2].uid, uid);
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
mockDB.verifyEmail.resetHistory();
|
|
||||||
mockLog.activityEvent.resetHistory();
|
|
||||||
mockMailer.sendPostVerifySecondaryEmail.resetHistory();
|
|
||||||
mockPush.notifyAccountUpdated.resetHistory();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -799,8 +777,8 @@ describe('/recovery_email', () => {
|
||||||
assert.equal(args[1].email, TEST_EMAIL_ADDITIONAL, 'call db.createEmail with correct email');
|
assert.equal(args[1].email, TEST_EMAIL_ADDITIONAL, 'call db.createEmail with correct email');
|
||||||
assert.equal(mockMailer.sendVerifySecondaryEmail.callCount, 1, 'call mailer.sendVerifySecondaryEmail');
|
assert.equal(mockMailer.sendVerifySecondaryEmail.callCount, 1, 'call mailer.sendVerifySecondaryEmail');
|
||||||
|
|
||||||
assert.equal(mockLog.info.callCount, 1);
|
assert.equal(mockLog.info.callCount, 5);
|
||||||
args = mockLog.info.args[0];
|
args = mockLog.info.args[4];
|
||||||
assert.lengthOf(args, 2);
|
assert.lengthOf(args, 2);
|
||||||
assert.equal(args[0], 'accountDeleted.unverifiedSecondaryEmail');
|
assert.equal(args[0], 'accountDeleted.unverifiedSecondaryEmail');
|
||||||
assert.equal(args[1].normalizedEmail, TEST_EMAIL);
|
assert.equal(args[1].normalizedEmail, TEST_EMAIL);
|
||||||
|
|
|
@ -63,8 +63,8 @@ describe('recovery codes', () => {
|
||||||
assert.equal(args[0], UID, 'called with uid');
|
assert.equal(args[0], UID, 'called with uid');
|
||||||
assert.equal(args[1], 8, 'called with recovery code count');
|
assert.equal(args[1], 8, 'called with recovery code count');
|
||||||
|
|
||||||
assert.equal(log.info.callCount, 1);
|
assert.equal(log.info.callCount, 2);
|
||||||
args = log.info.args[0];
|
args = log.info.args[1];
|
||||||
assert.equal(args[0], 'account.recoveryCode.replaced');
|
assert.equal(args[0], 'account.recoveryCode.replaced');
|
||||||
assert.equal(args[1].uid, UID);
|
assert.equal(args[1].uid, UID);
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,8 +45,8 @@ describe('/session/verify/token', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('called log.info correctly', () => {
|
it('called log.info correctly', () => {
|
||||||
assert.equal(log.info.callCount, 1);
|
assert.equal(log.info.callCount, 2);
|
||||||
const args = log.info.args[0];
|
const args = log.info.args[1];
|
||||||
assert.equal(args.length, 2);
|
assert.equal(args.length, 2);
|
||||||
assert.equal(args[0], 'account.token.code.verified');
|
assert.equal(args[0], 'account.token.code.verified');
|
||||||
});
|
});
|
||||||
|
|
|
@ -399,8 +399,8 @@ describe('checkCustomsAndLoadAccount', () => {
|
||||||
assert.calledWithExactly(request.emitMetricsEvent.getCall(0), 'account.login.blocked');
|
assert.calledWithExactly(request.emitMetricsEvent.getCall(0), 'account.login.blocked');
|
||||||
assert.calledWithExactly(request.emitMetricsEvent.getCall(1), 'account.login.invalidUnblockCode');
|
assert.calledWithExactly(request.emitMetricsEvent.getCall(1), 'account.login.invalidUnblockCode');
|
||||||
|
|
||||||
assert.calledOnce(log.info);
|
assert.equal(log.info.callCount, 2);
|
||||||
assert.calledWithMatch(log.info, 'Account.login.unblockCode.expired');
|
assert.equal(log.info.args[1][0], 'Account.login.unblockCode.expired');
|
||||||
|
|
||||||
assert.calledOnce(customs.flag);
|
assert.calledOnce(customs.flag);
|
||||||
assert.calledWithExactly(customs.flag, CLIENT_ADDRESS, {
|
assert.calledWithExactly(customs.flag, CLIENT_ADDRESS, {
|
||||||
|
|
|
@ -35,6 +35,8 @@ const messageTypes = [
|
||||||
'postVerifySecondaryEmail',
|
'postVerifySecondaryEmail',
|
||||||
'recoveryEmail',
|
'recoveryEmail',
|
||||||
'unblockCodeEmail',
|
'unblockCodeEmail',
|
||||||
|
'verificationReminderFirstEmail',
|
||||||
|
'verificationReminderSecondEmail',
|
||||||
'verifyEmail',
|
'verifyEmail',
|
||||||
'verifyLoginEmail',
|
'verifyLoginEmail',
|
||||||
'verifyLoginCodeEmail',
|
'verifyLoginCodeEmail',
|
||||||
|
@ -56,6 +58,8 @@ const typesContainSupportLinks = [
|
||||||
'postRemoveTwoStepAuthenticationEmail',
|
'postRemoveTwoStepAuthenticationEmail',
|
||||||
'postVerifyEmail',
|
'postVerifyEmail',
|
||||||
'recoveryEmail',
|
'recoveryEmail',
|
||||||
|
'verificationReminderFirstEmail',
|
||||||
|
'verificationReminderSecondEmail',
|
||||||
'verifyEmail',
|
'verifyEmail',
|
||||||
'verifyLoginCodeEmail',
|
'verifyLoginCodeEmail',
|
||||||
'verifyPrimaryEmail',
|
'verifyPrimaryEmail',
|
||||||
|
@ -716,10 +720,9 @@ describe(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'verifyEmail') {
|
switch (type) {
|
||||||
it(
|
case 'verifyEmail':
|
||||||
'passes the OAuth relier name to the template',
|
it('passes the OAuth relier name to the template', () => {
|
||||||
() => {
|
|
||||||
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
||||||
assert.equal(oauthClientInfo.fetch.callCount, 1);
|
assert.equal(oauthClientInfo.fetch.callCount, 1);
|
||||||
assert.equal(oauthClientInfo.fetch.args[0][0], 'foo');
|
assert.equal(oauthClientInfo.fetch.args[0][0], 'foo');
|
||||||
|
@ -728,11 +731,8 @@ describe(
|
||||||
});
|
});
|
||||||
message.service = 'foo';
|
message.service = 'foo';
|
||||||
return mailer[type](message);
|
return mailer[type](message);
|
||||||
}
|
});
|
||||||
);
|
it('works without a service', () => {
|
||||||
it(
|
|
||||||
'works without a service',
|
|
||||||
() => {
|
|
||||||
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
||||||
assert.isFalse(oauthClientInfo.fetch.called);
|
assert.isFalse(oauthClientInfo.fetch.called);
|
||||||
assert.ok(! includes(emailConfig.html, 'and continue to'));
|
assert.ok(! includes(emailConfig.html, 'and continue to'));
|
||||||
|
@ -740,12 +740,11 @@ describe(
|
||||||
});
|
});
|
||||||
delete message.service;
|
delete message.service;
|
||||||
return mailer[type](message);
|
return mailer[type](message);
|
||||||
}
|
});
|
||||||
);
|
break;
|
||||||
} else if (type === 'verifyLoginEmail') {
|
|
||||||
it(
|
case 'verifyLoginEmail':
|
||||||
'test verify token email',
|
it('test verify token email', () => {
|
||||||
() => {
|
|
||||||
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
||||||
const verifyLoginUrl = config.get('smtp').verifyLoginUrl;
|
const verifyLoginUrl = config.get('smtp').verifyLoginUrl;
|
||||||
assert.equal(emailConfig.subject, 'Confirm new sign-in to Firefox');
|
assert.equal(emailConfig.subject, 'Confirm new sign-in to Firefox');
|
||||||
|
@ -753,22 +752,20 @@ describe(
|
||||||
assert.ok(emailConfig.text.indexOf(verifyLoginUrl) > 0);
|
assert.ok(emailConfig.text.indexOf(verifyLoginUrl) > 0);
|
||||||
});
|
});
|
||||||
return mailer[type](message);
|
return mailer[type](message);
|
||||||
}
|
});
|
||||||
);
|
break;
|
||||||
} else if (type === 'newDeviceLoginEmail') {
|
|
||||||
it(
|
case 'newDeviceLoginEmail':
|
||||||
'test new device login email',
|
it('test new device login email', () => {
|
||||||
() => {
|
|
||||||
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
||||||
assert.equal(emailConfig.subject, 'New sign-in to Firefox');
|
assert.equal(emailConfig.subject, 'New sign-in to Firefox');
|
||||||
});
|
});
|
||||||
return mailer[type](message);
|
return mailer[type](message);
|
||||||
}
|
});
|
||||||
);
|
break;
|
||||||
} else if (type === 'postVerifyEmail') {
|
|
||||||
it(
|
case 'postVerifyEmail':
|
||||||
`test utm params for ${ type}`,
|
it(`test utm params for ${type}`, () => {
|
||||||
() => {
|
|
||||||
const syncLink = mailer._generateUTMLink(config.get('smtp').syncUrl, {}, type, 'connect-device');
|
const syncLink = mailer._generateUTMLink(config.get('smtp').syncUrl, {}, type, 'connect-device');
|
||||||
const androidLink = mailer._generateUTMLink(config.get('smtp').androidUrl, {}, type, 'connect-android');
|
const androidLink = mailer._generateUTMLink(config.get('smtp').androidUrl, {}, type, 'connect-android');
|
||||||
const iosLink = mailer._generateUTMLink(config.get('smtp').iosUrl, {}, type, 'connect-ios');
|
const iosLink = mailer._generateUTMLink(config.get('smtp').iosUrl, {}, type, 'connect-ios');
|
||||||
|
@ -780,9 +777,10 @@ describe(
|
||||||
assert.ok(includes(emailConfig.html, 'utm_source=email'));
|
assert.ok(includes(emailConfig.html, 'utm_source=email'));
|
||||||
});
|
});
|
||||||
return mailer[type](message);
|
return mailer[type](message);
|
||||||
}
|
});
|
||||||
);
|
break;
|
||||||
} else if (type === 'verifyPrimaryEmail') {
|
|
||||||
|
case 'verifyPrimaryEmail':
|
||||||
it('test verify token email', () => {
|
it('test verify token email', () => {
|
||||||
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
||||||
const verifyPrimaryEmailUrl = config.get('smtp').verifyPrimaryEmailUrl;
|
const verifyPrimaryEmailUrl = config.get('smtp').verifyPrimaryEmailUrl;
|
||||||
|
@ -793,6 +791,37 @@ describe(
|
||||||
});
|
});
|
||||||
return mailer[type](message);
|
return mailer[type](message);
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'verificationReminderFirstEmail':
|
||||||
|
it('emailConfig includes data specific to verificationReminderFirstEmail', () => {
|
||||||
|
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
||||||
|
assert.include(emailConfig.html, 'reminder=first');
|
||||||
|
assert.include(emailConfig.text, 'reminder=first');
|
||||||
|
assert.include(emailConfig.html, 'utm_campaign=fx-first-verification-reminder');
|
||||||
|
assert.include(emailConfig.text, 'utm_campaign=fx-first-verification-reminder');
|
||||||
|
assert.include(emailConfig.html, 'utm_content=fx-activate-oneclick');
|
||||||
|
assert.include(emailConfig.text, 'utm_content=fx-activate');
|
||||||
|
assert.equal(emailConfig.subject, 'Hello again');
|
||||||
|
});
|
||||||
|
return mailer[type](message);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'verificationReminderSecondEmail':
|
||||||
|
it('emailConfig includes data specific to verificationReminderSecondEmail', () => {
|
||||||
|
mailer.mailer.sendMail = stubSendMail(emailConfig => {
|
||||||
|
assert.include(emailConfig.html, 'reminder=second');
|
||||||
|
assert.include(emailConfig.text, 'reminder=second');
|
||||||
|
assert.include(emailConfig.html, 'utm_campaign=fx-second-verification-reminder');
|
||||||
|
assert.include(emailConfig.text, 'utm_campaign=fx-second-verification-reminder');
|
||||||
|
assert.include(emailConfig.html, 'utm_content=fx-activate-oneclick');
|
||||||
|
assert.include(emailConfig.text, 'utm_content=fx-activate');
|
||||||
|
assert.equal(emailConfig.subject, 'Still there?');
|
||||||
|
});
|
||||||
|
return mailer[type](message);
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -897,15 +926,15 @@ describe(
|
||||||
|
|
||||||
return mailer.send(message)
|
return mailer.send(message)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
assert.equal(mockLog.info.callCount, 3);
|
assert.equal(mockLog.info.callCount, 4);
|
||||||
const emailEventLog = mockLog.info.getCalls()[2];
|
const emailEventLog = mockLog.info.getCalls()[3];
|
||||||
assert.equal(emailEventLog.args[0], 'emailEvent');
|
assert.equal(emailEventLog.args[0], 'emailEvent');
|
||||||
assert.equal(emailEventLog.args[1].domain, 'other');
|
assert.equal(emailEventLog.args[1].domain, 'other');
|
||||||
assert.equal(emailEventLog.args[1].flow_id, 'wibble');
|
assert.equal(emailEventLog.args[1].flow_id, 'wibble');
|
||||||
assert.equal(emailEventLog.args[1].template, 'verifyLoginEmail');
|
assert.equal(emailEventLog.args[1].template, 'verifyLoginEmail');
|
||||||
assert.equal(emailEventLog.args[1].type, 'sent');
|
assert.equal(emailEventLog.args[1].type, 'sent');
|
||||||
assert.equal(emailEventLog.args[1].locale, 'en');
|
assert.equal(emailEventLog.args[1].locale, 'en');
|
||||||
const mailerSend1 = mockLog.info.getCalls()[1];
|
const mailerSend1 = mockLog.info.getCalls()[2];
|
||||||
assert.equal(mailerSend1.args[0], 'mailer.send.1');
|
assert.equal(mailerSend1.args[0], 'mailer.send.1');
|
||||||
assert.equal(mailerSend1.args[1].to, message.email);
|
assert.equal(mailerSend1.args[1].to, message.email);
|
||||||
});
|
});
|
||||||
|
|
|
@ -305,8 +305,8 @@ describe('lib/senders/index', () => {
|
||||||
assert.equal(errorBounces.check.callCount, 2);
|
assert.equal(errorBounces.check.callCount, 2);
|
||||||
assert.equal(e.errno, error.ERRNO.BOUNCE_COMPLAINT);
|
assert.equal(e.errno, error.ERRNO.BOUNCE_COMPLAINT);
|
||||||
|
|
||||||
assert.ok(log.info.callCount >= 2);
|
assert.isAtLeast(log.info.callCount, 3);
|
||||||
const msg = log.info.args[1];
|
const msg = log.info.args[2];
|
||||||
assert.equal(msg[0], 'mailer.blocked');
|
assert.equal(msg[0], 'mailer.blocked');
|
||||||
assert.equal(msg[1].errno, e.errno);
|
assert.equal(msg[1].errno, e.errno);
|
||||||
assert.equal(msg[1].bouncedAt, DATE);
|
assert.equal(msg[1].bouncedAt, DATE);
|
||||||
|
@ -364,8 +364,8 @@ describe('lib/senders/index', () => {
|
||||||
assert.equal(errorBounces.check.callCount, 1);
|
assert.equal(errorBounces.check.callCount, 1);
|
||||||
assert.equal(e.errno, error.ERRNO.BOUNCE_COMPLAINT);
|
assert.equal(e.errno, error.ERRNO.BOUNCE_COMPLAINT);
|
||||||
|
|
||||||
assert.ok(log.info.callCount >= 2);
|
assert.isAtLeast(log.info.callCount, 3);
|
||||||
const msg = log.info.args[1];
|
const msg = log.info.args[2];
|
||||||
assert.equal(msg[0], 'mailer.blocked');
|
assert.equal(msg[0], 'mailer.blocked');
|
||||||
assert.equal(msg[1].errno, e.errno);
|
assert.equal(msg[1].errno, e.errno);
|
||||||
assert.equal(msg[1].bouncedAt, DATE);
|
assert.equal(msg[1].bouncedAt, DATE);
|
||||||
|
|
|
@ -40,7 +40,7 @@ describe('lib/senders/templates/index:', () => {
|
||||||
it('result is correct', () => {
|
it('result is correct', () => {
|
||||||
assert.equal(typeof result, 'object');
|
assert.equal(typeof result, 'object');
|
||||||
const keys = Object.keys(result);
|
const keys = Object.keys(result);
|
||||||
assert.equal(keys.length, 25);
|
assert.equal(keys.length, 27);
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
const fn = result[key];
|
const fn = result[key];
|
||||||
assert.equal(typeof fn, 'function');
|
assert.equal(typeof fn, 'function');
|
||||||
|
|
|
@ -533,6 +533,7 @@ function getConfig () {
|
||||||
metrics: {
|
metrics: {
|
||||||
flow_id_expiry: 7200000,
|
flow_id_expiry: 7200000,
|
||||||
flow_id_key: 'wibble'
|
flow_id_key: 'wibble'
|
||||||
}
|
},
|
||||||
|
verificationReminders: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const ROOT_DIR = '../..';
|
||||||
|
const REMINDERS = [ 'first', 'second', 'third' ];
|
||||||
|
const EXPECTED_CREATE_DELETE_RESULT = REMINDERS.reduce((expected, reminder) => {
|
||||||
|
expected[reminder] = 1;
|
||||||
|
return expected;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const { assert } = require('chai');
|
||||||
|
const config = require(`${ROOT_DIR}/config`).getProperties();
|
||||||
|
const mocks = require('../mocks');
|
||||||
|
|
||||||
|
describe('lib/verification-reminders:', () => {
|
||||||
|
let log, mockConfig, redis, verificationReminders;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
log = mocks.mockLog();
|
||||||
|
mockConfig = {
|
||||||
|
redis: config.redis,
|
||||||
|
verificationReminders: {
|
||||||
|
rolloutRate: 1,
|
||||||
|
firstInterval: 1,
|
||||||
|
secondInterval: 2,
|
||||||
|
thirdInterval: 60000,
|
||||||
|
redis: {
|
||||||
|
maxConnections: 1,
|
||||||
|
minConnections: 1,
|
||||||
|
prefix: 'test-verification-reminders:',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
redis = require('fxa-shared/redis')({
|
||||||
|
...config.redis,
|
||||||
|
...mockConfig.verificationReminders.redis,
|
||||||
|
enabled: true,
|
||||||
|
}, mocks.mockLog());
|
||||||
|
verificationReminders = require(`${ROOT_DIR}/lib/verification-reminders`)(log, mockConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returned the expected interface', () => {
|
||||||
|
assert.isObject(verificationReminders);
|
||||||
|
assert.lengthOf(Object.keys(verificationReminders), 4);
|
||||||
|
|
||||||
|
assert.deepEqual(verificationReminders.keys, [ 'first', 'second', 'third' ]);
|
||||||
|
|
||||||
|
assert.isFunction(verificationReminders.create);
|
||||||
|
assert.lengthOf(verificationReminders.create, 1);
|
||||||
|
|
||||||
|
assert.isFunction(verificationReminders.delete);
|
||||||
|
assert.lengthOf(verificationReminders.delete, 1);
|
||||||
|
|
||||||
|
assert.isFunction(verificationReminders.process);
|
||||||
|
assert.lengthOf(verificationReminders.process, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('called log.info correctly', () => {
|
||||||
|
assert.equal(log.info.callCount, 1);
|
||||||
|
const args = log.info.args[0];
|
||||||
|
assert.lengthOf(args, 2);
|
||||||
|
assert.equal(args[0], 'redis.enabled');
|
||||||
|
assert.isObject(args[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create:', () => {
|
||||||
|
let createResult;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Clobber keys to assert that misbehaving callers can't wreck the internal behaviour
|
||||||
|
verificationReminders.keys = [];
|
||||||
|
createResult = await verificationReminders.create('wibble');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
return verificationReminders.delete('wibble');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returned the correct result', async () => {
|
||||||
|
assert.deepEqual(createResult, EXPECTED_CREATE_DELETE_RESULT);
|
||||||
|
});
|
||||||
|
|
||||||
|
REMINDERS.forEach(reminder => {
|
||||||
|
it(`wrote ${reminder} reminder to redis correctly`, async () => {
|
||||||
|
const reminders = await redis.zrange(reminder, 0, -1);
|
||||||
|
assert.deepEqual(reminders, [ 'wibble' ]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('called log.info correctly', () => {
|
||||||
|
assert.equal(log.info.callCount, 2);
|
||||||
|
const args = log.info.args[1];
|
||||||
|
assert.lengthOf(args, 2);
|
||||||
|
assert.equal(args[0], 'verificationReminders.create');
|
||||||
|
assert.deepEqual(args[1], { uid: 'wibble' });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete:', () => {
|
||||||
|
let deleteResult;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
deleteResult = await verificationReminders.delete('wibble');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returned the correct result', async () => {
|
||||||
|
assert.deepEqual(deleteResult, EXPECTED_CREATE_DELETE_RESULT);
|
||||||
|
});
|
||||||
|
|
||||||
|
REMINDERS.forEach(reminder => {
|
||||||
|
it(`removed ${reminder} reminder from redis correctly`, async () => {
|
||||||
|
const reminders = await redis.zrange(reminder, 0, -1);
|
||||||
|
assert.lengthOf(reminders, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('did not call log.error', () => {
|
||||||
|
assert.equal(log.error.callCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('called log.info correctly', () => {
|
||||||
|
assert.equal(log.info.callCount, 3);
|
||||||
|
const args = log.info.args[2];
|
||||||
|
assert.lengthOf(args, 2);
|
||||||
|
assert.equal(args[0], 'verificationReminders.delete');
|
||||||
|
assert.deepEqual(args[1], { uid: 'wibble' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('process:', () => {
|
||||||
|
let processResult, after;
|
||||||
|
|
||||||
|
beforeEach(done => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
processResult = await verificationReminders.process();
|
||||||
|
after = Date.now();
|
||||||
|
setImmediate(done);
|
||||||
|
}, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returned the correct result', async () => {
|
||||||
|
assert.deepEqual(processResult, {
|
||||||
|
first: [ 'wibble' ],
|
||||||
|
second: [ 'wibble' ],
|
||||||
|
third: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
REMINDERS.forEach(reminder => {
|
||||||
|
if (reminder !== 'third') {
|
||||||
|
it(`removed ${reminder} reminder from redis correctly`, async () => {
|
||||||
|
const reminders = await redis.zrange(reminder, 0, -1);
|
||||||
|
assert.lengthOf(reminders, 0);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it('left the third reminder in redis', async () => {
|
||||||
|
const reminders = await redis.zrange(reminder, 0, -1);
|
||||||
|
assert.deepEqual(reminders, [ 'wibble' ]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('did not call log.error', () => {
|
||||||
|
assert.equal(log.error.callCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('called log.info correctly', () => {
|
||||||
|
assert.equal(log.info.callCount, 5);
|
||||||
|
|
||||||
|
let args = log.info.args[2];
|
||||||
|
assert.lengthOf(args, 2);
|
||||||
|
assert.equal(args[0], 'verificationReminders.process');
|
||||||
|
assert.isObject(args[1]);
|
||||||
|
assert.equal(args[1].key, 'first');
|
||||||
|
assert.isAtMost(args[1].now, after);
|
||||||
|
assert.isAbove(args[1].now, after - 1000);
|
||||||
|
assert.equal(args[1].cutoff, args[1].now - mockConfig.verificationReminders.firstInterval);
|
||||||
|
|
||||||
|
args = log.info.args[3];
|
||||||
|
assert.equal(args[1].key, 'second');
|
||||||
|
assert.equal(args[1].now, log.info.args[2][1].now);
|
||||||
|
assert.equal(args[1].cutoff, args[1].now - mockConfig.verificationReminders.secondInterval);
|
||||||
|
|
||||||
|
args = log.info.args[4];
|
||||||
|
assert.equal(args[1].key, 'third');
|
||||||
|
assert.equal(args[1].now, log.info.args[2][1].now);
|
||||||
|
assert.equal(args[1].cutoff, args[1].now - mockConfig.verificationReminders.thirdInterval);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -171,7 +171,8 @@ module.exports = {
|
||||||
mockMetricsContext,
|
mockMetricsContext,
|
||||||
mockPush,
|
mockPush,
|
||||||
mockPushbox,
|
mockPushbox,
|
||||||
mockRequest
|
mockRequest,
|
||||||
|
mockVerificationReminders,
|
||||||
};
|
};
|
||||||
|
|
||||||
function mockCustoms (errors) {
|
function mockCustoms (errors) {
|
||||||
|
@ -543,7 +544,8 @@ function mockRequest (data, errors) {
|
||||||
const events = require('../lib/metrics/events')(data.log || module.exports.mockLog(), {
|
const events = require('../lib/metrics/events')(data.log || module.exports.mockLog(), {
|
||||||
oauth: {
|
oauth: {
|
||||||
clientIds: data.clientIds || {}
|
clientIds: data.clientIds || {}
|
||||||
}
|
},
|
||||||
|
verificationReminders: {},
|
||||||
});
|
});
|
||||||
const metricsContext = data.metricsContext || module.exports.mockMetricsContext();
|
const metricsContext = data.metricsContext || module.exports.mockMetricsContext();
|
||||||
|
|
||||||
|
@ -612,3 +614,12 @@ function mockRequest (data, errors) {
|
||||||
validateMetricsContext: metricsContext.validate
|
validateMetricsContext: metricsContext.validate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockVerificationReminders (data = {}) {
|
||||||
|
return {
|
||||||
|
keys: [ 'first', 'second', 'third' ],
|
||||||
|
create: sinon.spy(() => data.create || { first: 1, second: 1, third: 1 }),
|
||||||
|
delete: sinon.spy(() => data.delete || { first: 1, second: 1, third: 1 }),
|
||||||
|
process: sinon.spy(() => data.process || { first: [], second: [], third: [] }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -161,7 +161,7 @@ describe('reentrant updates of different keys:', () => {
|
||||||
let error;
|
let error;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
const redisPool = require('fxa-shared/redis/pool')({
|
const { pool: redisPool } = require('fxa-shared/redis/pool')({
|
||||||
...config.redis,
|
...config.redis,
|
||||||
...config.redis.sessionTokens
|
...config.redis.sessionTokens
|
||||||
}, log);
|
}, log);
|
||||||
|
|
Загрузка…
Ссылка в новой задаче