EmailBuilderTests.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. const SandboxedModule = require('sandboxed-module')
  2. const cheerio = require('cheerio')
  3. const path = require('path')
  4. const { expect } = require('chai')
  5. const MODULE_PATH = path.join(
  6. __dirname,
  7. '../../../../app/src/Features/Email/EmailBuilder'
  8. )
  9. const EmailMessageHelper = require('../../../../app/src/Features/Email/EmailMessageHelper')
  10. const ctaEmailBody = require('../../../../app/src/Features/Email/Bodies/cta-email')
  11. const NoCTAEmailBody = require('../../../../app/src/Features/Email/Bodies/NoCTAEmailBody')
  12. const BaseWithHeaderEmailLayout = require('../../../../app/src/Features/Email/Layouts/BaseWithHeaderEmailLayout')
  13. describe('EmailBuilder', function() {
  14. before(function() {
  15. this.settings = {
  16. appName: 'testApp',
  17. siteUrl: 'https://www.overleaf.com'
  18. }
  19. this.EmailBuilder = SandboxedModule.require(MODULE_PATH, {
  20. requires: {
  21. './EmailMessageHelper': EmailMessageHelper,
  22. './Bodies/cta-email': ctaEmailBody,
  23. './Bodies/NoCTAEmailBody': NoCTAEmailBody,
  24. './Layouts/BaseWithHeaderEmailLayout': BaseWithHeaderEmailLayout,
  25. 'settings-sharelatex': this.settings
  26. }
  27. })
  28. })
  29. describe('projectInvite', function() {
  30. beforeEach(function() {
  31. this.opts = {
  32. to: 'bob@bob.com',
  33. first_name: 'bob',
  34. owner: {
  35. email: 'sally@hally.com'
  36. },
  37. inviteUrl: 'http://example.com/invite',
  38. project: {
  39. url: 'http://www.project.com',
  40. name: 'standard project'
  41. }
  42. }
  43. })
  44. describe('when sending a normal email', function() {
  45. beforeEach(function() {
  46. this.email = this.EmailBuilder.buildEmail('projectInvite', this.opts)
  47. })
  48. it('should have html and text properties', function() {
  49. expect(this.email.html != null).to.equal(true)
  50. expect(this.email.text != null).to.equal(true)
  51. })
  52. it('should not have undefined in it', function() {
  53. this.email.html.indexOf('undefined').should.equal(-1)
  54. this.email.subject.indexOf('undefined').should.equal(-1)
  55. })
  56. })
  57. describe('when someone is up to no good', function() {
  58. beforeEach(function() {
  59. this.opts.project.name = "<img src='http://evilsite.com/evil.php'>"
  60. this.email = this.EmailBuilder.buildEmail('projectInvite', this.opts)
  61. })
  62. it('should not contain unescaped html in the html part', function() {
  63. expect(this.email.html).to.contain('New Project')
  64. })
  65. it('should not have undefined in it', function() {
  66. this.email.html.indexOf('undefined').should.equal(-1)
  67. this.email.subject.indexOf('undefined').should.equal(-1)
  68. })
  69. })
  70. })
  71. describe('SpamSafe', function() {
  72. beforeEach(function() {
  73. this.opts = {
  74. to: 'bob@joe.com',
  75. first_name: 'bob',
  76. owner: {
  77. email: 'sally@hally.com'
  78. },
  79. inviteUrl: 'http://example.com/invite',
  80. project: {
  81. url: 'http://www.project.com',
  82. name: 'come buy my product at http://notascam.com'
  83. }
  84. }
  85. this.email = this.EmailBuilder.buildEmail('projectInvite', this.opts)
  86. })
  87. it('should replace spammy project name', function() {
  88. this.email.html.indexOf('a new project').should.not.equal(-1)
  89. this.email.subject.indexOf('New Project').should.not.equal(-1)
  90. })
  91. })
  92. describe('ctaTemplate', function() {
  93. describe('missing required content', function() {
  94. const content = {
  95. title: () => {},
  96. greeting: () => {},
  97. message: () => {},
  98. secondaryMessage: () => {},
  99. ctaText: () => {},
  100. ctaURL: () => {},
  101. gmailGoToAction: () => {}
  102. }
  103. it('should throw an error when missing title', function() {
  104. let { title, ...missing } = content
  105. expect(() => {
  106. this.EmailBuilder.ctaTemplate(missing)
  107. }).to.throw(Error)
  108. })
  109. it('should throw an error when missing message', function() {
  110. let { message, ...missing } = content
  111. expect(() => {
  112. this.EmailBuilder.ctaTemplate(missing)
  113. }).to.throw(Error)
  114. })
  115. it('should throw an error when missing ctaText', function() {
  116. let { ctaText, ...missing } = content
  117. expect(() => {
  118. this.EmailBuilder.ctaTemplate(missing)
  119. }).to.throw(Error)
  120. })
  121. it('should throw an error when missing ctaURL', function() {
  122. let { ctaURL, ...missing } = content
  123. expect(() => {
  124. this.EmailBuilder.ctaTemplate(missing)
  125. }).to.throw(Error)
  126. })
  127. })
  128. })
  129. describe('templates', function() {
  130. describe('CTA', function() {
  131. describe('canceledSubscription', function() {
  132. beforeEach(function() {
  133. this.emailAddress = 'example@overleaf.com'
  134. this.opts = {
  135. to: this.emailAddress
  136. }
  137. this.email = this.EmailBuilder.buildEmail(
  138. 'canceledSubscription',
  139. this.opts
  140. )
  141. this.expectedUrl =
  142. 'https://docs.google.com/forms/d/e/1FAIpQLSfa7z_s-cucRRXm70N4jEcSbFsZeb0yuKThHGQL8ySEaQzF0Q/viewform?usp=sf_link'
  143. })
  144. it('should build the email', function() {
  145. expect(this.email.html).to.exist
  146. expect(this.email.text).to.exist
  147. })
  148. describe('HTML email', function() {
  149. it('should include a CTA button and a fallback CTA link', function() {
  150. const dom = cheerio.load(this.email.html)
  151. const buttonLink = dom('a:contains("Leave Feedback")')
  152. expect(buttonLink.length).to.equal(1)
  153. expect(buttonLink.attr('href')).to.equal(this.expectedUrl)
  154. const fallback = dom('.force-overleaf-style').last()
  155. expect(fallback.length).to.equal(1)
  156. const fallbackLink = fallback.html()
  157. expect(fallbackLink).to.contain(this.expectedUrl)
  158. })
  159. })
  160. describe('plain text email', function() {
  161. it('should contain the CTA link', function() {
  162. expect(this.email.text).to.contain(this.expectedUrl)
  163. })
  164. })
  165. })
  166. describe('confirmEmail', function() {
  167. before(function() {
  168. this.emailAddress = 'example@overleaf.com'
  169. this.userId = 'abc123'
  170. this.opts = {
  171. to: this.emailAddress,
  172. confirmEmailUrl: `${this.settings.siteUrl}/user/emails/confirm?token=aToken123`,
  173. sendingUser_id: this.userId
  174. }
  175. this.email = this.EmailBuilder.buildEmail('confirmEmail', this.opts)
  176. })
  177. it('should build the email', function() {
  178. expect(this.email.html).to.exist
  179. expect(this.email.text).to.exist
  180. })
  181. describe('HTML email', function() {
  182. it('should include a CTA button and a fallback CTA link', function() {
  183. const dom = cheerio.load(this.email.html)
  184. const buttonLink = dom('a:contains("Confirm Email")')
  185. expect(buttonLink.length).to.equal(1)
  186. expect(buttonLink.attr('href')).to.equal(this.opts.confirmEmailUrl)
  187. const fallback = dom('.force-overleaf-style').last()
  188. expect(fallback.length).to.equal(1)
  189. const fallbackLink = fallback.html()
  190. expect(fallbackLink).to.contain(this.opts.confirmEmailUrl)
  191. })
  192. })
  193. describe('plain text email', function() {
  194. it('should contain the CTA link', function() {
  195. expect(this.email.text).to.contain(this.opts.confirmEmailUrl)
  196. })
  197. })
  198. })
  199. describe('ownershipTransferConfirmationNewOwner', function() {
  200. before(function() {
  201. this.emailAddress = 'example@overleaf.com'
  202. this.opts = {
  203. to: this.emailAddress,
  204. previousOwner: {},
  205. project: {
  206. _id: 'abc123',
  207. name: 'example project'
  208. }
  209. }
  210. this.email = this.EmailBuilder.buildEmail(
  211. 'ownershipTransferConfirmationNewOwner',
  212. this.opts
  213. )
  214. this.expectedUrl = `${
  215. this.settings.siteUrl
  216. }/project/${this.opts.project._id.toString()}`
  217. })
  218. it('should build the email', function() {
  219. expect(this.email.html).to.exist
  220. expect(this.email.text).to.exist
  221. })
  222. describe('HTML email', function() {
  223. it('should include a CTA button and a fallback CTA link', function() {
  224. const dom = cheerio.load(this.email.html)
  225. const buttonLink = dom('td a')
  226. expect(buttonLink).to.exist
  227. expect(buttonLink.attr('href')).to.equal(this.expectedUrl)
  228. const fallback = dom('.force-overleaf-style').last()
  229. expect(fallback).to.exist
  230. const fallbackLink = fallback.html().replace(/&amp;/g, '&')
  231. expect(fallbackLink).to.contain(this.expectedUrl)
  232. })
  233. })
  234. describe('plain text email', function() {
  235. it('should contain the CTA link', function() {
  236. expect(this.email.text).to.contain(this.expectedUrl)
  237. })
  238. })
  239. })
  240. describe('passwordResetRequested', function() {
  241. before(function() {
  242. this.emailAddress = 'example@overleaf.com'
  243. this.opts = {
  244. to: this.emailAddress,
  245. setNewPasswordUrl: `${
  246. this.settings.siteUrl
  247. }/user/password/set?passwordResetToken=aToken&email=${encodeURIComponent(
  248. this.emailAddress
  249. )}`
  250. }
  251. this.email = this.EmailBuilder.buildEmail(
  252. 'passwordResetRequested',
  253. this.opts
  254. )
  255. })
  256. it('should build the email', function() {
  257. expect(this.email.html).to.exist
  258. expect(this.email.text).to.exist
  259. })
  260. describe('HTML email', function() {
  261. it('should include a CTA button and a fallback CTA link', function() {
  262. const dom = cheerio.load(this.email.html)
  263. const buttonLink = dom('td a')
  264. expect(buttonLink).to.exist
  265. expect(buttonLink.attr('href')).to.equal(
  266. this.opts.setNewPasswordUrl
  267. )
  268. const fallback = dom('.force-overleaf-style').last()
  269. expect(fallback).to.exist
  270. const fallbackLink = fallback.html().replace(/&amp;/g, '&')
  271. expect(fallbackLink).to.contain(this.opts.setNewPasswordUrl)
  272. })
  273. })
  274. describe('plain text email', function() {
  275. it('should contain the CTA link', function() {
  276. expect(this.email.text).to.contain(this.opts.setNewPasswordUrl)
  277. })
  278. })
  279. })
  280. describe('verifyEmailToJoinTeam', function() {
  281. before(function() {
  282. this.emailAddress = 'example@overleaf.com'
  283. this.opts = {
  284. to: this.emailAddress,
  285. acceptInviteUrl: `${this.settings.siteUrl}/subscription/invites/aToken123/`,
  286. inviter: {
  287. email: 'deanna@overleaf.com',
  288. first_name: 'Deanna',
  289. last_name: 'Troi'
  290. }
  291. }
  292. this.email = this.EmailBuilder.buildEmail(
  293. 'verifyEmailToJoinTeam',
  294. this.opts
  295. )
  296. })
  297. it('should build the email', function() {
  298. expect(this.email.html).to.exist
  299. expect(this.email.text).to.exist
  300. })
  301. describe('HTML email', function() {
  302. it('should include a CTA button and a fallback CTA link', function() {
  303. const dom = cheerio.load(this.email.html)
  304. const buttonLink = dom('a:contains("Join now")')
  305. expect(buttonLink.length).to.equal(1)
  306. expect(buttonLink.attr('href')).to.equal(this.opts.acceptInviteUrl)
  307. const fallback = dom('.force-overleaf-style').last()
  308. expect(fallback.length).to.equal(1)
  309. const fallbackLink = fallback.html()
  310. expect(fallbackLink).to.contain(this.opts.acceptInviteUrl)
  311. })
  312. })
  313. describe('plain text email', function() {
  314. it('should contain the CTA link', function() {
  315. expect(this.email.text).to.contain(this.opts.acceptInviteUrl)
  316. })
  317. })
  318. })
  319. describe('reactivatedSubscription', function() {
  320. before(function() {
  321. this.emailAddress = 'example@overleaf.com'
  322. this.opts = {
  323. to: this.emailAddress
  324. }
  325. this.email = this.EmailBuilder.buildEmail(
  326. 'reactivatedSubscription',
  327. this.opts
  328. )
  329. this.expectedUrl = `${this.settings.siteUrl}/user/subscription`
  330. })
  331. it('should build the email', function() {
  332. expect(this.email.html).to.exist
  333. expect(this.email.text).to.exist
  334. })
  335. describe('HTML email', function() {
  336. it('should include a CTA button and a fallback CTA link', function() {
  337. const dom = cheerio.load(this.email.html)
  338. const buttonLink = dom('a:contains("View Subscription Dashboard")')
  339. expect(buttonLink.length).to.equal(1)
  340. expect(buttonLink.attr('href')).to.equal(this.expectedUrl)
  341. const fallback = dom('.force-overleaf-style').last()
  342. expect(fallback.length).to.equal(1)
  343. const fallbackLink = fallback.html()
  344. expect(fallbackLink).to.contain(this.expectedUrl)
  345. })
  346. })
  347. describe('plain text email', function() {
  348. it('should contain the CTA link', function() {
  349. expect(this.email.text).to.contain(this.expectedUrl)
  350. })
  351. })
  352. })
  353. describe('testEmail', function() {
  354. before(function() {
  355. this.emailAddress = 'example@overleaf.com'
  356. this.opts = {
  357. to: this.emailAddress
  358. }
  359. this.email = this.EmailBuilder.buildEmail('testEmail', this.opts)
  360. })
  361. it('should build the email', function() {
  362. expect(this.email.html).to.exist
  363. expect(this.email.text).to.exist
  364. })
  365. describe('HTML email', function() {
  366. it('should include a CTA button and a fallback CTA link', function() {
  367. const dom = cheerio.load(this.email.html)
  368. const buttonLink = dom(
  369. `a:contains("Open ${this.settings.appName}")`
  370. )
  371. expect(buttonLink.length).to.equal(1)
  372. expect(buttonLink.attr('href')).to.equal(this.settings.siteUrl)
  373. const fallback = dom('.force-overleaf-style').last()
  374. expect(fallback.length).to.equal(1)
  375. const fallbackLink = fallback.html()
  376. expect(fallbackLink).to.contain(this.settings.siteUrl)
  377. })
  378. })
  379. describe('plain text email', function() {
  380. it('should contain the CTA link', function() {
  381. expect(this.email.text).to.contain(
  382. `Open ${this.settings.appName}: ${this.settings.siteUrl}`
  383. )
  384. })
  385. })
  386. })
  387. describe('registered', function() {
  388. before(function() {
  389. this.emailAddress = 'example@overleaf.com'
  390. this.opts = {
  391. to: this.emailAddress,
  392. setNewPasswordUrl: `${this.settings.siteUrl}/user/activate?token=aToken123&user_id=aUserId123`
  393. }
  394. this.email = this.EmailBuilder.buildEmail('registered', this.opts)
  395. })
  396. it('should build the email', function() {
  397. expect(this.email.html).to.exist
  398. expect(this.email.text).to.exist
  399. })
  400. describe('HTML email', function() {
  401. it('should include a CTA button and a fallback CTA link', function() {
  402. const dom = cheerio.load(this.email.html)
  403. const buttonLink = dom('a:contains("Set password")')
  404. expect(buttonLink.length).to.equal(1)
  405. expect(buttonLink.attr('href')).to.equal(
  406. this.opts.setNewPasswordUrl
  407. )
  408. const fallback = dom('.force-overleaf-style').last()
  409. expect(fallback.length).to.equal(1)
  410. const fallbackLink = fallback.html().replace(/&amp;/, '&')
  411. expect(fallbackLink).to.contain(this.opts.setNewPasswordUrl)
  412. })
  413. })
  414. describe('plain text email', function() {
  415. it('should contain the CTA link', function() {
  416. expect(this.email.text).to.contain(this.opts.setNewPasswordUrl)
  417. })
  418. })
  419. })
  420. describe('projectInvite', function() {
  421. before(function() {
  422. this.emailAddress = 'example@overleaf.com'
  423. this.owner = {
  424. email: 'owner@example.com',
  425. name: 'Bailey'
  426. }
  427. this.projectName = 'Top Secret'
  428. this.opts = {
  429. inviteUrl:
  430. `${this.settings.siteUrl}/project/projectId123/invite/token/aToken123?` +
  431. [
  432. `project_name=${encodeURIComponent(this.projectName)}`,
  433. `user_first_name=${encodeURIComponent(this.owner.name)}`
  434. ].join('&'),
  435. owner: {
  436. email: this.owner.email
  437. },
  438. project: {
  439. name: this.projectName
  440. },
  441. to: this.emailAddress
  442. }
  443. this.email = this.EmailBuilder.buildEmail('projectInvite', this.opts)
  444. })
  445. it('should build the email', function() {
  446. expect(this.email.html).to.exist
  447. expect(this.email.text).to.exist
  448. })
  449. describe('HTML email', function() {
  450. it('should include a CTA button and a fallback CTA link', function() {
  451. const dom = cheerio.load(this.email.html)
  452. const buttonLink = dom('a:contains("View project")')
  453. expect(buttonLink.length).to.equal(1)
  454. expect(buttonLink.attr('href')).to.equal(this.opts.inviteUrl)
  455. const fallback = dom('.force-overleaf-style').last()
  456. expect(fallback.length).to.equal(1)
  457. const fallbackLink = fallback.html().replace(/&amp;/g, '&')
  458. expect(fallbackLink).to.contain(this.opts.inviteUrl)
  459. })
  460. })
  461. describe('plain text email', function() {
  462. it('should contain the CTA link', function() {
  463. expect(this.email.text).to.contain(this.opts.inviteUrl)
  464. })
  465. })
  466. })
  467. })
  468. describe('no CTA', function() {
  469. describe('securityAlert', function() {
  470. before(function() {
  471. this.message = 'more details about the action'
  472. this.messageHTML = `<br /><span style="text-align:center" class="a-class"><b><i>${this.message}</i></b></span>`
  473. this.messageNotAllowedHTML = `<div></div>${this.messageHTML}`
  474. this.actionDescribed = 'an action described'
  475. this.actionDescribedHTML = `<br /><span style="text-align:center" class="a-class"><b><i>${this.actionDescribed}</i></b>`
  476. this.actionDescribedNotAllowedHTML = `<div></div>${this.actionDescribedHTML}`
  477. this.opts = {
  478. to: this.email,
  479. actionDescribed: this.actionDescribedNotAllowedHTML,
  480. action: 'an action',
  481. message: [this.messageNotAllowedHTML]
  482. }
  483. this.email = this.EmailBuilder.buildEmail('securityAlert', this.opts)
  484. })
  485. it('should build the email', function() {
  486. expect(this.email.html != null).to.equal(true)
  487. expect(this.email.text != null).to.equal(true)
  488. })
  489. describe('HTML email', function() {
  490. it('should clean HTML in opts.actionDescribed', function() {
  491. expect(this.email.html).to.not.contain(
  492. this.actionDescribedNotAllowedHTML
  493. )
  494. expect(this.email.html).to.contain(this.actionDescribedHTML)
  495. })
  496. it('should clean HTML in opts.message', function() {
  497. expect(this.email.html).to.not.contain(this.messageNotAllowedHTML)
  498. expect(this.email.html).to.contain(this.messageHTML)
  499. })
  500. })
  501. describe('plain text email', function() {
  502. it('should remove all HTML in opts.actionDescribed', function() {
  503. expect(this.email.text).to.not.contain(this.actionDescribedHTML)
  504. expect(this.email.text).to.contain(this.actionDescribed)
  505. })
  506. it('should remove all HTML in opts.message', function() {
  507. expect(this.email.text).to.not.contain(this.messageHTML)
  508. expect(this.email.text).to.contain(this.message)
  509. })
  510. })
  511. })
  512. })
  513. })
  514. })