RemoveUnconfirmedEmailsScriptTests.mjs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. import { promisify } from 'node:util'
  2. import { exec } from 'node:child_process'
  3. import { expect } from 'chai'
  4. import { filterOutput } from './helpers/settings.mjs'
  5. import { db, ObjectId } from '../../../app/src/infrastructure/mongodb.js'
  6. import fs from 'node:fs/promises'
  7. const CSV_FILENAME = '/tmp/remove_unconfirmed_emails.csv'
  8. async function runScript(mode, commit) {
  9. const result = await promisify(exec)(
  10. [
  11. 'node',
  12. 'scripts/remove_unconfirmed_emails.mjs',
  13. mode === 'generate' ? '--generate' : '--consume',
  14. commit && '--commit',
  15. ]
  16. .filter(Boolean)
  17. .join(' ')
  18. )
  19. return {
  20. ...result,
  21. stdout: result.stdout.split('\n').filter(filterOutput),
  22. }
  23. }
  24. function createUser(signUpDate, emails, userIdx) {
  25. const email = `primary${userIdx ?? ''}@overleaf.com`
  26. return {
  27. _id: new ObjectId(),
  28. email,
  29. emails,
  30. signUpDate,
  31. }
  32. }
  33. describe('scripts/remove_unconfirmed_emails', function () {
  34. let user
  35. afterEach(async function () {
  36. try {
  37. await fs.unlink(CSV_FILENAME)
  38. } catch (err) {
  39. // Ignore errors if file doesn't exist
  40. }
  41. })
  42. describe('when removing unconfirmed secondary emails', function () {
  43. beforeEach(async function () {
  44. user = createUser(new Date('2000-01-01'), [
  45. { email: 'primary@overleaf.com', confirmedAt: new Date() },
  46. { email: 'unconfirmed1@overleaf.com' },
  47. { email: 'unconfirmed-special-,\'"@overleaf.com' },
  48. ])
  49. await db.users.insertOne(user)
  50. })
  51. it('should remove all unconfirmed secondary emails', async function () {
  52. await runScript('generate')
  53. const r = await runScript('consume', true)
  54. expect(r.stdout).to.include('Total emails in the CSV: 2')
  55. expect(r.stdout).to.include('Total users processed: 1')
  56. const updatedUser = await db.users.findOne({ _id: user._id })
  57. expect(updatedUser.emails).to.have.length(1)
  58. expect(updatedUser.emails[0].email).to.equal(user.email)
  59. })
  60. it('should not modify anything in dry run mode', async function () {
  61. await runScript('generate')
  62. const r = await runScript('consume', false)
  63. expect(r.stdout).to.include('Total emails in the CSV: 2')
  64. expect(r.stdout).to.include('Total users processed: 1')
  65. expect(r.stdout).to.include(
  66. 'Note: this was a dry-run. No changes were made.'
  67. )
  68. const updatedUser = await db.users.findOne({ _id: user._id })
  69. expect(updatedUser.emails).to.have.length(3)
  70. })
  71. })
  72. describe('when handling confirmed secondary emails', function () {
  73. beforeEach(async function () {
  74. user = createUser(new Date('2000-01-01'), [
  75. { email: 'primary@overleaf.com', confirmedAt: new Date() },
  76. { email: 'confirmed@overleaf.com', confirmedAt: new Date() },
  77. ])
  78. await db.users.insertOne(user)
  79. })
  80. it('should preserve confirmed secondary emails', async function () {
  81. await runScript('generate')
  82. const r = await runScript('consume', true)
  83. expect(r.stdout).to.include('Total emails in the CSV: 0')
  84. expect(r.stdout).to.include('Total users processed: 0')
  85. const updatedUser = await db.users.findOne({ _id: user._id })
  86. expect(updatedUser.emails).to.have.length(2)
  87. expect(updatedUser.emails[1].confirmedAt).to.exist
  88. })
  89. })
  90. describe('when handling unconfirmed primary emails', function () {
  91. beforeEach(async function () {
  92. user = createUser(new Date('2000-01-01'), [
  93. { email: 'primary@overleaf.com' },
  94. ])
  95. await db.users.insertOne(user)
  96. })
  97. it('should not remove unconfirmed primary emails', async function () {
  98. await runScript('generate')
  99. const r = await runScript('consume', true)
  100. expect(r.stdout).to.include('Total emails in the CSV: 0')
  101. expect(r.stdout).to.include('Total users processed: 0')
  102. const updatedUser = await db.users.findOne({ _id: user._id })
  103. expect(updatedUser.emails).to.have.length(1)
  104. expect(updatedUser.emails[0].email).to.equal('primary@overleaf.com')
  105. })
  106. })
  107. describe('when users confirmed their email in between', function () {
  108. beforeEach(async function () {
  109. user = createUser(new Date('2000-01-01'), [
  110. { email: 'primary@overleaf.com' },
  111. { email: 'secondary@overleaf.com' },
  112. ])
  113. await db.users.insertOne(user)
  114. })
  115. it('should not remove emails from users who confirmed their email in between', async function () {
  116. await runScript('generate')
  117. await db.users.updateOne(
  118. { _id: user._id },
  119. { $set: { 'emails.1.confirmedAt': new Date() } }
  120. )
  121. const r = await runScript('consume', true)
  122. expect(r.stdout).to.include('Total emails in the CSV: 1')
  123. expect(r.stdout).to.include('Skipped emails: 1')
  124. expect(r.stdout).to.include(' - Email now confirmed: 1')
  125. const updatedUser = await db.users.findOne({ _id: user._id })
  126. expect(updatedUser.emails).to.have.length(2)
  127. })
  128. })
  129. describe('when users changed their primary email in between', function () {
  130. beforeEach(async function () {
  131. user = createUser(new Date('2000-01-01'), [
  132. { email: 'primary@overleaf.com' },
  133. { email: 'secondary@overleaf.com' },
  134. ])
  135. await db.users.insertOne(user)
  136. })
  137. it('should not remove emails from users who changed their primary email in between', async function () {
  138. await runScript('generate')
  139. await db.users.updateOne(
  140. { _id: user._id },
  141. { $set: { email: 'secondary@overleaf.com' } }
  142. )
  143. const r = await runScript('consume', true)
  144. expect(r.stdout).to.include('Total emails in the CSV: 1')
  145. expect(r.stdout).to.include('Skipped emails: 1')
  146. expect(r.stdout).to.include(' - Email now primary: 1')
  147. const updatedUser = await db.users.findOne({ _id: user._id })
  148. expect(updatedUser.emails).to.have.length(2)
  149. })
  150. })
  151. describe('when users account was deleted in between', function () {
  152. beforeEach(async function () {
  153. user = createUser(new Date('2000-01-01'), [
  154. { email: 'primary@overleaf.com' },
  155. { email: 'secondary@overleaf.com' },
  156. ])
  157. await db.users.insertOne(user)
  158. })
  159. it('should skip emails from users whose account was deleted', async function () {
  160. await runScript('generate')
  161. // Delete the user
  162. await db.users.deleteOne({ _id: user._id })
  163. const r = await runScript('consume', true)
  164. expect(r.stdout).to.include('Total emails in the CSV: 1')
  165. expect(r.stdout).to.include('Skipped emails: 1')
  166. expect(r.stdout).to.include(' - User not found: 1')
  167. })
  168. })
  169. describe('when users email was deleted in between', function () {
  170. beforeEach(async function () {
  171. user = createUser(new Date('2000-01-01'), [
  172. { email: 'primary@overleaf.com' },
  173. { email: 'secondary@overleaf.com' },
  174. ])
  175. await db.users.insertOne(user)
  176. })
  177. it('should skip emails that were already removed', async function () {
  178. await runScript('generate')
  179. // Remove the secondary email
  180. await db.users.updateOne(
  181. { _id: user._id },
  182. { $pull: { emails: { email: 'secondary@overleaf.com' } } }
  183. )
  184. const r = await runScript('consume', true)
  185. expect(r.stdout).to.include('Total emails in the CSV: 1')
  186. expect(r.stdout).to.include('Skipped emails: 1')
  187. expect(r.stdout).to.include(' - Email now removed: 1')
  188. const updatedUser = await db.users.findOne({ _id: user._id })
  189. expect(updatedUser.emails).to.have.length(1)
  190. expect(updatedUser.emails[0].email).to.equal('primary@overleaf.com')
  191. })
  192. })
  193. describe('when handling confirmation field edge cases', function () {
  194. beforeEach(async function () {
  195. user = createUser(new Date('2000-01-01'), [
  196. { email: 'primary@overleaf.com', confirmedAt: new Date() },
  197. { email: 'secondary1@overleaf.com', confirmedAt: null },
  198. { email: 'secondary2@overleaf.com' },
  199. ])
  200. await db.users.insertOne(user)
  201. })
  202. it('should remove emails with both missing and null confirmedAt', async function () {
  203. await runScript('generate')
  204. const r = await runScript('consume', true)
  205. expect(r.stdout).to.include('Total emails in the CSV: 2')
  206. expect(r.stdout).to.include('Total users processed: 1')
  207. const updatedUser = await db.users.findOne({ _id: user._id })
  208. expect(updatedUser.emails).to.have.length(1)
  209. expect(updatedUser.emails[0].email).to.equal(user.email)
  210. })
  211. })
  212. describe('CSV file generation', function () {
  213. beforeEach(async function () {
  214. user = createUser(new Date('2000-01-01'), [
  215. { email: 'primary@overleaf.com', confirmedAt: new Date() },
  216. { email: 'unconfirmed1@overleaf.com' },
  217. { email: 'confirmed1@overleaf.com', confirmedAt: new Date() },
  218. { email: 'unconfirmed2@overleaf.com' },
  219. { email: '!,@overleaf.com' },
  220. { email: "!'@overleaf.com" },
  221. { email: '!,\'"@overleaf.com' },
  222. ])
  223. await db.users.insertOne(user)
  224. })
  225. it('should generate a valid CSV file', async function () {
  226. const r = await runScript('generate')
  227. expect(r.stdout).to.include(
  228. 'Generated CSV file: /tmp/remove_unconfirmed_emails.csv'
  229. )
  230. expect(r.stdout).to.include('Total emails in the CSV: 5')
  231. const csvContent = await fs.readFile(CSV_FILENAME, 'utf8')
  232. expect(csvContent).to.equal(`User ID,Email,Sign Up Date
  233. ${user._id},unconfirmed1@overleaf.com,2000-01-01T00:00:00.000Z
  234. ${user._id},unconfirmed2@overleaf.com,2000-01-01T00:00:00.000Z
  235. ${user._id},"!,@overleaf.com",2000-01-01T00:00:00.000Z
  236. ${user._id},!'@overleaf.com,2000-01-01T00:00:00.000Z
  237. ${user._id},"!,'""@overleaf.com",2000-01-01T00:00:00.000Z
  238. `)
  239. })
  240. })
  241. })