mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #27476 from overleaf/jpa-transfer-all
[web] add script for transferring all of a users projects in Server Pro GitOrigin-RevId: 3aad2b624e1da2af83fec0715c2e5e08eff43695
This commit is contained in:
@@ -9,9 +9,75 @@ const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
|
||||
const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
|
||||
const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
|
||||
const AnalyticsManager = require('../Analytics/AnalyticsManager')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const TagsHandler = require('../Tags/TagsHandler')
|
||||
const { promiseMapWithLimit } = require('@overleaf/promise-utils')
|
||||
|
||||
module.exports = {
|
||||
promises: { transferOwnership },
|
||||
promises: {
|
||||
transferOwnership,
|
||||
transferAllProjectsToUser,
|
||||
},
|
||||
}
|
||||
|
||||
const TAG_COLOR_BLUE = '#434AF0'
|
||||
|
||||
/**
|
||||
* @param {string} fromUserId
|
||||
* @param {string} toUserId
|
||||
* @param {string} ipAddress
|
||||
* @return {Promise<{projectCount: number, newTagName: string}>}
|
||||
*/
|
||||
async function transferAllProjectsToUser({ fromUserId, toUserId, ipAddress }) {
|
||||
// - Verify that both users exist
|
||||
const fromUser = await UserGetter.promises.getUser(fromUserId, {
|
||||
_id: 1,
|
||||
email: 1,
|
||||
})
|
||||
const toUser = await UserGetter.promises.getUser(toUserId, { _id: 1 })
|
||||
if (!fromUser) throw new OError('missing source user', { fromUserId })
|
||||
if (!toUser) throw new OError('missing destination user', { toUserId })
|
||||
if (fromUser._id.equals(toUser._id))
|
||||
throw new OError('rejecting transfer between identical users', {
|
||||
fromUserId,
|
||||
toUserId,
|
||||
})
|
||||
logger.debug(
|
||||
{ fromUserId, toUserId },
|
||||
'started bulk transfer of all projects from one user to another'
|
||||
)
|
||||
// - Get all owned projects for fromUserId
|
||||
const projects = await Project.find({ owner_ref: fromUserId }, { _id: 1 })
|
||||
|
||||
// - Create new tag on toUserId
|
||||
const newTag = await TagsHandler.promises.createTag(
|
||||
toUserId,
|
||||
`transferred-from-${fromUser.email}`,
|
||||
TAG_COLOR_BLUE,
|
||||
{ truncate: true }
|
||||
)
|
||||
|
||||
// - Add tag to projects (can happen before ownership is transferred)
|
||||
await TagsHandler.promises.addProjectsToTag(
|
||||
toUserId,
|
||||
newTag._id,
|
||||
projects.map(p => p._id)
|
||||
)
|
||||
|
||||
// - Transfer all projects
|
||||
await promiseMapWithLimit(5, projects, async project => {
|
||||
await transferOwnership(project._id, toUserId, {
|
||||
allowTransferToNonCollaborators: true,
|
||||
skipEmails: true,
|
||||
ipAddress,
|
||||
})
|
||||
})
|
||||
|
||||
logger.debug(
|
||||
{ fromUserId, toUserId },
|
||||
'finished bulk transfer of all projects from one user to another'
|
||||
)
|
||||
return { projectCount: projects.length, newTagName: newTag.name }
|
||||
}
|
||||
|
||||
async function transferOwnership(projectId, newOwnerId, options = {}) {
|
||||
@@ -74,8 +140,8 @@ async function transferOwnership(projectId, newOwnerId, options = {}) {
|
||||
await TpdsProjectFlusher.promises.flushProjectToTpds(projectId)
|
||||
|
||||
// Send confirmation emails
|
||||
const previousOwner = await UserGetter.promises.getUser(previousOwnerId)
|
||||
if (!skipEmails) {
|
||||
const previousOwner = await UserGetter.promises.getUser(previousOwnerId)
|
||||
await _sendEmails(project, previousOwner, newOwner)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ObjectId } from '../../../app/src/infrastructure/mongodb.js'
|
||||
import minimist from 'minimist'
|
||||
import OwnershipTransferHandler from '../../../app/src/Features/Collaborators/OwnershipTransferHandler.js'
|
||||
import UserGetter from '../../../app/src/Features/User/UserGetter.js'
|
||||
import EmailHelper from '../../../app/src/Features/Helpers/EmailHelper.js'
|
||||
|
||||
const args = minimist(process.argv.slice(2), {
|
||||
string: ['from-user', 'to-user'],
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string} flag
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
async function resolveUser(flag) {
|
||||
const raw = args[flag]
|
||||
if (!raw) throw new Error(`missing parameter --${flag}`)
|
||||
if (ObjectId.isValid(raw)) return raw
|
||||
const email = EmailHelper.parseEmail(raw)
|
||||
if (!email) throw new Error(`invalid email --${flag}=${raw}`)
|
||||
const user = await UserGetter.promises.getUser({ email }, { _id: 1 })
|
||||
if (!user)
|
||||
throw new Error(`user with email --${flag}=${email} does not exist`)
|
||||
return user._id.toString()
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const fromUserId = await resolveUser('from-user')
|
||||
const toUserId = await resolveUser('to-user')
|
||||
await OwnershipTransferHandler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress: '0.0.0.0',
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
console.error('Done.')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('---')
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { execSync } from 'node:child_process'
|
||||
import { exec } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import Settings from '@overleaf/settings'
|
||||
import { expect } from 'chai'
|
||||
@@ -9,31 +9,43 @@ const { promises: User } = UserHelper
|
||||
|
||||
/**
|
||||
* @param {string} cmd
|
||||
* @return {string}
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
function run(cmd) {
|
||||
async function run(cmd) {
|
||||
// https://nodejs.org/docs/latest-v12.x/api/child_process.html#child_process_child_process_execsync_command_options
|
||||
// > stderr by default will be output to the parent process' stderr
|
||||
// > unless stdio is specified.
|
||||
// https://nodejs.org/docs/latest-v12.x/api/child_process.html#child_process_options_stdio
|
||||
// Pipe stdin from /dev/null, store stdout, pipe stderr to /dev/null.
|
||||
return execSync(cmd, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
LOG_LEVEL: 'warn',
|
||||
},
|
||||
}).toString()
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
cmd,
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
LOG_LEVEL: 'warn',
|
||||
},
|
||||
},
|
||||
(error, stdout) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(stdout)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function runAndExpectError(cmd, errorMessages) {
|
||||
async function runAndExpectError(cmd, errorMessages, exitCode = 1) {
|
||||
try {
|
||||
run(cmd)
|
||||
await run(cmd)
|
||||
} catch (error) {
|
||||
expect(error.status).to.equal(1)
|
||||
expect(error.code).to.equal(exitCode)
|
||||
if (errorMessages) {
|
||||
errorMessages.forEach(errorMessage =>
|
||||
expect(error.stderr.toString()).to.include(errorMessage)
|
||||
expect(error.message).to.include(errorMessage)
|
||||
)
|
||||
}
|
||||
return
|
||||
@@ -47,60 +59,48 @@ async function getUser(email) {
|
||||
|
||||
describe('ServerCEScripts', function () {
|
||||
describe('check-mongodb', function () {
|
||||
it('should exit with code 0 on success', function () {
|
||||
run('node modules/server-ce-scripts/scripts/check-mongodb.mjs')
|
||||
it('should exit with code 0 on success', async function () {
|
||||
await run('node modules/server-ce-scripts/scripts/check-mongodb.mjs')
|
||||
})
|
||||
|
||||
it('should exit with code 1 on error', function () {
|
||||
try {
|
||||
run(
|
||||
'MONGO_SERVER_SELECTION_TIMEOUT=1' +
|
||||
'MONGO_CONNECTION_STRING=mongodb://127.0.0.1:4242 ' +
|
||||
'node modules/server-ce-scripts/scripts/check-mongodb.mjs'
|
||||
)
|
||||
} catch (e) {
|
||||
expect(e.status).to.equal(1)
|
||||
return
|
||||
}
|
||||
expect.fail('command should have failed')
|
||||
it('should exit with code 1 on error', async function () {
|
||||
await runAndExpectError(
|
||||
'MONGO_SERVER_SELECTION_TIMEOUT=1 ' +
|
||||
'MONGO_CONNECTION_STRING=mongodb://127.0.0.1:4242 ' +
|
||||
'node modules/server-ce-scripts/scripts/check-mongodb.mjs'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('check-redis', function () {
|
||||
it('should exit with code 0 on success', function () {
|
||||
run('node modules/server-ce-scripts/scripts/check-redis.mjs')
|
||||
it('should exit with code 0 on success', async function () {
|
||||
await run('node modules/server-ce-scripts/scripts/check-redis.mjs')
|
||||
})
|
||||
|
||||
it('should exit with code 1 on error', function () {
|
||||
try {
|
||||
run(
|
||||
'REDIS_PORT=42 node modules/server-ce-scripts/scripts/check-redis.mjs'
|
||||
)
|
||||
} catch (e) {
|
||||
expect(e.status).to.equal(1)
|
||||
return
|
||||
}
|
||||
expect.fail('command should have failed')
|
||||
it('should exit with code 1 on error', async function () {
|
||||
await runAndExpectError(
|
||||
'REDIS_PORT=42 node modules/server-ce-scripts/scripts/check-redis.mjs'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('create-user', function () {
|
||||
it('should exit with code 0 on success', function () {
|
||||
const out = run(
|
||||
it('should exit with code 0 on success', async function () {
|
||||
const out = await run(
|
||||
'node modules/server-ce-scripts/scripts/create-user.js --email=foo@bar.com'
|
||||
)
|
||||
expect(out).to.include('/user/activate?token=')
|
||||
})
|
||||
|
||||
it('should create a regular user by default', async function () {
|
||||
run(
|
||||
await run(
|
||||
'node modules/server-ce-scripts/scripts/create-user.js --email=foo@bar.com'
|
||||
)
|
||||
expect(await getUser('foo@bar.com')).to.deep.equal({ isAdmin: false })
|
||||
})
|
||||
|
||||
it('should also work with mjs version', async function () {
|
||||
const out = run(
|
||||
const out = await run(
|
||||
'node modules/server-ce-scripts/scripts/create-user.mjs --email=foo@bar.com'
|
||||
)
|
||||
expect(out).to.include('/user/activate?token=')
|
||||
@@ -108,20 +108,16 @@ describe('ServerCEScripts', function () {
|
||||
})
|
||||
|
||||
it('should create an admin user with --admin flag', async function () {
|
||||
run(
|
||||
await run(
|
||||
'node modules/server-ce-scripts/scripts/create-user.js --admin --email=foo@bar.com'
|
||||
)
|
||||
expect(await getUser('foo@bar.com')).to.deep.equal({ isAdmin: true })
|
||||
})
|
||||
|
||||
it('should exit with code 1 on missing email', function () {
|
||||
try {
|
||||
run('node modules/server-ce-scripts/scripts/create-user.js')
|
||||
} catch (e) {
|
||||
expect(e.status).to.equal(1)
|
||||
return
|
||||
}
|
||||
expect.fail('command should have failed')
|
||||
it('should exit with code 1 on missing email', async function () {
|
||||
await runAndExpectError(
|
||||
'node modules/server-ce-scripts/scripts/create-user.js'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -132,18 +128,18 @@ describe('ServerCEScripts', function () {
|
||||
await user.login()
|
||||
})
|
||||
|
||||
it('should log missing user', function () {
|
||||
it('should log missing user', async function () {
|
||||
const email = 'does-not-exist@example.com'
|
||||
const out = run(
|
||||
const out = await run(
|
||||
'node modules/server-ce-scripts/scripts/delete-user.mjs --email=' +
|
||||
email
|
||||
)
|
||||
expect(out).to.include('not in database, potentially already deleted')
|
||||
})
|
||||
|
||||
it('should exit with code 0 on success', function () {
|
||||
it('should exit with code 0 on success', async function () {
|
||||
const email = user.email
|
||||
run(
|
||||
await run(
|
||||
'node modules/server-ce-scripts/scripts/delete-user.mjs --email=' +
|
||||
email
|
||||
)
|
||||
@@ -151,7 +147,7 @@ describe('ServerCEScripts', function () {
|
||||
|
||||
it('should have deleted the user on success', async function () {
|
||||
const email = user.email
|
||||
run(
|
||||
await run(
|
||||
'node modules/server-ce-scripts/scripts/delete-user.mjs --email=' +
|
||||
email
|
||||
)
|
||||
@@ -164,14 +160,10 @@ describe('ServerCEScripts', function () {
|
||||
expect(softDeletedEntry.deleterData.deleterIpAddress).to.equal('0.0.0.0')
|
||||
})
|
||||
|
||||
it('should exit with code 1 on missing email', function () {
|
||||
try {
|
||||
run('node modules/server-ce-scripts/scripts/delete-user.mjs')
|
||||
} catch (e) {
|
||||
expect(e.status).to.equal(1)
|
||||
return
|
||||
}
|
||||
expect.fail('command should have failed')
|
||||
it('should exit with code 1 on missing email', async function () {
|
||||
await runAndExpectError(
|
||||
'node modules/server-ce-scripts/scripts/delete-user.mjs'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -221,7 +213,7 @@ describe('ServerCEScripts', function () {
|
||||
})
|
||||
|
||||
it('should do a dry run by default', async function () {
|
||||
run(
|
||||
await run(
|
||||
`node modules/server-ce-scripts/scripts/migrate-user-emails.mjs ${csv}`
|
||||
)
|
||||
for (const user of usersToMigrate) {
|
||||
@@ -234,14 +226,14 @@ describe('ServerCEScripts', function () {
|
||||
}
|
||||
})
|
||||
|
||||
it('should exit with code 0 when successfully migrating user emails', function () {
|
||||
run(
|
||||
it('should exit with code 0 when successfully migrating user emails', async function () {
|
||||
await run(
|
||||
`node modules/server-ce-scripts/scripts/migrate-user-emails.mjs --commit ${csv}`
|
||||
)
|
||||
})
|
||||
|
||||
it('should migrate the user emails with the --commit option', async function () {
|
||||
run(
|
||||
await run(
|
||||
`node modules/server-ce-scripts/scripts/migrate-user-emails.mjs --commit ${csv}`
|
||||
)
|
||||
for (const user of usersToMigrate) {
|
||||
@@ -255,7 +247,7 @@ describe('ServerCEScripts', function () {
|
||||
})
|
||||
|
||||
it('should leave other user emails unchanged', async function () {
|
||||
run(
|
||||
await run(
|
||||
`node modules/server-ce-scripts/scripts/migrate-user-emails.mjs --commit ${csv}`
|
||||
)
|
||||
for (const user of otherUsers) {
|
||||
@@ -264,39 +256,27 @@ describe('ServerCEScripts', function () {
|
||||
}
|
||||
})
|
||||
|
||||
it('should exit with code 1 when there are failures migrating user emails', function () {
|
||||
try {
|
||||
run(
|
||||
`node modules/server-ce-scripts/scripts/migrate-user-emails.mjs --commit ${csvfail}`
|
||||
)
|
||||
} catch (e) {
|
||||
expect(e.status).to.equal(1)
|
||||
return
|
||||
}
|
||||
expect.fail('command should have failed')
|
||||
it('should exit with code 1 when there are failures migrating user emails', async function () {
|
||||
await runAndExpectError(
|
||||
`node modules/server-ce-scripts/scripts/migrate-user-emails.mjs --commit ${csvfail}`
|
||||
)
|
||||
})
|
||||
|
||||
it('should migrate other users when there are failures with the --continue option', async function () {
|
||||
try {
|
||||
run(
|
||||
`node modules/server-ce-scripts/scripts/migrate-user-emails.mjs --commit ${csvfail}`
|
||||
)
|
||||
} catch (e) {
|
||||
expect(e.status).to.equal(1)
|
||||
run(
|
||||
`node modules/server-ce-scripts/scripts/migrate-user-emails.mjs --commit --continue ${csvfail}`
|
||||
)
|
||||
for (const user of usersToMigrate) {
|
||||
const dbEntry = await user.get()
|
||||
expect(dbEntry.email).to.equal(`new-${user.email}`)
|
||||
expect(dbEntry.emails).to.have.lengthOf(1)
|
||||
expect(dbEntry.emails[0].email).to.equal(`new-${user.email}`)
|
||||
expect(dbEntry.emails[0].reversedHostname).to.equal('moc.elpmaxe')
|
||||
expect(dbEntry.emails[0].createdAt).to.eql(user.emails[0].createdAt)
|
||||
}
|
||||
return
|
||||
await runAndExpectError(
|
||||
`node modules/server-ce-scripts/scripts/migrate-user-emails.mjs --commit ${csvfail}`
|
||||
)
|
||||
await run(
|
||||
`node modules/server-ce-scripts/scripts/migrate-user-emails.mjs --commit --continue ${csvfail}`
|
||||
)
|
||||
for (const user of usersToMigrate) {
|
||||
const dbEntry = await user.get()
|
||||
expect(dbEntry.email).to.equal(`new-${user.email}`)
|
||||
expect(dbEntry.emails).to.have.lengthOf(1)
|
||||
expect(dbEntry.emails[0].email).to.equal(`new-${user.email}`)
|
||||
expect(dbEntry.emails[0].reversedHostname).to.equal('moc.elpmaxe')
|
||||
expect(dbEntry.emails[0].createdAt).to.eql(user.emails[0].createdAt)
|
||||
}
|
||||
expect.fail('command should have failed')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -323,7 +303,7 @@ describe('ServerCEScripts', function () {
|
||||
|
||||
expect(await getTagNames()).to.deep.equal([oldName])
|
||||
|
||||
run(
|
||||
await run(
|
||||
`node modules/server-ce-scripts/scripts/rename-tag.mjs --user-id=${user.id} --old-name=${oldName} --new-name=${newName}`
|
||||
)
|
||||
|
||||
@@ -354,9 +334,9 @@ describe('ServerCEScripts', function () {
|
||||
|
||||
describe('happy path', function () {
|
||||
let newUserATimeout
|
||||
beforeEach('run script on user a', function () {
|
||||
beforeEach('run script on user a', async function () {
|
||||
newUserATimeout = userATimeout - 1
|
||||
run(
|
||||
await run(
|
||||
`node modules/server-ce-scripts/scripts/change-compile-timeout.mjs --user-id=${userA.id} --compile-timeout=${newUserATimeout}`
|
||||
)
|
||||
})
|
||||
@@ -374,27 +354,21 @@ describe('ServerCEScripts', function () {
|
||||
|
||||
describe('bad options', function () {
|
||||
it('should reject zero timeout', async function () {
|
||||
try {
|
||||
run(
|
||||
`node modules/server-ce-scripts/scripts/change-compile-timeout.mjs --user-id=${userA.id} --compile-timeout=0`
|
||||
)
|
||||
expect.fail('should error out')
|
||||
} catch (err) {
|
||||
expect(err.stderr.toString()).to.include('positive number of seconds')
|
||||
}
|
||||
await runAndExpectError(
|
||||
`node modules/server-ce-scripts/scripts/change-compile-timeout.mjs --user-id=${userA.id} --compile-timeout=0`,
|
||||
['positive number of seconds'],
|
||||
101
|
||||
)
|
||||
expect(await getCompileTimeout(userA)).to.equal(userATimeout)
|
||||
expect(await getCompileTimeout(userB)).to.equal(userBTimeout)
|
||||
})
|
||||
|
||||
it('should reject a 20min timeout', async function () {
|
||||
try {
|
||||
run(
|
||||
`node modules/server-ce-scripts/scripts/change-compile-timeout.mjs --user-id=${userA.id} --compile-timeout=1200`
|
||||
)
|
||||
expect.fail('should error out')
|
||||
} catch (err) {
|
||||
expect(err.stderr.toString()).to.include('below 10 minutes')
|
||||
}
|
||||
await runAndExpectError(
|
||||
`node modules/server-ce-scripts/scripts/change-compile-timeout.mjs --user-id=${userA.id} --compile-timeout=1200`,
|
||||
['below 10 minutes'],
|
||||
101
|
||||
)
|
||||
expect(await getCompileTimeout(userA)).to.equal(userATimeout)
|
||||
expect(await getCompileTimeout(userB)).to.equal(userBTimeout)
|
||||
})
|
||||
@@ -432,13 +406,13 @@ describe('ServerCEScripts', function () {
|
||||
})
|
||||
|
||||
beforeEach('downgrade userCustomTimeoutLower', async function () {
|
||||
run(
|
||||
await run(
|
||||
`node modules/server-ce-scripts/scripts/change-compile-timeout.mjs --user-id=${userCustomTimeoutLower.id} --compile-timeout=42`
|
||||
)
|
||||
})
|
||||
|
||||
beforeEach('upgrade userCustomTimeoutHigher', async function () {
|
||||
run(
|
||||
await run(
|
||||
`node modules/server-ce-scripts/scripts/change-compile-timeout.mjs --user-id=${userCustomTimeoutHigher.id} --compile-timeout=360`
|
||||
)
|
||||
})
|
||||
@@ -472,8 +446,8 @@ describe('ServerCEScripts', function () {
|
||||
|
||||
describe('dry-run', function () {
|
||||
let output
|
||||
beforeEach('run script', function () {
|
||||
output = run(
|
||||
beforeEach('run script', async function () {
|
||||
output = await run(
|
||||
`node modules/server-ce-scripts/scripts/upgrade-user-features.mjs`
|
||||
)
|
||||
})
|
||||
@@ -501,8 +475,8 @@ describe('ServerCEScripts', function () {
|
||||
|
||||
describe('live run', function () {
|
||||
let output
|
||||
beforeEach('run script', function () {
|
||||
output = run(
|
||||
beforeEach('run script', async function () {
|
||||
output = await run(
|
||||
`node modules/server-ce-scripts/scripts/upgrade-user-features.mjs --dry-run=false`
|
||||
)
|
||||
})
|
||||
@@ -572,8 +546,10 @@ describe('ServerCEScripts', function () {
|
||||
})
|
||||
|
||||
describe('when running in CE', function () {
|
||||
beforeEach('run script', function () {
|
||||
output = run(buildCheckTexLiveCmd({ OVERLEAF_IS_SERVER_PRO: false }))
|
||||
beforeEach('run script', async function () {
|
||||
output = await run(
|
||||
buildCheckTexLiveCmd({ OVERLEAF_IS_SERVER_PRO: false })
|
||||
)
|
||||
})
|
||||
|
||||
it('should skip checks', function () {
|
||||
@@ -584,8 +560,8 @@ describe('ServerCEScripts', function () {
|
||||
})
|
||||
|
||||
describe('when sandboxed compiles are disabled', function () {
|
||||
beforeEach('run script', function () {
|
||||
output = run(buildCheckTexLiveCmd({ SANDBOXED_COMPILES: false }))
|
||||
beforeEach('run script', async function () {
|
||||
output = await run(buildCheckTexLiveCmd({ SANDBOXED_COMPILES: false }))
|
||||
})
|
||||
|
||||
it('should skip checks', function () {
|
||||
@@ -596,8 +572,8 @@ describe('ServerCEScripts', function () {
|
||||
})
|
||||
|
||||
describe('when texlive configuration is incorrect', function () {
|
||||
it('should fail when TEX_LIVE_DOCKER_IMAGE is not set', function () {
|
||||
runAndExpectError(
|
||||
it('should fail when TEX_LIVE_DOCKER_IMAGE is not set', async function () {
|
||||
await runAndExpectError(
|
||||
buildCheckTexLiveCmd({
|
||||
SANDBOXED_COMPILES: true,
|
||||
ALL_TEX_LIVE_DOCKER_IMAGES: TEST_TL_IMAGE_LIST,
|
||||
@@ -608,8 +584,8 @@ describe('ServerCEScripts', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('should fail when ALL_TEX_LIVE_DOCKER_IMAGES is not set', function () {
|
||||
runAndExpectError(
|
||||
it('should fail when ALL_TEX_LIVE_DOCKER_IMAGES is not set', async function () {
|
||||
await runAndExpectError(
|
||||
buildCheckTexLiveCmd({
|
||||
SANDBOXED_COMPILES: true,
|
||||
TEX_LIVE_DOCKER_IMAGE: TEST_TL_IMAGE,
|
||||
@@ -620,8 +596,8 @@ describe('ServerCEScripts', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('should fail when TEX_LIVE_DOCKER_IMAGE is not defined in ALL_TEX_LIVE_DOCKER_IMAGES', function () {
|
||||
runAndExpectError(
|
||||
it('should fail when TEX_LIVE_DOCKER_IMAGE is not defined in ALL_TEX_LIVE_DOCKER_IMAGES', async function () {
|
||||
await runAndExpectError(
|
||||
buildCheckTexLiveCmd({
|
||||
SANDBOXED_COMPILES: true,
|
||||
TEX_LIVE_DOCKER_IMAGE: 'tl-1',
|
||||
@@ -639,8 +615,8 @@ describe('ServerCEScripts', function () {
|
||||
await db.projects.updateMany({}, { $unset: { imageName: 1 } })
|
||||
})
|
||||
|
||||
it('should fail and suggest running backfilling scripts', function () {
|
||||
runAndExpectError(
|
||||
it('should fail and suggest running backfilling scripts', async function () {
|
||||
await runAndExpectError(
|
||||
buildCheckTexLiveCmd({
|
||||
SANDBOXED_COMPILES: true,
|
||||
TEX_LIVE_DOCKER_IMAGE: TEST_TL_IMAGE,
|
||||
@@ -659,8 +635,8 @@ describe('ServerCEScripts', function () {
|
||||
await db.projects.updateMany({}, { $set: { imageName: null } })
|
||||
})
|
||||
|
||||
it('should fail and suggest running backfilling scripts', function () {
|
||||
runAndExpectError(
|
||||
it('should fail and suggest running backfilling scripts', async function () {
|
||||
await runAndExpectError(
|
||||
buildCheckTexLiveCmd({
|
||||
SANDBOXED_COMPILES: true,
|
||||
TEX_LIVE_DOCKER_IMAGE: TEST_TL_IMAGE,
|
||||
@@ -677,7 +653,7 @@ describe('ServerCEScripts', function () {
|
||||
describe('when TexLive ALL_TEX_LIVE_DOCKER_IMAGES are upgraded and used images are no longer available', function () {
|
||||
it('should suggest running a fixing script', async function () {
|
||||
await db.projects.updateMany({}, { $set: { imageName: TEST_TL_IMAGE } })
|
||||
runAndExpectError(
|
||||
await runAndExpectError(
|
||||
buildCheckTexLiveCmd({
|
||||
SANDBOXED_COMPILES: true,
|
||||
TEX_LIVE_DOCKER_IMAGE: 'tl-1',
|
||||
@@ -695,8 +671,8 @@ describe('ServerCEScripts', function () {
|
||||
await db.projects.updateMany({}, { $set: { imageName: TEST_TL_IMAGE } })
|
||||
})
|
||||
|
||||
it('should succeed when there are no changes to the TexLive images', function () {
|
||||
const output = run(
|
||||
it('should succeed when there are no changes to the TexLive images', async function () {
|
||||
const output = await run(
|
||||
buildCheckTexLiveCmd({
|
||||
SANDBOXED_COMPILES: true,
|
||||
TEX_LIVE_DOCKER_IMAGE: TEST_TL_IMAGE,
|
||||
@@ -706,8 +682,8 @@ describe('ServerCEScripts', function () {
|
||||
expect(output).to.include('Done.')
|
||||
})
|
||||
|
||||
it('should succeed when there are valid changes to the TexLive images', function () {
|
||||
const output = run(
|
||||
it('should succeed when there are valid changes to the TexLive images', async function () {
|
||||
const output = await run(
|
||||
buildCheckTexLiveCmd({
|
||||
SANDBOXED_COMPILES: true,
|
||||
TEX_LIVE_DOCKER_IMAGE: 'new-image',
|
||||
@@ -718,4 +694,51 @@ describe('ServerCEScripts', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('transfer-all-projects-to-user', function () {
|
||||
let fromUser, projects
|
||||
beforeEach(async function () {
|
||||
fromUser = new User()
|
||||
await fromUser.login()
|
||||
projects = await Promise.all([
|
||||
fromUser.createProject('a'),
|
||||
fromUser.createProject('b'),
|
||||
fromUser.createProject('c'),
|
||||
])
|
||||
})
|
||||
let toUser
|
||||
beforeEach(async function () {
|
||||
toUser = new User()
|
||||
await toUser.login()
|
||||
})
|
||||
|
||||
it('should log missing user', async function () {
|
||||
const email = 'does-not-exist@example.com'
|
||||
await runAndExpectError(
|
||||
`node modules/server-ce-scripts/scripts/transfer-all-projects-to-user.mjs --from-user=${email}`,
|
||||
[`user with email --from-user=${email} does not exist`]
|
||||
)
|
||||
})
|
||||
|
||||
it('should transfer projects by email', async function () {
|
||||
await run(
|
||||
`node modules/server-ce-scripts/scripts/transfer-all-projects-to-user.mjs --from-user=${fromUser.email} --to-user=${toUser.email}`
|
||||
)
|
||||
for (const projectId of projects) {
|
||||
expect(
|
||||
(await toUser.getProject(projectId)).owner_ref.toString()
|
||||
).to.equal(toUser._id.toString())
|
||||
}
|
||||
})
|
||||
it('should transfer projects by id', async function () {
|
||||
await run(
|
||||
`node modules/server-ce-scripts/scripts/transfer-all-projects-to-user.mjs --from-user=${fromUser._id} --to-user=${toUser._id}`
|
||||
)
|
||||
for (const projectId of projects) {
|
||||
expect(
|
||||
(await toUser.getProject(projectId)).owner_ref.toString()
|
||||
).to.equal(toUser._id.toString())
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -140,7 +140,7 @@ class AbstractMockApi {
|
||||
console.log('Starting mock on port', this.constructor.name, this.port)
|
||||
}
|
||||
this.server = this.app
|
||||
.listen(this.port, err => {
|
||||
.listen(this.port, '127.0.0.1', err => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
@@ -37,13 +37,14 @@ describe('OwnershipTransferHandler', function () {
|
||||
},
|
||||
}
|
||||
this.ProjectModel = {
|
||||
find: sinon.stub().resolves([]),
|
||||
updateOne: sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(),
|
||||
}),
|
||||
}
|
||||
this.UserGetter = {
|
||||
promises: {
|
||||
getUser: sinon.stub().resolves(this.user),
|
||||
getUser: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.TpdsUpdateSender = {
|
||||
@@ -72,12 +73,19 @@ describe('OwnershipTransferHandler', function () {
|
||||
addEntry: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.TagsHandler = {
|
||||
promises: {
|
||||
createTag: sinon.stub().resolves(),
|
||||
addProjectsToTag: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.handler = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'../Project/ProjectGetter': this.ProjectGetter,
|
||||
'../../models/Project': {
|
||||
Project: this.ProjectModel,
|
||||
},
|
||||
'../Tags/TagsHandler': this.TagsHandler,
|
||||
'../User/UserGetter': this.UserGetter,
|
||||
'../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
|
||||
'../Project/ProjectAuditLogHandler': this.ProjectAuditLogHandler,
|
||||
@@ -326,4 +334,161 @@ describe('OwnershipTransferHandler', function () {
|
||||
).to.be.rejectedWith(Errors.UserNotCollaboratorError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('transferAllProjectsToUser', function () {
|
||||
const fromUserEmail = 'user.one@example.com'
|
||||
const ipAddress = '1.2.3.4'
|
||||
let fromUserId, toUserId
|
||||
beforeEach(function () {
|
||||
fromUserId = new ObjectId().toString()
|
||||
toUserId = new ObjectId().toString()
|
||||
})
|
||||
|
||||
describe('with missing user', function () {
|
||||
it('should throw an error', async function () {
|
||||
this.UserGetter.promises.getUser.withArgs(fromUserId).resolves(null)
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(toUserId)
|
||||
.resolves({ _id: new ObjectId(toUserId) })
|
||||
await expect(
|
||||
this.handler.promises.transferAllProjectsToUser({
|
||||
toUserId,
|
||||
fromUserId,
|
||||
ipAddress,
|
||||
})
|
||||
).to.be.rejectedWith(/missing source user/)
|
||||
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(fromUserId)
|
||||
.resolves({ _id: new ObjectId(fromUserId), email: fromUserEmail })
|
||||
this.UserGetter.promises.getUser.withArgs(toUserId).resolves(null)
|
||||
await expect(
|
||||
this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
).to.be.rejectedWith(/missing destination user/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the same id', function () {
|
||||
it('should throw an error', async function () {
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(fromUserId)
|
||||
.resolves({ _id: new ObjectId(fromUserId), email: fromUserEmail })
|
||||
await expect(
|
||||
this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId: fromUserId,
|
||||
ipAddress,
|
||||
})
|
||||
).to.be.rejectedWith(/rejecting transfer between identical users/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('happy path', function () {
|
||||
let tag, fromUserEmail, projects
|
||||
|
||||
beforeEach(function () {
|
||||
tag = {
|
||||
_id: new ObjectId(),
|
||||
name: 'some-tag-name',
|
||||
}
|
||||
projects = [
|
||||
{ _id: 'project-1' },
|
||||
{ _id: 'project-2' },
|
||||
{ _id: 'project-3' },
|
||||
]
|
||||
|
||||
this.UserGetter.promises.getUser.withArgs(fromUserId).resolves({
|
||||
_id: new ObjectId(fromUserId),
|
||||
email: fromUserEmail,
|
||||
})
|
||||
this.UserGetter.promises.getUser.withArgs(toUserId).resolves({
|
||||
_id: new ObjectId(toUserId),
|
||||
})
|
||||
this.ProjectModel.find.resolves(projects)
|
||||
this.TagsHandler.promises.createTag.resolves({
|
||||
_id: tag._id,
|
||||
name: 'some-tag-name',
|
||||
})
|
||||
this.TagsHandler.promises.addProjectsToTag.resolves()
|
||||
})
|
||||
|
||||
it('creates a tag', async function () {
|
||||
await this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(this.TagsHandler.promises.createTag).to.have.been.calledWith(
|
||||
toUserId,
|
||||
`transferred-from-${fromUserEmail}`,
|
||||
'#434AF0',
|
||||
{ truncate: true }
|
||||
)
|
||||
})
|
||||
|
||||
it('returns a projectCount, and tag name', async function () {
|
||||
const result = await this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(result.projectCount).to.equal(projects.length)
|
||||
expect(result.newTagName).to.equal('some-tag-name')
|
||||
})
|
||||
|
||||
it('gets the user records', async function () {
|
||||
await this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(this.UserGetter.promises.getUser).to.have.been.calledWith(
|
||||
fromUserId
|
||||
)
|
||||
expect(this.UserGetter.promises.getUser).to.have.been.calledWith(
|
||||
toUserId
|
||||
)
|
||||
})
|
||||
|
||||
it('gets the list of affected projects', async function () {
|
||||
await this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(this.ProjectModel.find).to.have.been.calledWith({
|
||||
owner_ref: fromUserId,
|
||||
})
|
||||
})
|
||||
|
||||
it('transfers all of the projects', async function () {
|
||||
await this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
|
||||
expect(this.ProjectModel.updateOne.callCount).to.equal(3)
|
||||
expect(this.TagsHandler.promises.addProjectsToTag.callCount).to.equal(1)
|
||||
|
||||
for (const project of projects) {
|
||||
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: project._id },
|
||||
sinon.match({ $set: { owner_ref: toUserId } })
|
||||
)
|
||||
}
|
||||
expect(
|
||||
this.TagsHandler.promises.addProjectsToTag
|
||||
).to.have.been.calledWith(
|
||||
toUserId,
|
||||
tag._id,
|
||||
projects.map(p => p._id)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user