EmailBuilder.test.mjs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047
  1. import { vi, expect } from 'vitest'
  2. import cheerio from 'cheerio'
  3. import path from 'node:path'
  4. import EmailMessageHelper from '../../../../app/src/Features/Email/EmailMessageHelper.mjs'
  5. import ctaEmailBody from '../../../../app/src/Features/Email/Bodies/cta-email.mjs'
  6. import NoCTAEmailBody from '../../../../app/src/Features/Email/Bodies/NoCTAEmailBody.mjs'
  7. import BaseWithHeaderEmailLayout from '../../../../app/src/Features/Email/Layouts/BaseWithHeaderEmailLayout.mjs'
  8. const MODULE_PATH = path.join(
  9. import.meta.dirname,
  10. '../../../../app/src/Features/Email/EmailBuilder'
  11. )
  12. describe('EmailBuilder', function () {
  13. beforeEach(async function (ctx) {
  14. ctx.settings = {
  15. appName: 'testApp',
  16. siteUrl: 'https://www.overleaf.com',
  17. }
  18. vi.doMock('../../../../app/src/Features/Email/EmailMessageHelper', () => ({
  19. default: EmailMessageHelper,
  20. }))
  21. vi.doMock('../../../../app/src/Features/Email/Bodies/cta-email', () => ({
  22. default: ctaEmailBody,
  23. }))
  24. vi.doMock(
  25. '../../../../app/src/Features/Email/Bodies/NoCTAEmailBody',
  26. () => ({
  27. default: NoCTAEmailBody,
  28. })
  29. )
  30. vi.doMock(
  31. '../../../../app/src/Features/Email/Layouts/BaseWithHeaderEmailLayout',
  32. () => ({
  33. default: BaseWithHeaderEmailLayout,
  34. })
  35. )
  36. vi.doMock('@overleaf/settings', () => ({
  37. default: ctx.settings,
  38. }))
  39. ctx.EmailBuilder = (await import(MODULE_PATH)).default
  40. })
  41. describe('projectInvite', function () {
  42. beforeEach(function (ctx) {
  43. ctx.opts = {
  44. to: 'bob@bob.com',
  45. first_name: 'bob',
  46. owner: {
  47. email: 'sally@hally.com',
  48. },
  49. inviteUrl: 'http://example.com/invite',
  50. project: {
  51. url: 'http://www.project.com',
  52. name: 'standard project',
  53. },
  54. }
  55. })
  56. describe('when sending a normal email', function () {
  57. beforeEach(function (ctx) {
  58. ctx.email = ctx.EmailBuilder.buildEmail('projectInvite', ctx.opts)
  59. })
  60. it('should have html and text properties', function (ctx) {
  61. expect(ctx.email.html != null).to.equal(true)
  62. expect(ctx.email.text != null).to.equal(true)
  63. })
  64. it('should not have undefined in it', function (ctx) {
  65. ctx.email.html.indexOf('undefined').should.equal(-1)
  66. ctx.email.subject.indexOf('undefined').should.equal(-1)
  67. })
  68. })
  69. describe('when dealing with escaping', function () {
  70. it("should not show possessive 's as '", function (ctx) {
  71. ctx.opts.project.name = "Aktöbe's project"
  72. ctx.email = ctx.EmailBuilder.buildEmail('projectInvite', ctx.opts)
  73. expect(ctx.email.subject).to.not.contain(''')
  74. expect(ctx.email.subject).to.contain(ctx.opts.project.name)
  75. })
  76. it('should not show an ampersand as &', function (ctx) {
  77. ctx.opts.project.name = 'Aktöbe & Almaty project'
  78. ctx.email = ctx.EmailBuilder.buildEmail('projectInvite', ctx.opts)
  79. expect(ctx.email.subject).to.not.contain('&')
  80. expect(ctx.email.subject).to.contain(ctx.opts.project.name)
  81. })
  82. it('should prevent dangerous characters as project names', function (ctx) {
  83. const characters = ['""', '<>', '//']
  84. for (const pair of characters) {
  85. ctx.opts.project.name = `${pair} project`
  86. ctx.email = ctx.EmailBuilder.buildEmail('projectInvite', ctx.opts)
  87. expect(ctx.email.subject).to.not.contain(pair)
  88. }
  89. })
  90. })
  91. describe('when someone is up to no good', function () {
  92. it('should not contain the project name at all if unsafe', function (ctx) {
  93. ctx.opts.project.name = "<img src='http://evilsite.com/evil.php'>"
  94. ctx.email = ctx.EmailBuilder.buildEmail('projectInvite', ctx.opts)
  95. expect(ctx.email.html).to.not.contain('evilsite.com')
  96. expect(ctx.email.subject).to.not.contain('evilsite.com')
  97. // but email should appear
  98. expect(ctx.email.html).to.contain(ctx.opts.owner.email)
  99. expect(ctx.email.subject).to.contain(ctx.opts.owner.email)
  100. })
  101. it('should not contain the inviter email at all if unsafe', function (ctx) {
  102. ctx.opts.owner.email =
  103. 'verylongemailaddressthatwillfailthecheck@longdomain.domain'
  104. ctx.email = ctx.EmailBuilder.buildEmail('projectInvite', ctx.opts)
  105. expect(ctx.email.html).to.not.contain(ctx.opts.owner.email)
  106. expect(ctx.email.subject).to.not.contain(ctx.opts.owner.email)
  107. // but title should appear
  108. expect(ctx.email.html).to.contain(ctx.opts.project.name)
  109. expect(ctx.email.subject).to.contain(ctx.opts.project.name)
  110. })
  111. it('should handle both email and title being unsafe', function (ctx) {
  112. ctx.opts.project.name = "<img src='http://evilsite.com/evil.php'>"
  113. ctx.opts.owner.email =
  114. 'verylongemailaddressthatwillfailthecheck@longdomain.domain'
  115. ctx.email = ctx.EmailBuilder.buildEmail('projectInvite', ctx.opts)
  116. expect(ctx.email.html).to.not.contain('evilsite.com')
  117. expect(ctx.email.subject).to.not.contain('evilsite.com')
  118. expect(ctx.email.html).to.not.contain(ctx.opts.owner.email)
  119. expect(ctx.email.subject).to.not.contain(ctx.opts.owner.email)
  120. expect(ctx.email.html).to.contain(
  121. 'Please view the project to find out more'
  122. )
  123. })
  124. })
  125. })
  126. describe('SpamSafe', function () {
  127. beforeEach(function (ctx) {
  128. ctx.opts = {
  129. to: 'bob@joe.com',
  130. first_name: 'bob',
  131. newOwner: {
  132. email: 'sally@hally.com',
  133. },
  134. inviteUrl: 'http://example.com/invite',
  135. project: {
  136. url: 'http://www.project.com',
  137. name: 'come buy my product at http://notascam.com',
  138. },
  139. }
  140. ctx.email = ctx.EmailBuilder.buildEmail(
  141. 'ownershipTransferConfirmationPreviousOwner',
  142. ctx.opts
  143. )
  144. })
  145. it('should replace spammy project name', function (ctx) {
  146. ctx.email.html.indexOf('your project').should.not.equal(-1)
  147. })
  148. })
  149. describe('ctaTemplate', function () {
  150. describe('missing required content', function () {
  151. const content = {
  152. title: () => {},
  153. greeting: () => {},
  154. message: () => {},
  155. secondaryMessage: () => {},
  156. ctaText: () => {},
  157. ctaURL: () => {},
  158. gmailGoToAction: () => {},
  159. }
  160. it('should throw an error when missing title', function (ctx) {
  161. const { title, ...missing } = content
  162. expect(() => {
  163. ctx.EmailBuilder.ctaTemplate(missing)
  164. }).to.throw(Error)
  165. })
  166. it('should throw an error when missing message', function (ctx) {
  167. const { message, ...missing } = content
  168. expect(() => {
  169. ctx.EmailBuilder.ctaTemplate(missing)
  170. }).to.throw(Error)
  171. })
  172. it('should throw an error when missing ctaText', function (ctx) {
  173. const { ctaText, ...missing } = content
  174. expect(() => {
  175. ctx.EmailBuilder.ctaTemplate(missing)
  176. }).to.throw(Error)
  177. })
  178. it('should throw an error when missing ctaURL', function (ctx) {
  179. const { ctaURL, ...missing } = content
  180. expect(() => {
  181. ctx.EmailBuilder.ctaTemplate(missing)
  182. }).to.throw(Error)
  183. })
  184. })
  185. })
  186. describe('templates', function () {
  187. describe('CTA', function () {
  188. describe('canceledSubscription', function () {
  189. beforeEach(function (ctx) {
  190. ctx.emailAddress = 'example@overleaf.com'
  191. ctx.opts = {
  192. to: ctx.emailAddress,
  193. }
  194. ctx.email = ctx.EmailBuilder.buildEmail(
  195. 'canceledSubscription',
  196. ctx.opts
  197. )
  198. ctx.expectedUrl =
  199. 'https://docs.google.com/forms/d/e/1FAIpQLSfa7z_s-cucRRXm70N4jEcSbFsZeb0yuKThHGQL8ySEaQzF0Q/viewform?usp=sf_link'
  200. })
  201. it('should build the email', function (ctx) {
  202. expect(ctx.email.html).to.exist
  203. expect(ctx.email.text).to.exist
  204. })
  205. describe('HTML email', function () {
  206. it('should include a CTA button and a fallback CTA link', function (ctx) {
  207. const dom = cheerio.load(ctx.email.html)
  208. const buttonLink = dom('a:contains("Leave Feedback")')
  209. expect(buttonLink.length).to.equal(1)
  210. expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
  211. const fallback = dom('.force-overleaf-style').last()
  212. expect(fallback.length).to.equal(1)
  213. const fallbackLink = fallback.html()
  214. expect(fallbackLink).to.contain(ctx.expectedUrl)
  215. })
  216. })
  217. describe('plain text email', function () {
  218. it('should contain the CTA link', function (ctx) {
  219. expect(ctx.email.text).to.contain(ctx.expectedUrl)
  220. })
  221. })
  222. })
  223. describe('canceledSubscriptionOrAddOn', function () {
  224. beforeEach(function (ctx) {
  225. ctx.emailAddress = 'example@overleaf.com'
  226. ctx.opts = {
  227. to: ctx.emailAddress,
  228. }
  229. ctx.email = ctx.EmailBuilder.buildEmail(
  230. 'canceledSubscriptionOrAddOn',
  231. ctx.opts
  232. )
  233. ctx.expectedUrl =
  234. 'https://digitalscience.qualtrics.com/jfe/form/SV_2n2aSlWgvoxXdGK'
  235. })
  236. it('should build the email', function (ctx) {
  237. expect(ctx.email.html).to.exist
  238. expect(ctx.email.text).to.exist
  239. })
  240. describe('HTML email', function () {
  241. it('should include a CTA button and a fallback CTA link', function (ctx) {
  242. const dom = cheerio.load(ctx.email.html)
  243. const buttonLink = dom('a:contains("Leave feedback")')
  244. expect(buttonLink.length).to.equal(1)
  245. expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
  246. const fallback = dom('.force-overleaf-style').last()
  247. expect(fallback.length).to.equal(1)
  248. const fallbackLink = fallback.html()
  249. expect(fallbackLink).to.contain(ctx.expectedUrl)
  250. })
  251. })
  252. describe('plain text email', function () {
  253. it('should contain the CTA link', function (ctx) {
  254. expect(ctx.email.text).to.contain(ctx.expectedUrl)
  255. })
  256. })
  257. })
  258. describe('confirmEmail', function () {
  259. beforeEach(function (ctx) {
  260. ctx.emailAddress = 'example@overleaf.com'
  261. ctx.userId = 'abc123'
  262. ctx.opts = {
  263. to: ctx.emailAddress,
  264. confirmEmailUrl: `${ctx.settings.siteUrl}/user/emails/confirm?token=aToken123`,
  265. sendingUser_id: ctx.userId,
  266. }
  267. ctx.email = ctx.EmailBuilder.buildEmail('confirmEmail', ctx.opts)
  268. })
  269. it('should build the email', function (ctx) {
  270. expect(ctx.email.html).to.exist
  271. expect(ctx.email.text).to.exist
  272. })
  273. describe('HTML email', function () {
  274. it('should include a CTA button and a fallback CTA link', function (ctx) {
  275. const dom = cheerio.load(ctx.email.html)
  276. const buttonLink = dom('a:contains("Confirm email")')
  277. expect(buttonLink.length).to.equal(1)
  278. expect(buttonLink.attr('href')).to.equal(ctx.opts.confirmEmailUrl)
  279. const fallback = dom('.force-overleaf-style').last()
  280. expect(fallback.length).to.equal(1)
  281. const fallbackLink = fallback.html()
  282. expect(fallbackLink).to.contain(ctx.opts.confirmEmailUrl)
  283. })
  284. })
  285. describe('plain text email', function () {
  286. it('should contain the CTA link', function (ctx) {
  287. expect(ctx.email.text).to.contain(ctx.opts.confirmEmailUrl)
  288. })
  289. })
  290. })
  291. describe('ownershipTransferConfirmationNewOwner', function () {
  292. beforeEach(function (ctx) {
  293. ctx.emailAddress = 'example@overleaf.com'
  294. ctx.opts = {
  295. to: ctx.emailAddress,
  296. previousOwner: {},
  297. project: {
  298. _id: 'abc123',
  299. name: 'example project',
  300. },
  301. }
  302. ctx.email = ctx.EmailBuilder.buildEmail(
  303. 'ownershipTransferConfirmationNewOwner',
  304. ctx.opts
  305. )
  306. ctx.expectedUrl = `${
  307. ctx.settings.siteUrl
  308. }/project/${ctx.opts.project._id.toString()}`
  309. })
  310. it('should build the email', function (ctx) {
  311. expect(ctx.email.html).to.exist
  312. expect(ctx.email.text).to.exist
  313. })
  314. describe('HTML email', function () {
  315. it('should include a CTA button and a fallback CTA link', function (ctx) {
  316. const dom = cheerio.load(ctx.email.html)
  317. const buttonLink = dom('td a')
  318. expect(buttonLink).to.exist
  319. expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
  320. const fallback = dom('.force-overleaf-style').last()
  321. expect(fallback).to.exist
  322. const fallbackLink = fallback.html().replace(/&amp;/g, '&')
  323. expect(fallbackLink).to.contain(ctx.expectedUrl)
  324. })
  325. })
  326. describe('plain text email', function () {
  327. it('should contain the CTA link', function (ctx) {
  328. expect(ctx.email.text).to.contain(ctx.expectedUrl)
  329. })
  330. })
  331. })
  332. describe('passwordResetRequested', function () {
  333. beforeEach(function (ctx) {
  334. ctx.emailAddress = 'example@overleaf.com'
  335. ctx.opts = {
  336. to: ctx.emailAddress,
  337. setNewPasswordUrl: `${
  338. ctx.settings.siteUrl
  339. }/user/password/set?passwordResetToken=aToken&email=${encodeURIComponent(
  340. ctx.emailAddress
  341. )}`,
  342. }
  343. ctx.email = ctx.EmailBuilder.buildEmail(
  344. 'passwordResetRequested',
  345. ctx.opts
  346. )
  347. })
  348. it('should build the email', function (ctx) {
  349. expect(ctx.email.html).to.exist
  350. expect(ctx.email.text).to.exist
  351. })
  352. describe('HTML email', function () {
  353. it('should include a CTA button and a fallback CTA link', function (ctx) {
  354. const dom = cheerio.load(ctx.email.html)
  355. const buttonLink = dom('td a')
  356. expect(buttonLink).to.exist
  357. expect(buttonLink.attr('href')).to.equal(ctx.opts.setNewPasswordUrl)
  358. const fallback = dom('.force-overleaf-style').last()
  359. expect(fallback).to.exist
  360. const fallbackLink = fallback.html().replace(/&amp;/g, '&')
  361. expect(fallbackLink).to.contain(ctx.opts.setNewPasswordUrl)
  362. })
  363. })
  364. describe('plain text email', function () {
  365. it('should contain the CTA link', function (ctx) {
  366. expect(ctx.email.text).to.contain(ctx.opts.setNewPasswordUrl)
  367. })
  368. })
  369. })
  370. describe('reconfirmEmail', function () {
  371. beforeEach(function (ctx) {
  372. ctx.emailAddress = 'example@overleaf.com'
  373. ctx.userId = 'abc123'
  374. ctx.opts = {
  375. to: ctx.emailAddress,
  376. confirmEmailUrl: `${ctx.settings.siteUrl}/user/emails/confirm?token=aToken123`,
  377. sendingUser_id: ctx.userId,
  378. }
  379. ctx.email = ctx.EmailBuilder.buildEmail('reconfirmEmail', ctx.opts)
  380. })
  381. it('should build the email', function (ctx) {
  382. expect(ctx.email.html).to.exist
  383. expect(ctx.email.text).to.exist
  384. })
  385. describe('HTML email', function () {
  386. it('should include a CTA button and a fallback CTA link', function (ctx) {
  387. const dom = cheerio.load(ctx.email.html)
  388. const buttonLink = dom('a:contains("Reconfirm Email")')
  389. expect(buttonLink.length).to.equal(1)
  390. expect(buttonLink.attr('href')).to.equal(ctx.opts.confirmEmailUrl)
  391. const fallback = dom('.force-overleaf-style').last()
  392. expect(fallback.length).to.equal(1)
  393. const fallbackLink = fallback.html()
  394. expect(fallbackLink).to.contain(ctx.opts.confirmEmailUrl)
  395. })
  396. })
  397. describe('plain text email', function () {
  398. it('should contain the CTA link', function (ctx) {
  399. expect(ctx.email.text).to.contain(ctx.opts.confirmEmailUrl)
  400. })
  401. })
  402. })
  403. describe('verifyEmailToJoinTeam', function () {
  404. beforeEach(function (ctx) {
  405. ctx.emailAddress = 'example@overleaf.com'
  406. ctx.opts = {
  407. to: ctx.emailAddress,
  408. acceptInviteUrl: `${ctx.settings.siteUrl}/subscription/invites/aToken123/`,
  409. inviter: {
  410. email: 'deanna@overleaf.com',
  411. first_name: 'Deanna',
  412. last_name: 'Troi',
  413. },
  414. }
  415. ctx.email = ctx.EmailBuilder.buildEmail(
  416. 'verifyEmailToJoinTeam',
  417. ctx.opts
  418. )
  419. })
  420. it('should build the email', function (ctx) {
  421. expect(ctx.email.html).to.exist
  422. expect(ctx.email.text).to.exist
  423. })
  424. describe('HTML email', function () {
  425. it('should include a CTA button and a fallback CTA link', function (ctx) {
  426. const dom = cheerio.load(ctx.email.html)
  427. const buttonLink = dom('a:contains("Join now")')
  428. expect(buttonLink.length).to.equal(1)
  429. expect(buttonLink.attr('href')).to.equal(ctx.opts.acceptInviteUrl)
  430. const fallback = dom('.force-overleaf-style').last()
  431. expect(fallback.length).to.equal(1)
  432. const fallbackLink = fallback.html()
  433. expect(fallbackLink).to.contain(ctx.opts.acceptInviteUrl)
  434. })
  435. })
  436. describe('plain text email', function () {
  437. it('should contain the CTA link', function (ctx) {
  438. expect(ctx.email.text).to.contain(ctx.opts.acceptInviteUrl)
  439. })
  440. })
  441. })
  442. describe('reactivatedSubscription', function () {
  443. beforeEach(function (ctx) {
  444. ctx.emailAddress = 'example@overleaf.com'
  445. ctx.opts = {
  446. to: ctx.emailAddress,
  447. }
  448. ctx.email = ctx.EmailBuilder.buildEmail(
  449. 'reactivatedSubscription',
  450. ctx.opts
  451. )
  452. ctx.expectedUrl = `${ctx.settings.siteUrl}/user/subscription`
  453. })
  454. it('should build the email', function (ctx) {
  455. expect(ctx.email.html).to.exist
  456. expect(ctx.email.text).to.exist
  457. })
  458. describe('HTML email', function () {
  459. it('should include a CTA button and a fallback CTA link', function (ctx) {
  460. const dom = cheerio.load(ctx.email.html)
  461. const buttonLink = dom('a:contains("View Subscription Dashboard")')
  462. expect(buttonLink.length).to.equal(1)
  463. expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
  464. const fallback = dom('.force-overleaf-style').last()
  465. expect(fallback.length).to.equal(1)
  466. const fallbackLink = fallback.html()
  467. expect(fallbackLink).to.contain(ctx.expectedUrl)
  468. })
  469. })
  470. describe('plain text email', function () {
  471. it('should contain the CTA link', function (ctx) {
  472. expect(ctx.email.text).to.contain(ctx.expectedUrl)
  473. })
  474. })
  475. })
  476. describe('testEmail', function () {
  477. beforeEach(function (ctx) {
  478. ctx.emailAddress = 'example@overleaf.com'
  479. ctx.opts = {
  480. to: ctx.emailAddress,
  481. }
  482. ctx.email = ctx.EmailBuilder.buildEmail('testEmail', ctx.opts)
  483. })
  484. it('should build the email', function (ctx) {
  485. expect(ctx.email.html).to.exist
  486. expect(ctx.email.text).to.exist
  487. })
  488. describe('HTML email', function () {
  489. it('should include a CTA button and a fallback CTA link', function (ctx) {
  490. const dom = cheerio.load(ctx.email.html)
  491. const buttonLink = dom(`a:contains("Open ${ctx.settings.appName}")`)
  492. expect(buttonLink.length).to.equal(1)
  493. expect(buttonLink.attr('href')).to.equal(ctx.settings.siteUrl)
  494. const fallback = dom('.force-overleaf-style').last()
  495. expect(fallback.length).to.equal(1)
  496. const fallbackLink = fallback.html()
  497. expect(fallbackLink).to.contain(ctx.settings.siteUrl)
  498. })
  499. })
  500. describe('plain text email', function () {
  501. it('should contain the CTA link', function (ctx) {
  502. expect(ctx.email.text).to.contain(
  503. `Open ${ctx.settings.appName}: ${ctx.settings.siteUrl}`
  504. )
  505. })
  506. })
  507. })
  508. describe('registered', function () {
  509. beforeEach(function (ctx) {
  510. ctx.emailAddress = 'example@overleaf.com'
  511. ctx.opts = {
  512. to: ctx.emailAddress,
  513. setNewPasswordUrl: `${ctx.settings.siteUrl}/user/activate?token=aToken123&user_id=aUserId123`,
  514. }
  515. ctx.email = ctx.EmailBuilder.buildEmail('registered', ctx.opts)
  516. })
  517. it('should build the email', function (ctx) {
  518. expect(ctx.email.html).to.exist
  519. expect(ctx.email.text).to.exist
  520. })
  521. describe('HTML email', function () {
  522. it('should include a CTA button and a fallback CTA link', function (ctx) {
  523. const dom = cheerio.load(ctx.email.html)
  524. const buttonLink = dom('a:contains("Set password")')
  525. expect(buttonLink.length).to.equal(1)
  526. expect(buttonLink.attr('href')).to.equal(ctx.opts.setNewPasswordUrl)
  527. const fallback = dom('.force-overleaf-style').last()
  528. expect(fallback.length).to.equal(1)
  529. const fallbackLink = fallback.html().replace(/&amp;/, '&')
  530. expect(fallbackLink).to.contain(ctx.opts.setNewPasswordUrl)
  531. })
  532. })
  533. describe('plain text email', function () {
  534. it('should contain the CTA link', function (ctx) {
  535. expect(ctx.email.text).to.contain(ctx.opts.setNewPasswordUrl)
  536. })
  537. })
  538. })
  539. describe('projectInvite', function () {
  540. beforeEach(function (ctx) {
  541. ctx.emailAddress = 'example@overleaf.com'
  542. ctx.owner = {
  543. email: 'owner@example.com',
  544. name: 'Bailey',
  545. }
  546. ctx.projectName = 'Top Secret'
  547. ctx.opts = {
  548. inviteUrl: `${ctx.settings.siteUrl}/project/projectId123/invite/token/aToken123`,
  549. owner: {
  550. email: ctx.owner.email,
  551. },
  552. project: {
  553. name: ctx.projectName,
  554. },
  555. to: ctx.emailAddress,
  556. }
  557. ctx.email = ctx.EmailBuilder.buildEmail('projectInvite', ctx.opts)
  558. })
  559. it('should build the email', function (ctx) {
  560. expect(ctx.email.html).to.exist
  561. expect(ctx.email.text).to.exist
  562. })
  563. describe('HTML email', function () {
  564. it('should include a CTA button and a fallback CTA link', function (ctx) {
  565. const dom = cheerio.load(ctx.email.html)
  566. const buttonLink = dom('a:contains("View project")')
  567. expect(buttonLink.length).to.equal(1)
  568. expect(buttonLink.attr('href')).to.equal(ctx.opts.inviteUrl)
  569. const fallback = dom('.force-overleaf-style').last()
  570. expect(fallback.length).to.equal(1)
  571. const fallbackLink = fallback.html().replace(/&amp;/g, '&')
  572. expect(fallbackLink).to.contain(ctx.opts.inviteUrl)
  573. })
  574. })
  575. describe('plain text email', function () {
  576. it('should contain the CTA link', function (ctx) {
  577. expect(ctx.email.text).to.contain(ctx.opts.inviteUrl)
  578. })
  579. })
  580. })
  581. describe('welcome', function () {
  582. beforeEach(function (ctx) {
  583. ctx.emailAddress = 'example@overleaf.com'
  584. ctx.opts = {
  585. to: ctx.emailAddress,
  586. confirmEmailUrl: `${ctx.settings.siteUrl}/user/emails/confirm?token=token123`,
  587. }
  588. ctx.email = ctx.EmailBuilder.buildEmail('welcome', ctx.opts)
  589. ctx.dom = cheerio.load(ctx.email.html)
  590. })
  591. it('should build the email', function (ctx) {
  592. expect(ctx.email.html).to.exist
  593. expect(ctx.email.text).to.exist
  594. })
  595. describe('HTML email', function () {
  596. it('should include a CTA button and a fallback CTA link', function (ctx) {
  597. const buttonLink = ctx.dom('a:contains("Confirm email")')
  598. expect(buttonLink.length).to.equal(1)
  599. expect(buttonLink.attr('href')).to.equal(ctx.opts.confirmEmailUrl)
  600. const fallback = ctx.dom('.force-overleaf-style').last()
  601. expect(fallback.length).to.equal(1)
  602. expect(fallback.html()).to.contain(ctx.opts.confirmEmailUrl)
  603. })
  604. it('should include help links', function (ctx) {
  605. const helpGuidesLink = ctx.dom('a:contains("Help Guides")')
  606. const templatesLink = ctx.dom('a:contains("Templates")')
  607. const logInLink = ctx.dom('a:contains("log in")')
  608. expect(helpGuidesLink.length).to.equal(1)
  609. expect(templatesLink.length).to.equal(1)
  610. expect(logInLink.length).to.equal(1)
  611. })
  612. })
  613. describe('plain text email', function () {
  614. it('should contain the CTA URL', function (ctx) {
  615. expect(ctx.email.text).to.contain(ctx.opts.confirmEmailUrl)
  616. })
  617. it('should include help URL', function (ctx) {
  618. expect(ctx.email.text).to.contain('/learn')
  619. expect(ctx.email.text).to.contain('/login')
  620. expect(ctx.email.text).to.contain('/templates')
  621. })
  622. it('should contain HTML links', function (ctx) {
  623. expect(ctx.email.text).to.not.contain('<a')
  624. })
  625. })
  626. })
  627. describe('groupSSODisabled', function () {
  628. it('should build the email for non managed and linked users', function (ctx) {
  629. const setNewPasswordUrl = `${ctx.settings.siteUrl}/user/password/reset`
  630. const emailAddress = 'example@overleaf.com'
  631. const opts = {
  632. to: emailAddress,
  633. setNewPasswordUrl,
  634. userIsManaged: false,
  635. }
  636. const email = ctx.EmailBuilder.buildEmail('groupSSODisabled', opts)
  637. expect(email.subject).to.equal(
  638. 'A change to your Overleaf login options'
  639. )
  640. const dom = cheerio.load(email.html)
  641. expect(email.html).to.exist
  642. expect(email.html).to.contain(
  643. 'Your group administrator has disabled single sign-on for your group.'
  644. )
  645. expect(email.html).to.contain(
  646. 'You can still log in to Overleaf using one of our other'
  647. )
  648. const links = dom('a')
  649. expect(links[0].attribs.href).to.equal(
  650. `${ctx.settings.siteUrl}/login`
  651. )
  652. expect(links[1].attribs.href).to.equal(setNewPasswordUrl)
  653. expect(email.html).to.contain(
  654. "If you don't have a password, you can set one now."
  655. )
  656. expect(email.text).to.exist
  657. const expectedPlainText = [
  658. 'Hi,',
  659. '',
  660. 'Your group administrator has disabled single sign-on for your group.',
  661. '',
  662. '',
  663. '',
  664. 'What does this mean for you?',
  665. '',
  666. 'You can still log in to Overleaf using one of our other login options or with your email address and password.',
  667. '',
  668. "If you don't have a password, you can set one now.",
  669. '',
  670. `Set your new password: ${setNewPasswordUrl}`,
  671. '',
  672. '',
  673. '',
  674. 'Regards,',
  675. `The ${ctx.settings.appName} Team - ${ctx.settings.siteUrl}`,
  676. ]
  677. expect(email.text.split(/\r?\n/)).to.deep.equal(expectedPlainText)
  678. })
  679. it('should build the email for managed and linked users', function (ctx) {
  680. const emailAddress = 'example@overleaf.com'
  681. const setNewPasswordUrl = `${ctx.settings.siteUrl}/user/password/reset`
  682. const opts = {
  683. to: emailAddress,
  684. setNewPasswordUrl,
  685. userIsManaged: true,
  686. }
  687. const email = ctx.EmailBuilder.buildEmail('groupSSODisabled', opts)
  688. expect(email.subject).to.equal(
  689. 'Action required: Set your Overleaf password'
  690. )
  691. const dom = cheerio.load(email.html)
  692. expect(email.html).to.exist
  693. expect(email.html).to.contain(
  694. 'Your group administrator has disabled single sign-on for your group.'
  695. )
  696. expect(email.html).to.contain(
  697. 'You now need an email address and password to sign in to your Overleaf account.'
  698. )
  699. const links = dom('a')
  700. expect(links[0].attribs.href).to.equal(
  701. `${ctx.settings.siteUrl}/user/password/reset`
  702. )
  703. expect(email.text).to.exist
  704. const expectedPlainText = [
  705. 'Hi,',
  706. '',
  707. 'Your group administrator has disabled single sign-on for your group.',
  708. '',
  709. '',
  710. '',
  711. 'What does this mean for you?',
  712. '',
  713. 'You now need an email address and password to sign in to your Overleaf account.',
  714. '',
  715. `Set your new password: ${setNewPasswordUrl}`,
  716. '',
  717. '',
  718. '',
  719. 'Regards,',
  720. `The ${ctx.settings.appName} Team - ${ctx.settings.siteUrl}`,
  721. ]
  722. expect(email.text.split(/\r?\n/)).to.deep.equal(expectedPlainText)
  723. })
  724. })
  725. })
  726. describe('no CTA', function () {
  727. describe('securityAlert', function () {
  728. beforeEach(function (ctx) {
  729. ctx.message = 'more details about the action'
  730. ctx.messageHTML = `<br /><span style="text-align:center" class="a-class"><b><i>${ctx.message}</i></b></span>`
  731. ctx.messageNotAllowedHTML = `<div></div>${ctx.messageHTML}`
  732. ctx.actionDescribed = 'an action described'
  733. ctx.actionDescribedHTML = `<br /><span style="text-align:center" class="a-class"><b><i>${ctx.actionDescribed}</i></b>`
  734. ctx.actionDescribedNotAllowedHTML = `<div></div>${ctx.actionDescribedHTML}`
  735. ctx.opts = {
  736. to: ctx.email,
  737. actionDescribed: ctx.actionDescribedNotAllowedHTML,
  738. action: 'an action',
  739. message: [ctx.messageNotAllowedHTML],
  740. }
  741. ctx.email = ctx.EmailBuilder.buildEmail('securityAlert', ctx.opts)
  742. })
  743. it('should build the email', function (ctx) {
  744. expect(ctx.email.html != null).to.equal(true)
  745. expect(ctx.email.text != null).to.equal(true)
  746. })
  747. describe('HTML email', function () {
  748. it('should clean HTML in opts.actionDescribed', function (ctx) {
  749. expect(ctx.email.html).to.not.contain(
  750. ctx.actionDescribedNotAllowedHTML
  751. )
  752. expect(ctx.email.html).to.contain(ctx.actionDescribedHTML)
  753. })
  754. it('should clean HTML in opts.message', function (ctx) {
  755. expect(ctx.email.html).to.not.contain(ctx.messageNotAllowedHTML)
  756. expect(ctx.email.html).to.contain(ctx.messageHTML)
  757. })
  758. })
  759. describe('plain text email', function () {
  760. it('should remove all HTML in opts.actionDescribed', function (ctx) {
  761. expect(ctx.email.text).to.not.contain(ctx.actionDescribedHTML)
  762. expect(ctx.email.text).to.contain(ctx.actionDescribed)
  763. })
  764. it('should remove all HTML in opts.message', function (ctx) {
  765. expect(ctx.email.text).to.not.contain(ctx.messageHTML)
  766. expect(ctx.email.text).to.contain(ctx.message)
  767. })
  768. })
  769. })
  770. describe('welcomeWithoutCTA', function () {
  771. beforeEach(function (ctx) {
  772. ctx.emailAddress = 'example@overleaf.com'
  773. ctx.opts = {
  774. to: ctx.emailAddress,
  775. }
  776. ctx.email = ctx.EmailBuilder.buildEmail('welcomeWithoutCTA', ctx.opts)
  777. ctx.dom = cheerio.load(ctx.email.html)
  778. })
  779. it('should build the email', function (ctx) {
  780. expect(ctx.email.html).to.exist
  781. expect(ctx.email.text).to.exist
  782. })
  783. describe('HTML email', function () {
  784. it('should include help links', function (ctx) {
  785. const helpGuidesLink = ctx.dom('a:contains("Help Guides")')
  786. const templatesLink = ctx.dom('a:contains("Templates")')
  787. const logInLink = ctx.dom('a:contains("log in")')
  788. expect(helpGuidesLink.length).to.equal(1)
  789. expect(templatesLink.length).to.equal(1)
  790. expect(logInLink.length).to.equal(1)
  791. })
  792. })
  793. describe('plain text email', function () {
  794. it('should include help URL', function (ctx) {
  795. expect(ctx.email.text).to.contain('/learn')
  796. expect(ctx.email.text).to.contain('/login')
  797. expect(ctx.email.text).to.contain('/templates')
  798. })
  799. it('should contain HTML links', function (ctx) {
  800. expect(ctx.email.text).to.not.contain('<a')
  801. })
  802. })
  803. })
  804. describe('removeGroupMember', function () {
  805. beforeEach(function (ctx) {
  806. ctx.passwordResetUrl = `${ctx.settings.siteUrl}/user/password/reset`
  807. ctx.emailAddress = 'example@overleaf.com'
  808. ctx.opts = {
  809. to: ctx.emailAddress,
  810. adminName: 'abcdef',
  811. }
  812. ctx.email = ctx.EmailBuilder.buildEmail('removeGroupMember', ctx.opts)
  813. ctx.dom = cheerio.load(ctx.email.html)
  814. })
  815. it('should build the email', function (ctx) {
  816. expect(ctx.email.html).to.exist
  817. expect(ctx.email.text).to.exist
  818. })
  819. describe('HTML email', function () {
  820. it('should include links', function (ctx) {
  821. const resetPasswordLink = ctx.dom('a:contains("set a password")')
  822. expect(resetPasswordLink.length).to.equal(1)
  823. expect(resetPasswordLink.attr('href')).to.equal(
  824. ctx.passwordResetUrl
  825. )
  826. })
  827. })
  828. describe('plain text email', function () {
  829. it('should include URLs', function (ctx) {
  830. expect(ctx.email.text).to.contain(ctx.passwordResetUrl)
  831. })
  832. })
  833. })
  834. describe('taxExemptCertificateRequired', function () {
  835. beforeEach(function (ctx) {
  836. ctx.emailAddress = 'customer@example.com'
  837. ctx.opts = {
  838. to: ctx.emailAddress,
  839. ein: '12-3456789',
  840. stripeCustomerId: 'cus_123456789',
  841. }
  842. ctx.email = ctx.EmailBuilder.buildEmail(
  843. 'taxExemptCertificateRequired',
  844. ctx.opts
  845. )
  846. ctx.dom = cheerio.load(ctx.email.html)
  847. })
  848. it('should build the email', function (ctx) {
  849. expect(ctx.email.html).to.exist
  850. expect(ctx.email.text).to.exist
  851. })
  852. describe('HTML email', function () {
  853. it('should include the EIN', function (ctx) {
  854. expect(ctx.email.html).to.contain(ctx.opts.ein)
  855. })
  856. it('should include the Stripe customer ID', function (ctx) {
  857. expect(ctx.email.html).to.contain(ctx.opts.stripeCustomerId)
  858. })
  859. it('should include tax exemption verification text', function (ctx) {
  860. expect(ctx.email.html).to.contain('tax exempt')
  861. expect(ctx.email.html).to.contain('verification')
  862. })
  863. })
  864. describe('plain text email', function () {
  865. it('should include the EIN', function (ctx) {
  866. expect(ctx.email.text).to.contain(ctx.opts.ein)
  867. })
  868. it('should include the Stripe customer ID', function (ctx) {
  869. expect(ctx.email.text).to.contain(ctx.opts.stripeCustomerId)
  870. })
  871. it('should include tax exemption verification text', function (ctx) {
  872. expect(ctx.email.text).to.contain('tax exempt')
  873. expect(ctx.email.text).to.contain('verification')
  874. })
  875. })
  876. })
  877. describe('groupMemberLimitWarning', function () {
  878. beforeEach(function (ctx) {
  879. ctx.emailAddress = 'example@overleaf.com'
  880. ctx.opts = {
  881. to: ctx.emailAddress,
  882. groupName: 'Example Group',
  883. firstName: 'Joe',
  884. currentMembers: 9,
  885. membersLimit: 10,
  886. remainingSeats: 1,
  887. }
  888. ctx.email = ctx.EmailBuilder.buildEmail(
  889. 'groupMemberLimitWarning',
  890. ctx.opts
  891. )
  892. ctx.expectedUrl = `${ctx.settings.siteUrl}/user/subscription/group/add-users`
  893. })
  894. it('should build the email', function (ctx) {
  895. expect(ctx.email.html).to.exist
  896. expect(ctx.email.text).to.exist
  897. })
  898. describe('HTML email', function () {
  899. it('should include a CTA button and a fallback CTA link', function (ctx) {
  900. const dom = cheerio.load(ctx.email.html)
  901. const plainText = dom.text()
  902. expect(ctx.email.subject).to.equal(
  903. `${ctx.opts.groupName} is approaching its member limit`
  904. )
  905. expect(ctx.email.html).to.exist
  906. expect(ctx.email.html).to.contain(
  907. `Your group "${ctx.opts.groupName}" is approaching its member limit.`
  908. )
  909. expect(plainText).to.contain(
  910. `Current usage: ${ctx.opts.currentMembers} of ` +
  911. `${ctx.opts.membersLimit} licenses used ` +
  912. `(${ctx.opts.remainingSeats} remaining)`
  913. )
  914. expect(ctx.email.html).to.contain(
  915. 'With domain capture enabled, users with verified email ' +
  916. 'addresses from your domain can automatically join the group ' +
  917. 'via SSO. Once the member limit is reached, new users will ' +
  918. 'be blocked from joining.'
  919. )
  920. expect(ctx.email.html).to.contain(
  921. 'To ensure uninterrupted access for your users, consider ' +
  922. 'adding more licenses or removing inactive members.'
  923. )
  924. const buttonLink = dom('td a')
  925. expect(buttonLink).to.exist
  926. expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl)
  927. const fallback = dom('.force-overleaf-style').last()
  928. expect(fallback).to.exist
  929. const fallbackLink = fallback.html().replace(/&amp;/g, '&')
  930. expect(fallbackLink).to.contain(ctx.expectedUrl)
  931. })
  932. })
  933. describe('plain text email', function () {
  934. it('should contain the CTA link', function (ctx) {
  935. expect(ctx.email.text).to.contain(ctx.expectedUrl)
  936. })
  937. })
  938. })
  939. })
  940. })
  941. })