Merge branch 'main' into MNTOR-938/auth-v2

This commit is contained in:
Joey Zhou 2022-10-22 22:07:22 -07:00
Родитель 06a8813122 7f38242d79
Коммит d4e69ad12a
14 изменённых файлов: 273 добавлений и 107 удалений

Просмотреть файл

@ -1,5 +1,7 @@
# Firefox Monitor Server
[![Coverage Status](https://coveralls.io/repos/github/mozilla/blurts-server/badge.svg?branch=main)](https://coveralls.io/github/mozilla/blurts-server?branch=main)
## Summary
Firefox Monitor notifies users when their credentials have been compromised in a data breach.
@ -46,6 +48,16 @@ To run linting/formatting as you type or upon save, add the ESLint and Stylelint
```
See here for more on Stylelint config with VSCode: https://github.com/stylelint/vscode-stylelint#editorcodeactionsonsave
### GIT
We track commits that are largely style/formatting via `.git-blame-ignore-revs`. This allows Git Blame to ignore the format commit author and show the original code author. In order to enable this in GitLens, add the following to VS Code `settings.json`:
```
"gitlens.advanced.blame.customArguments": [
"--ignore-revs-file",
".git-blame-ignore-revs"
],
```
### Install
1. Clone and change to the directory:
@ -148,13 +160,11 @@ the `OAUTH_CLIENT_SECRET` value from someone in #fxmonitor-engineering.
## Testing
The full test suite can be run via `npm test`.
The full test suite can be run via `npm test`.
At the beginning of a test suite run, the `test-blurts` database will be populated with test tables and seed data found in `db/seeds/`
At the end of a test suite run, coverage info will be sent to [Coveralls](https://coveralls.io/) to assess coverage changes and provide a neat badge. For this step to complete locally, you need a root `.coveralls.yml` which contains a token – get this from another member of the Monitor team. Alternatively, without the token you can simply ignore the `coveralls` error.
*TODO:* Disable Coveralls step for local testing?
At the end of a test suite run in CircleCI, coverage info will be sent to [Coveralls](https://coveralls.io/) to assess coverage changes and provide a neat badge. To upload coverage locally, you need a root `.coveralls.yml` which contains a token – get this from another member of the Monitor team.
### Individual tests
@ -224,4 +234,4 @@ If you encounter issues with Heroku deploys, be sure to check your environment v
A banner has been added to inform users whether their IP address is being masked by Mozilla VPN. It also uses their IP address to demonstrate geolocation. This can inform users why they might use Mozilla VPN for privacy.
The IP location data includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com. For localhost, a test MaxMind database with limited data is included with this repo. For the Heroku Dev site, the following buildpack is used to enable geolocation: https://github.com/HiMamaInc/heroku-buildpack-geoip-geolite2. For stage and prod environments, a shared database is set via env vars.
The IP location data includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com. For localhost, a test MaxMind database with limited data is included with this repo. For the Heroku Dev site, the following buildpack is used to enable geolocation: https://github.com/HiMamaInc/heroku-buildpack-geoip-geolite2. For stage and prod environments, a shared database is set via env vars.

Просмотреть файл

@ -99,6 +99,7 @@ const EmailUtils = {
},
getVerificationUrl (subscriber) {
if (!subscriber.verification_token) throw new Error('subscriber has no verification_token')
let url = new URL(`${AppConstants.SERVER_URL}/user/verify`)
url = this.appendUtmParams(url, 'verified-subscribers', 'account-verification-email')
url.searchParams.append('token', encodeURIComponent(subscriber.verification_token))
@ -108,8 +109,8 @@ const EmailUtils = {
getUnsubscribeUrl (subscriber, emailType) {
// TODO: email unsubscribe is broken for most emails
let url = new URL(`${AppConstants.SERVER_URL}/user/unsubscribe`)
const token = (subscriber.hasOwnProperty('verification_token')) ? subscriber.verification_token : subscriber.primary_verification_token
const hash = (subscriber.hasOwnProperty('sha1')) ? subscriber.sha1 : subscriber.primary_sha1
const token = (Object.prototype.hasOwnProperty.call(subscriber, 'verification_token')) ? subscriber.verification_token : subscriber.primary_verification_token
const hash = (Object.prototype.hasOwnProperty.call(subscriber, 'sha1')) ? subscriber.sha1 : subscriber.primary_sha1
url.searchParams.append('token', encodeURIComponent(token))
url.searchParams.append('hash', encodeURIComponent(hash))
url = this.appendUtmParams(url, 'unsubscribe', emailType)
@ -118,6 +119,7 @@ const EmailUtils = {
getMonthlyUnsubscribeUrl (subscriber, campaign, content) {
// TODO: create new subscriptions section in settings to manage all emails and avoid one-off routes like this
if (!subscriber.primary_verification_token) throw new Error('subscriber has no primary verification_token')
let url = new URL('user/unsubscribe-monthly/', AppConstants.SERVER_URL)
url = this.appendUtmParams(url, campaign, content)

Просмотреть файл

@ -57,43 +57,8 @@ fxm-warns-you-found-breaches =
email-breach-alert-blurb =
{ -product-name } advarer dig om datalæk, der omfatter dine personlige data.
Vi har lige modtaget detaljer om et andet firmas datalæk.
# List headline
faq-list-headline = Ofte stillede spørgsmål
# Link Title
faq-v2-1 = Jeg genkender ikke nogle af disse firma eller websteder. Hvorfor er jeg i denne datalæk?
# Link Title
faq-v2-2 = Behøver jeg at foretage mig noget, hvis en datalæk skete for år tilbage eller hvis det er en gammel konto?
# Link Title
faq-v2-3 = Jeg har lige fundet ud af, at jeg er omfattet af en datalæk. Hvad gør jeg nu?
# Link Title
faq-v2-4 = Hvordan behandler { -product-name } websteder, der har følsomme data om sine brugere?
# This string contains nested markup that becomes a link to Firefox Monitor
# later in the code. Please do not modify or remove "<a>" and "</a>".
pre-fxa-message = <a>Opret en gratis { -brand-fxa }</a> , så du kan tilføje op til 15 mailadresser.
# Section headline
monitor-another-email = Vil du overvåge en anden mailadresse?
# Subject line of email
pre-fxa-subject = Nyheder fra { -product-name }
pre-fxa-headline = Ændringer i { -product-name }
pre-fxa-blurb = Vi har ændret noget, siden du tilmeldte dig { -product-name } - en service der holder øje med, om dine personlige data optræder i kendte datalæk. Din profil er nu tilknyttet din Firefox-konto.
pre-fxa-tout-1 = Få advarsler om flere datalæk
pre-fxa-p-1 = <a>Opret en konto</a> for at holde øje med op til 15 mailadresser og få besked, hvis de er ramt af datalæk. Vi anbefaler, at du tilføjer alle mailadresser, du bruger til at oprette online-konti.
pre-fxa-tout-2 = Få et bedre overblik
pre-fxa-p-2 =
Se alle datalæk på ét sted, så du ved, hvilke adgangskoder der skal skiftes.
Oversigten med datalæk er kun tilgængelig med en konto.
pre-fxa-tout-3 = Fortsæt med at modtage advarsler via mail
pre-fxa-p-3 =
Du vil stadig modtage advarsler fra { -product-name }. Vi informerer dig, hvis dine personlige data
optræder i en ny datalæk.
# Button at the bottom of pre-fxa email.
create-account = Opret konto
# More security products
more-products-headline = Beskyt dig selv med flere af vores produkter
more-products-vpn = Fuld beskyttelse af alle dine enheder.
more-products-cta-vpn = Hent { -product-name-vpn }
more-products-relay = Skjul din rigtige mailadresse for at beskytte din identitet.
more-products-cta-relay = Hent { -product-name-relay }
## 2022 email template. HTML tags should not be translated, e.g. `<a>`
@ -117,3 +82,21 @@ email-resolved = Løste datalæk:
# table row 4 label
email-unresolved = Uløste datalæk:
email-resolve-cta = Løs datalæk
## Verification email
email-verify-heading = Beskyt dine data med det samme
email-verify-subhead = Bekræft din mailadresse for at beskytte dine data efter en datalæk.
email-verify-simply-click = Klik på linket nedenfor for at færdiggøre bekræften af din konto.
## Breach report
email-breach-summary = Her er din oversigt over datalæk
email-breach-detected = Resultatet af en søgning efter { $email-address } viser, at din mailadresse kan være involveret i en datalæk. Vi anbefaler, at du reagerer med det samme for at løse denne datalæk.
email-no-breach-detected = Gode nyheder! Vi har ikke fundet nogen datalæk, der påvirker din mailadresse, { $email-address }.
email-dashboard-cta = Gå til oversigten
## Breach alert
email-may-have-been-exposed = Din mailadresse kan have været involveret i en datalæk.
email-spotted-new-breach = Vi har opdaget en ny datalæk

Просмотреть файл

@ -49,41 +49,8 @@ email-sensitive-disclaimer = 由于该数据外泄事件的敏感性,相关的
fxm-warns-you-no-breaches = { -product-name } 会在有与您相关的个人信息外泄事件发生时警告您。目前为止,未发生过外泄事件。我们会在您的电子邮件地址出现在新事件中时通知您。
fxm-warns-you-found-breaches = { -product-name } 会在有与您相关的个人信息外泄事件发生时警告您。当您的电子邮件地址出现在新事件中时,您会收到订阅的警报。
email-breach-alert-blurb = { -product-name } 会在有与您相关的个人信息外泄事件发生时警告您。我们刚收到其他公司的数据外泄事件。
# List headline
faq-list-headline = 常见问题
# Link Title
faq-v2-1 = 我不认识其中的一家公司或网站,为什么我与该外泄事件有关?
# Link Title
faq-v2-2 = 如果外泄事件发生在几年前,或是已经不用的账号,我还需要做什么吗?
# Link Title
faq-v2-3 = 我刚刚发现自己遭受了数据外泄。接下来我该怎么做?
# Link Title
faq-v2-4 = { -product-name } 如何处理这些敏感网站?
# This string contains nested markup that becomes a link to Firefox Monitor
# later in the code. Please do not modify or remove "<a>" and "</a>".
pre-fxa-message = <a>创建免费的 { -brand-fxa }</a>,您最多可添加 15 个电子邮件地址。
# Section headline
monitor-another-email = 要监控其他电子邮件地址吗?
# Subject line of email
pre-fxa-subject = { -product-name } 的更新
pre-fxa-headline = { -product-name } 的变化
pre-fxa-blurb = { -product-name } —— 保障个人信息的数据泄露监控服务,在您注册之后已经有一些改变。我们已将它与 Firefox 账号相连。
pre-fxa-tout-1 = 警惕新发生的数据泄露
pre-fxa-p-1 = <a>创建一个账号</a>,监控最多 15 个电子邮件地址是否涉及到数据泄露。我们推荐您添加常用于网上账号的所有电子邮件地址。
pre-fxa-tout-2 = 获得概况面板
pre-fxa-p-2 =
同一个地方检查已知的所有数据泄露,了解您需要更换哪些密码。
该面板仅供注册用户使用。
pre-fxa-tout-3 = 接收电子邮件通知
pre-fxa-p-3 = 您仍然会收到 { -product-name } 的警讯。如果您的信息出现在新发生的数据泄漏中,我们会通知您。
# Button at the bottom of pre-fxa email.
create-account = 创建账号
# More security products
more-products-headline = 用我们的系列产品保护自己
more-products-vpn = 全方位保护您的每台设备
more-products-cta-vpn = 下载 { -product-name-vpn }
more-products-relay = 隐藏您的的真实邮箱地址,保护身份信息。
more-products-cta-relay = 下载 { -product-name-relay }
## 2022 email template. HTML tags should not be translated, e.g. `<a>`
@ -111,6 +78,8 @@ email-verify-simply-click = 请尽快点击下方链接,完成账户验证。
## Breach report
email-breach-summary = 以下是您的数据外泄情况概览
email-no-breach-detected = 好消息!我们并未发现与您邮箱 { $email-address } 有关的数据外泄事件。
email-dashboard-cta = 前往面板
## Breach alert

Просмотреть файл

@ -97,7 +97,7 @@
"test:integration-headless": "MOZ_HEADLESS=1 wdio tests/integration/wdio.conf.js",
"test:integration-headless-ci": "MOZ_HEADLESS=1 ERROR_SHOTS=1 wdio tests/integration/wdio.conf.js",
"test:integration-docker": "MOZ_HEADLESS=1 wdio tests/integration/wdio.docker.js",
"test": "npm run test:db:migrate && npm run test:tests && npm run test:coveralls"
"test": "npm run test:db:migrate && npm run test:tests && (node scripts/is-coveralls-configured.js && npm run test:coveralls || echo 'Skipping coveralls.')"
},
"workspaces": [
"src"

Двоичные данные
public/img/logos/JobAndTalent.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.1 KiB

Двоичные данные
public/img/logos/Jobandtalent.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.1 KiB

Просмотреть файл

@ -0,0 +1,21 @@
'use strict'
/*
Check if coveralls configuration is available.
Exit 0 if configured to run coveralls
Exit 1 if coveralls will fail
For required environment variables, see:
https://github.com/nickmerwin/node-coveralls
*/
const { existsSync } = require('node:fs')
if (process.env.COVERALLS_REPO_TOKEN) {
console.log('coveralls configured with environment variable COVERALLS_REPO_TOKEN.')
} else if (existsSync('.coveralls.yml')) {
console.log('coveralls configured with configuration file ".coveralls.yml".')
} else {
console.log('coveralls is not configured.')
process.exit(1)
}

Просмотреть файл

@ -4,12 +4,20 @@
box-sizing: border-box;
}
html {
height: 100%;
}
body {
min-width: 320px;
height: 100%;
padding-top: var(--header-h);
font: normal 1rem/1.2 Inter, sans-serif;
}
header {
position: fixed;
top: 0;
width: 100%;
display: flex;
justify-content: space-between;
@ -20,23 +28,35 @@ header {
transition: box-shadow 0.3s;
}
header .monitor-logo {
transform: scale(var(--multiplier));
transform-origin: left;
}
html.scrolled header {
box-shadow: 0 4px 8px -8px black;
}
footer {
position: sticky;
top: 100%;
display: flex;
justify-content: space-between;
color: white;
background: black;
padding: var(--padding-md) var(--padding-lg);
padding: var(--padding-lg);
}
footer img {
display: block;
}
menu {
display: flex;
align-items: center;
gap: var(--padding-md);
gap: var(--padding-lg);
list-style-type: none;
font-size: 0.875rem;
}
a {
@ -53,10 +73,11 @@ button,
font-family: inherit;
font-size: 0.875rem;
font-weight: normal;
line-height: 40px;
padding: 0 var(--padding-lg);
line-height: 0.75;
padding: var(--padding-lg);
cursor: pointer;
box-shadow: inset 0 0 0 1px;
white-space: nowrap;
}
.button-primary {

Просмотреть файл

@ -1,7 +1,15 @@
:root {
--multiplier: 1;
--header-h: 76px; /* set in resize-observer.js and added here for discovery */
--border-radius: 4px;
--padding-sm: 8px;
--padding-md: 16px;
--padding-lg: 32px;
--padding-sm: calc(4px * var(--multiplier));
--padding-md: calc(8px * var(--multiplier));
--padding-lg: calc(16px * var(--multiplier));
--blue-50: #0060df;
}
@media (max-width: 480px) {
:root {
--multiplier: 0.75;
}
}

Просмотреть файл

@ -1,2 +1,3 @@
import './app.js'
import './scroll-observer.js'
import './resize-observer.js'

Просмотреть файл

@ -0,0 +1,19 @@
const resizeObserver = new ResizeObserver(handleResize)
const header = document.querySelector('body header')
function handleResize (entries) {
let h
entries.forEach((entry) => {
switch (entry.target) {
case header:
h = entry.borderBoxSize[0].blockSize
if (header.h === h) return
document.documentElement.style.setProperty('--header-h', `${Math.round(h)}px`)
header.h = h
break
}
})
}
resizeObserver.observe(header)

Просмотреть файл

@ -1,11 +1,12 @@
import { getMessage } from '../../utils/fluent.js'
import { getMessage, getPattern } from '../../utils/fluent.js'
export default data => `
<!doctype html>
<html lang=${data.locale}>
<head>
<meta charset='utf-8'>
<title>${getMessage('take-control')}</title>
<meta name="viewport" content="width=320, initial-scale=1">
<title>${getPattern('home-title')}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet">
@ -14,8 +15,8 @@ export default data => `
</head>
<body>
<header>
<a href='/'><img src='images/monitor-logo-transparent.png' width='220' height='40'></a>
<a href='/user/login' class='button'>Sign Up</a>
<a href='/'><img class='monitor-logo' src='images/monitor-logo-transparent.png' width='220' height='40'></a>
<a href='/breach-details' class='button'>${getMessage('sign-up')}</a>
</header>
<nav>
</nav>
@ -25,7 +26,7 @@ export default data => `
<footer>
<a href='https://www.mozilla.org' target='_blank'><img src='images/moz-logo-1color-white-rgb-01.svg' width='100'></a>
<menu>
<li><a href='https://www.mozilla.org/privacy/firefox-monitor' target='_blank'>Terms & Privacy</a></li>
<li><a href='https://www.mozilla.org/privacy/firefox-monitor' target='_blank'>${getMessage('terms-and-privacy')}</a></li>
<li><a href='https://github.com/mozilla/blurts-server' target='_blank' rel='noreferrer'>GitHub</a></li>
</menu>
</footer>

Просмотреть файл

@ -3,40 +3,149 @@
const nodemailer = require('nodemailer')
const EmailUtils = require('../email-utils')
const { EMAIL_FROM, SERVER_URL, SES_CONFIG_SET } = require('../app-constants')
const { TEST_SUBSCRIBERS, TEST_EMAIL_ADDRESSES } = require('../db/seeds/test_subscribers')
jest.mock('nodemailer')
test('EmailUtils.init with empty host doesnt invoke nodemailer', () => {
nodemailer.createTransport = jest.fn()
EmailUtils.init('')
const mockCreateTransport = nodemailer.createTransport.mock
expect(mockCreateTransport.calls.length).toBe(1)
expect(mockCreateTransport.calls[0][0]).toEqual({ jsonTransport: true })
test('EmailUtils.sendEmail before .init() fails', async () => {
const sendMailArgs = ['test@example.com', 'subject', 'template.hbs', { breach: 'Test' }]
const expectedError = new Error('SMTP transport not initialized')
await expect(EmailUtils.sendEmail(...sendMailArgs)).rejects.toEqual(expectedError)
})
test.skip('EmailUtils.init with user, pass, host, port invokes nodemailer.createTransport', () => {
test('EmailUtils.init with empty host uses jsonTransport', async () => {
nodemailer.createTransport = jest.fn()
await expect(EmailUtils.init('')).resolves.toBe(true)
expect(nodemailer.createTransport).toHaveBeenCalledWith({ jsonTransport: true })
})
test('EmailUtils.init with SMTP URL invokes nodemailer.createTransport', async () => {
const testSmtpUrl = 'smtps://test:test@test:1'
nodemailer.createTransport = jest.fn()
const mockTransporter = {
verify: jest.fn().mockReturnValueOnce("✓"),
use: jest.fn(),
}
nodemailer.createTransport = jest.fn().mockReturnValueOnce(mockTransporter)
EmailUtils.init(testSmtpUrl)
await expect(EmailUtils.init(testSmtpUrl)).resolves.toBe("✓")
const mockCreateTransport = nodemailer.createTransport.mock
expect(mockCreateTransport.calls.length).toBe(1)
expect(mockCreateTransport.calls[0][0]).toBe(testSmtpUrl)
expect(nodemailer.createTransport).toHaveBeenCalledWith(testSmtpUrl)
expect(mockTransporter.verify).toHaveBeenCalledWith()
expect(mockTransporter.use.mock.calls.length).toBe(1)
expect(mockTransporter.use.mock.calls[0].length).toEqual(2)
expect(mockTransporter.use.mock.calls[0][0]).toBe('compile')
})
test.skip('EmailUtils.sendEmail with recipient, subject, template, context calls gTransporter.sendMail', () => {
test('EmailUtils.sendEmail with recipient, subject, template, context calls gTransporter.sendMail', async () => {
const testSmtpUrl = 'smtps://test:test@test:1'
const sendMailArgs = ['test@example.com', 'subject', 'template.hbs', { breach: 'Test' }]
nodemailer.createTransport = jest.fn()
const mockTransporter = {
verify: jest.fn().mockReturnValueOnce("verified"),
use: jest.fn(),
sendMail: jest.fn((options, cb) => cb(null, "sent")),
transporter: {name: 'MockTransporter'}
}
nodemailer.createTransport = jest.fn().mockReturnValueOnce(mockTransporter)
EmailUtils.init(testSmtpUrl)
EmailUtils.sendEmail(...sendMailArgs)
await expect(EmailUtils.init(testSmtpUrl)).resolves.toBe("verified")
await expect(EmailUtils.sendEmail(...sendMailArgs)).resolves.toBe("sent")
// TODO: find a way to expect gTransporter.sendMail
expect(mockTransporter.sendMail.mock.calls.length).toBe(1)
expect(mockTransporter.sendMail.mock.calls[0].length).toBe(2)
expect(mockTransporter.sendMail.mock.calls[0][0]).toEqual(
{
context: {
SERVER_URL,
layout: 'template.hbs',
breach: 'Test',
},
from: EMAIL_FROM,
headers: {"x-ses-configuration-set": SES_CONFIG_SET},
subject: "subject",
template: "template.hbs",
to: "test@example.com",
})
})
test('EmailUtils.sendEmail rejects with error', async () => {
const testSmtpUrl = 'smtps://test:test@test:1'
const sendMailArgs = ['test@example.com', 'subject', 'template.hbs', { breach: 'Test' }]
const mockTransporter = {
verify: jest.fn().mockReturnValueOnce("verified"),
use: jest.fn(),
sendMail: jest.fn((options, cb) => cb("error", null)),
transporter: {name: 'MockTransporter'}
}
nodemailer.createTransport = jest.fn().mockReturnValueOnce(mockTransporter)
await expect(EmailUtils.init('smtps://test:test@test:1')).resolves.toBe("verified")
await expect(EmailUtils.sendEmail(...sendMailArgs)).rejects.toBe("error")
})
test('EmailUtils.init with empty host uses jsonTransport. logs messages', async () => {
const sendMailArgs = ['test@example.com', 'subject', 'template.hbs', { breach: 'Test' }]
const sendMailInfo = {message: "sent"}
const mockTransporter = {
sendMail: jest.fn((options, cb) => cb(null, sendMailInfo)),
transporter: {name: 'JSONTransport'}
}
nodemailer.createTransport = jest.fn().mockReturnValueOnce(mockTransporter)
await expect(EmailUtils.init('')).resolves.toBe(true)
expect(nodemailer.createTransport).toHaveBeenCalledWith({ jsonTransport: true })
await expect(EmailUtils.sendEmail(...sendMailArgs)).resolves.toEqual(sendMailInfo)
})
test('EmailUtils.getEmailCtaHref works without a subscriber ID', () => {
const emailCtaHref = EmailUtils.getEmailCtaHref('email-type', 'content')
expect(emailCtaHref.pathname).toBe('/')
emailCtaHref.searchParams.sort()
expect(Array.from(emailCtaHref.searchParams.entries())).toEqual(
[
['utm_campaign', 'email-type'],
['utm_content', 'content'],
['utm_medium', 'email'],
['utm_source', 'fx-monitor'],
])
})
test('EmailUtils.getEmailCtaHref works with a subscriber ID', () => {
const emailCtaHref = EmailUtils.getEmailCtaHref('email-type-2', 'content-2', 1234)
expect(emailCtaHref.pathname).toBe('/')
emailCtaHref.searchParams.sort()
expect(Array.from(emailCtaHref.searchParams.entries())).toEqual(
[
['subscriber_id', '1234'],
['utm_campaign', 'email-type-2'],
['utm_content', 'content-2'],
['utm_medium', 'email'],
['utm_source', 'fx-monitor'],
])
})
test('EmailUtils.getVerificationUrl returns a URL', () => {
const fakeSubscriber = {"verification_token": "SubscriberVerificationToken"}
const verificationUrl = EmailUtils.getVerificationUrl(fakeSubscriber)
expect(verificationUrl.pathname).toBe('/user/verify')
verificationUrl.searchParams.sort()
expect(Array.from(verificationUrl.searchParams.entries())).toEqual(
[
['token', 'SubscriberVerificationToken'],
['utm_campaign', 'verified-subscribers'],
['utm_content', 'account-verification-email'],
['utm_medium', 'email'],
['utm_source', 'fx-monitor'],
])
})
test('EmailUtils.getVerificationUrl throws when subscriber has no token', () => {
const fakeSubscriber = {"verification_token": null}
const expected = 'subscriber has no verification_token'
expect(() => EmailUtils.getVerificationUrl(fakeSubscriber)).toThrow(expected)
})
test('EmailUtils.getUnsubscribeUrl works with subscriber record', () => {
@ -56,3 +165,25 @@ test('EmailUtils.getUnsubscribeUrl works with email_address record', () => {
expect(unsubUrl).toMatch(emailAddressRecord.sha1)
expect(unsubUrl).toMatch(emailAddressRecord.verification_token)
})
test('EmailUtils.getMonthlyUnsubscribeUrl returns unsubscribe URL', () => {
const fakeSubscriber = {'primary_verification_token': 'PrimaryVerificationToken'}
const unsubUrl = EmailUtils.getMonthlyUnsubscribeUrl(fakeSubscriber, 'campaign', 'content')
expect(unsubUrl.pathname).toBe('/user/unsubscribe-monthly/')
unsubUrl.searchParams.sort()
expect(Array.from(unsubUrl.searchParams.entries())).toEqual(
[
['token', 'PrimaryVerificationToken'],
['utm_campaign', 'campaign'],
['utm_content', 'content'],
['utm_medium', 'email'],
['utm_source', 'fx-monitor'],
])
})
test('EmailUtils.getMonthlyUnsubscribeUrl throws when subscriber has no token', () => {
const fakeSubscriber = {'primary_verification_token': null}
const expected = 'subscriber has no primary verification_token'
expect(() => EmailUtils.getMonthlyUnsubscribeUrl(fakeSubscriber, 'campaign', 'content')).toThrow(expected)
})