From c5bb18045ef9d55b5f60de70453b048e0d6d353c Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Fri, 18 Aug 2023 09:57:58 +0200 Subject: [PATCH] Merge pull request #14383 from overleaf/jpa-server-pro-feature-refresh-migration [web] add migration for Server Pro/CE to refresh features once GitOrigin-RevId: 799e6aef2ad9ad6806ec369911d56f7a40945098 --- ..._back_fill_gitBridge_feature_server_pro.js | 11 ++ .../scripts/upgrade-user-features.js | 60 ++++++++ .../acceptance/src/ServerCEScriptsTests.js | 137 ++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 services/web/migrations/20230817081910_back_fill_gitBridge_feature_server_pro.js create mode 100644 services/web/modules/server-ce-scripts/scripts/upgrade-user-features.js diff --git a/services/web/migrations/20230817081910_back_fill_gitBridge_feature_server_pro.js b/services/web/migrations/20230817081910_back_fill_gitBridge_feature_server_pro.js new file mode 100644 index 0000000000..71e062da63 --- /dev/null +++ b/services/web/migrations/20230817081910_back_fill_gitBridge_feature_server_pro.js @@ -0,0 +1,11 @@ +exports.tags = ['server-ce', 'server-pro'] + +exports.migrate = async () => { + // Run-time import as SaaS does not ship with the server-ce-scripts module + const runScript = require('../modules/server-ce-scripts/scripts/upgrade-user-features') + await runScript(false, { + gitBridge: 1, + }) +} + +exports.rollback = async () => {} diff --git a/services/web/modules/server-ce-scripts/scripts/upgrade-user-features.js b/services/web/modules/server-ce-scripts/scripts/upgrade-user-features.js new file mode 100644 index 0000000000..eb511fc298 --- /dev/null +++ b/services/web/modules/server-ce-scripts/scripts/upgrade-user-features.js @@ -0,0 +1,60 @@ +const Settings = require('@overleaf/settings') +const logger = require('@overleaf/logger') +const { db, waitForDb } = require('../../../app/src/infrastructure/mongodb') +const { + mergeFeatures, + compareFeatures, +} = require('../../../app/src/Features/Subscription/FeaturesHelper') +const DRY_RUN = !process.argv.includes('--dry-run=false') + +async function main(DRY_RUN, defaultFeatures) { + await waitForDb() + + logger.info({ defaultFeatures }, 'default features') + + const cursor = db.users.find( + {}, + { projection: { _id: 1, email: 1, features: 1 } } + ) + for await (const user of cursor) { + const newFeatures = mergeFeatures(user.features, defaultFeatures) + const diff = compareFeatures(newFeatures, user.features) + if (Object.keys(diff).length > 0) { + logger.warn( + { + userId: user._id, + email: user.email, + oldFeatures: user.features, + newFeatures, + }, + 'user features upgraded' + ) + + if (!DRY_RUN) { + await db.users.updateOne( + { _id: user._id }, + { $set: { features: newFeatures } } + ) + } + } + } +} + +module.exports = main + +if (require.main === module) { + if (DRY_RUN) { + console.error('---') + console.error('Dry-run enabled, use --dry-run=false to commit changes') + console.error('---') + } + main(DRY_RUN, Settings.defaultFeatures) + .then(() => { + console.log('Done.') + process.exit(0) + }) + .catch(error => { + console.error({ error }) + process.exit(1) + }) +} diff --git a/services/web/modules/server-ce-scripts/test/acceptance/src/ServerCEScriptsTests.js b/services/web/modules/server-ce-scripts/test/acceptance/src/ServerCEScriptsTests.js index 9a4d069c63..54d59cfca2 100644 --- a/services/web/modules/server-ce-scripts/test/acceptance/src/ServerCEScriptsTests.js +++ b/services/web/modules/server-ce-scripts/test/acceptance/src/ServerCEScriptsTests.js @@ -1,3 +1,4 @@ +const Settings = require('@overleaf/settings') const { execSync } = require('child_process') const { expect } = require('chai') const { db } = require('../../../../../app/src/infrastructure/mongodb') @@ -230,4 +231,140 @@ describe('ServerCEScripts', function () { }) }) }) + + describe('upgrade-user-features', function () { + let userLatest, userSP1, userCustomTimeoutLower, userCustomTimeoutHigher + beforeEach('create users', async function () { + userLatest = new User() + userSP1 = new User() + userCustomTimeoutLower = new User() + userCustomTimeoutHigher = new User() + + await Promise.all([ + userLatest.ensureUserExists(), + userSP1.ensureUserExists(), + userCustomTimeoutLower.ensureUserExists(), + userCustomTimeoutHigher.ensureUserExists(), + ]) + }) + + const serverPro1Features = { + collaborators: -1, + dropbox: true, + versioning: true, + compileTimeout: 180, + compileGroup: 'standard', + references: true, + templates: true, + trackChanges: true, + } + + beforeEach('downgrade userSP1', async function () { + await userSP1.mongoUpdate({ $set: { features: serverPro1Features } }) + }) + + beforeEach('downgrade userCustomTimeoutLower', async function () { + run( + `node modules/server-ce-scripts/scripts/change-compile-timeout --user-id=${userCustomTimeoutLower.id} --compile-timeout=42` + ) + }) + + beforeEach('upgrade userCustomTimeoutHigher', async function () { + run( + `node modules/server-ce-scripts/scripts/change-compile-timeout --user-id=${userCustomTimeoutHigher.id} --compile-timeout=360` + ) + }) + + async function getFeatures() { + return [ + await userLatest.getFeatures(), + await userSP1.getFeatures(), + await userCustomTimeoutLower.getFeatures(), + await userCustomTimeoutHigher.getFeatures(), + ] + } + + let initialFeatures + beforeEach('collect initial features', async function () { + initialFeatures = await getFeatures() + }) + + it('should have prepared the right features', async function () { + expect(initialFeatures).to.deep.equal([ + Settings.defaultFeatures, + serverPro1Features, + Object.assign({}, Settings.defaultFeatures, { + compileTimeout: 42, + }), + Object.assign({}, Settings.defaultFeatures, { + compileTimeout: 360, + }), + ]) + }) + + describe('dry-run', function () { + let output + beforeEach('run script', function () { + output = run( + `node modules/server-ce-scripts/scripts/upgrade-user-features` + ) + }) + + it('should update SP1 features', function () { + expect(output).to.include(userSP1.id) + }) + + it('should update lowerTimeout features', function () { + expect(output).to.include(userCustomTimeoutLower.id) + }) + + it('should not update latest features', function () { + expect(output).to.not.include(userLatest.id) + }) + + it('should not update higherTimeout features', function () { + expect(output).to.not.include(userCustomTimeoutHigher.id) + }) + + it('should not change any features in the db', async function () { + expect(await getFeatures()).to.deep.equal(initialFeatures) + }) + }) + + describe('live run', function () { + let output + beforeEach('run script', function () { + output = run( + `node modules/server-ce-scripts/scripts/upgrade-user-features --dry-run=false` + ) + }) + + it('should update SP1 features', function () { + expect(output).to.include(userSP1.id) + }) + + it('should update lowerTimeout features', function () { + expect(output).to.include(userCustomTimeoutLower.id) + }) + + it('should not update latest features', function () { + expect(output).to.not.include(userLatest.id) + }) + + it('should not update higherTimeout features', function () { + expect(output).to.not.include(userCustomTimeoutHigher.id) + }) + + it('should update features in the db', async function () { + expect(await getFeatures()).to.deep.equal([ + Settings.defaultFeatures, + Settings.defaultFeatures, + Settings.defaultFeatures, + Object.assign({}, Settings.defaultFeatures, { + compileTimeout: 360, + }), + ]) + }) + }) + }) })