From 2a5f0e4de2ce18bf8730251210d99c24b8e8fced Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 20 Jan 2026 09:38:21 +0000 Subject: [PATCH] Merge pull request #30856 from overleaf/bg-batched-update-collection-audit add script to check for incorrect batchedUpdate migrations GitOrigin-RevId: b6ab9750ef1c3ddd02c2fabad54e2053ce2c2584 --- ...ck_collection_for_batched_update_issue.mjs | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100755 tools/migrations/scripts/check_collection_for_batched_update_issue.mjs diff --git a/tools/migrations/scripts/check_collection_for_batched_update_issue.mjs b/tools/migrations/scripts/check_collection_for_batched_update_issue.mjs new file mode 100755 index 0000000000..6684f1e32a --- /dev/null +++ b/tools/migrations/scripts/check_collection_for_batched_update_issue.mjs @@ -0,0 +1,181 @@ +#!/usr/bin/env node + +/** + * Checks collections for potential issues with batchedUpdate missing + * entries at the start of the collection. + */ + +import assert from 'node:assert/strict' +import { describe, it, before, after } from 'node:test' +import { db, connectionPromise } from '../lib/mongodb.mjs' + +/** + * Finds IDs where the natural order and _id sorted order differ. + * This identifies records that might have been moved or updated in a way + * that affected their storage position in MongoDB. + */ +async function findDivergentRecords(coll) { + const [firstNatural, firstSorted] = await Promise.all([ + db[coll].find().limit(1).next(), + db[coll].find().sort({ _id: 1 }).limit(1).next(), + ]) + + if (!firstNatural || !firstSorted || firstNatural._id.equals(firstSorted._id)) + return [] + + const [minId, maxId] = + firstNatural._id.toString() < firstSorted._id.toString() + ? [firstNatural._id, firstSorted._id] + : [firstSorted._id, firstNatural._id] + + const found = await db[coll] + .find({ _id: { $gte: minId, $lte: maxId } }) + .toArray() + return found +} + +describe('Batched update audit', async () => { + let client + + before(async () => { + client = await connectionPromise + }) + + after(async () => { + if (client) await client.close() + }) + + describe('users collection', async () => { + const records = await findDivergentRecords('users') + if (records.length === 0) { + it('no divergent records found', () => assert.ok(true)) + } else { + for (const record of records) { + describe(`Record: ${record._id}`, () => { + // tools/migrations/20210726083523_convert_confirmedAt_strings_to_dates.mjs + // any confirmedAt field must be a date + it('confirmedAt is a Date object in email records', () => { + record.emails?.forEach(emailObj => { + if ('confirmedAt' in emailObj) { + assert.ok(emailObj.confirmedAt instanceof Date) + } + }) + }) + // tools/migrations/20210726083523_convert_split_tests_assigned_at_strings_to_dates.mjs + // each split test entry has a date in the assignedAt field if set + it('splitTests entry assignedAt fields are Date objects', () => { + // splitTests is stored as an object keyed by test name + Object.values(record.splitTests || {}).forEach(test => { + if (test.assignedAt) assert.ok(test.assignedAt instanceof Date) + }) + }) + // tools/migrations/20220826104236_disable_alpha_beta_program.mjs + // should be strictly false, but allow undefined for simplicity + it('alpha/beta program are disabled', () => { + assert.ok( + record.alphaProgram === false || record.alphaProgram === undefined + ) + assert.ok( + record.betaProgram === false || record.betaProgram === undefined + ) + }) + // tools/migrations/20220913125500_migrate_auditLog_to_collections.mjs + it('obsolete auditLog field is not present', () => { + assert.equal(record.auditLog, undefined) + }) + // tools/migrations/20230124092607_clear_old_2fa_setup.mjs + it('obsolete 2fa field is not present', () => { + assert.equal(record.twoFactorAuthentication, undefined) + }) + // tools/migrations/20240618125145_cleanup_user_features_templates.mjs + // unsets features.templates + it('features does not contain a templates field', () => { + assert.equal( + record.features?.templates, + undefined, + `features ${JSON.stringify(record.features)}` + ) + }) + // tools/migrations/20240713110905_emails_reversed_hostname.mjs + it('reversedHostname matches reversed email domain', () => { + record.emails.forEach((emailObj, index) => { + if (emailObj.email) { + const expected = emailObj.email + .split('@')[1] + .split('') + .reverse() + .join('') + assert.equal( + emailObj.reversedHostname, + expected, + `Email ${index} mismatch` + ) + } + }) + }) + }) + } + } + }) + + describe('subscriptions collection', async () => { + const records = await findDivergentRecords('subscriptions') + if (records.length === 0) { + it('no divergent records found', () => assert.ok(true)) + } else { + for (const record of records) { + describe(`Record: ${record._id}`, () => { + // tools/migrations/20230110140452_rename_recurly_cached_status.mjs + // renames recurly to recurlyStatus + it('recurly field does not exist', () => + assert.equal(record.recurly, undefined)) + // tools/migrations/20230207134844_group_invite_emails_to_lowercase.mjs + // changes array entries to lowercase + it('teamInvites emails are lowercase', () => { + record.teamInvites?.forEach(invite => { + if (invite.email) + assert.equal(invite.email, invite.email.toLowerCase()) + }) + }) + // tools/migrations/20230928092537_backfill_subscriptions_managed_users_feature_flag.mjs + // unsets features.managedUsers if $ne:true + // Note this is superseded by the 20241111133330_remove_null_managed_users_sso_from_subscriptions.mjs below + it('features.managedUsers is either true or null', () => { + const managedUsers = record.features?.managedUsers + assert.ok( + managedUsers === true || managedUsers === null, + 'features.managedUsers must be either true or null' + ) + }) + // tools/migrations/20231030160030_managed_users_enabled.mjs + // sets managedUsersEnabled to true when groupPolicy exists + it('managedUsersEnabled is true if groupPolicy exists', () => { + if ('groupPolicy' in record) { + assert(record.managedUsersEnabled === true) + } + }) + // tools/migrations/20241111133330_remove_null_managed_users_sso_from_subscriptions.mjs + it('features.managedUsers is true if set', () => { + if (record.features && 'managedUsers' in record.features) { + assert.strictEqual( + record.features.managedUsers, + true, + 'features.managedUsers must be true when set' + ) + } + }) + // also in tools/migrations/20241111133330_remove_null_managed_users_sso_from_subscriptions.mjs + it('features.groupSSO is true if set', () => { + if (record.features && 'groupSSO' in record.features) { + assert.strictEqual( + record.features.groupSSO, + true, + 'features.groupSSO must be true when set' + ) + } + }) + }) + } + } + }) +})