check_collection_for_batched_update_issue.mjs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. #!/usr/bin/env node
  2. /**
  3. * Checks collections for potential issues with batchedUpdate missing
  4. * entries at the start of the collection.
  5. */
  6. import assert from 'node:assert/strict'
  7. import { describe, it, before, after } from 'node:test'
  8. import { db, connectionPromise } from '../lib/mongodb.mjs'
  9. /**
  10. * Finds IDs where the natural order and _id sorted order differ.
  11. * This identifies records that might have been moved or updated in a way
  12. * that affected their storage position in MongoDB.
  13. */
  14. async function findDivergentRecords(coll) {
  15. const [firstNatural, firstSorted] = await Promise.all([
  16. db[coll].find().limit(1).next(),
  17. db[coll].find().sort({ _id: 1 }).limit(1).next(),
  18. ])
  19. if (!firstNatural || !firstSorted || firstNatural._id.equals(firstSorted._id))
  20. return []
  21. const [minId, maxId] =
  22. firstNatural._id.toString() < firstSorted._id.toString()
  23. ? [firstNatural._id, firstSorted._id]
  24. : [firstSorted._id, firstNatural._id]
  25. const found = await db[coll]
  26. .find({ _id: { $gte: minId, $lte: maxId } })
  27. .toArray()
  28. return found
  29. }
  30. describe('Batched update audit', async () => {
  31. let client
  32. before(async () => {
  33. client = await connectionPromise
  34. })
  35. after(async () => {
  36. if (client) await client.close()
  37. })
  38. describe('users collection', async () => {
  39. const records = await findDivergentRecords('users')
  40. if (records.length === 0) {
  41. it('no divergent records found', () => assert.ok(true))
  42. } else {
  43. for (const record of records) {
  44. describe(`Record: ${record._id}`, () => {
  45. // tools/migrations/20210726083523_convert_confirmedAt_strings_to_dates.mjs
  46. // any confirmedAt field must be a date
  47. it('confirmedAt is a Date object in email records', () => {
  48. record.emails?.forEach(emailObj => {
  49. if ('confirmedAt' in emailObj) {
  50. assert.ok(emailObj.confirmedAt instanceof Date)
  51. }
  52. })
  53. })
  54. // tools/migrations/20210726083523_convert_split_tests_assigned_at_strings_to_dates.mjs
  55. // each split test entry has a date in the assignedAt field if set
  56. it('splitTests entry assignedAt fields are Date objects', () => {
  57. // splitTests is stored as an object keyed by test name
  58. Object.values(record.splitTests || {}).forEach(test => {
  59. if (test.assignedAt) assert.ok(test.assignedAt instanceof Date)
  60. })
  61. })
  62. // tools/migrations/20220826104236_disable_alpha_beta_program.mjs
  63. // should be strictly false, but allow undefined for simplicity
  64. it('alpha/beta program are disabled', () => {
  65. assert.ok(
  66. record.alphaProgram === false || record.alphaProgram === undefined
  67. )
  68. assert.ok(
  69. record.betaProgram === false || record.betaProgram === undefined
  70. )
  71. })
  72. // tools/migrations/20220913125500_migrate_auditLog_to_collections.mjs
  73. it('obsolete auditLog field is not present', () => {
  74. assert.equal(record.auditLog, undefined)
  75. })
  76. // tools/migrations/20230124092607_clear_old_2fa_setup.mjs
  77. it('obsolete 2fa field is not present', () => {
  78. assert.equal(record.twoFactorAuthentication, undefined)
  79. })
  80. // tools/migrations/20240618125145_cleanup_user_features_templates.mjs
  81. // unsets features.templates
  82. it('features does not contain a templates field', () => {
  83. assert.equal(
  84. record.features?.templates,
  85. undefined,
  86. `features ${JSON.stringify(record.features)}`
  87. )
  88. })
  89. // tools/migrations/20240713110905_emails_reversed_hostname.mjs
  90. it('reversedHostname matches reversed email domain', () => {
  91. record.emails.forEach((emailObj, index) => {
  92. if (emailObj.email) {
  93. const expected = emailObj.email
  94. .split('@')[1]
  95. .split('')
  96. .reverse()
  97. .join('')
  98. assert.equal(
  99. emailObj.reversedHostname,
  100. expected,
  101. `Email ${index} mismatch`
  102. )
  103. }
  104. })
  105. })
  106. })
  107. }
  108. }
  109. })
  110. describe('subscriptions collection', async () => {
  111. const records = await findDivergentRecords('subscriptions')
  112. if (records.length === 0) {
  113. it('no divergent records found', () => assert.ok(true))
  114. } else {
  115. for (const record of records) {
  116. describe(`Record: ${record._id}`, () => {
  117. // tools/migrations/20230110140452_rename_recurly_cached_status.mjs
  118. // renames recurly to recurlyStatus
  119. it('recurly field does not exist', () =>
  120. assert.equal(record.recurly, undefined))
  121. // tools/migrations/20230207134844_group_invite_emails_to_lowercase.mjs
  122. // changes array entries to lowercase
  123. it('teamInvites emails are lowercase', () => {
  124. record.teamInvites?.forEach(invite => {
  125. if (invite.email)
  126. assert.equal(invite.email, invite.email.toLowerCase())
  127. })
  128. })
  129. // tools/migrations/20230928092537_backfill_subscriptions_managed_users_feature_flag.mjs
  130. // unsets features.managedUsers if $ne:true
  131. // Note this is superseded by the 20241111133330_remove_null_managed_users_sso_from_subscriptions.mjs below
  132. it('features.managedUsers is either true or null', () => {
  133. const managedUsers = record.features?.managedUsers
  134. assert.ok(
  135. managedUsers === true || managedUsers === null,
  136. 'features.managedUsers must be either true or null'
  137. )
  138. })
  139. // tools/migrations/20231030160030_managed_users_enabled.mjs
  140. // sets managedUsersEnabled to true when groupPolicy exists
  141. it('managedUsersEnabled is true if groupPolicy exists', () => {
  142. if ('groupPolicy' in record) {
  143. assert(record.managedUsersEnabled === true)
  144. }
  145. })
  146. // tools/migrations/20241111133330_remove_null_managed_users_sso_from_subscriptions.mjs
  147. it('features.managedUsers is true if set', () => {
  148. if (record.features && 'managedUsers' in record.features) {
  149. assert.strictEqual(
  150. record.features.managedUsers,
  151. true,
  152. 'features.managedUsers must be true when set'
  153. )
  154. }
  155. })
  156. // also in tools/migrations/20241111133330_remove_null_managed_users_sso_from_subscriptions.mjs
  157. it('features.groupSSO is true if set', () => {
  158. if (record.features && 'groupSSO' in record.features) {
  159. assert.strictEqual(
  160. record.features.groupSSO,
  161. true,
  162. 'features.groupSSO must be true when set'
  163. )
  164. }
  165. })
  166. })
  167. }
  168. }
  169. })
  170. })