mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-22 13:38:08 +00:00
Merge pull request #2164 from overleaf/em-ownership-transfer
Project ownership transfer backend endpoint GitOrigin-RevId: b7d267f2c105e8f51d5013289ac4afeb077c1e21
This commit is contained in:
parent
acd926e2e0
commit
3ec74ac6f2
14 changed files with 867 additions and 777 deletions
|
@ -111,6 +111,33 @@ class SubscriptionAdminDeletionError extends OError {
|
|||
}
|
||||
}
|
||||
|
||||
class ProjectNotFoundError extends OError {
|
||||
constructor(options) {
|
||||
super({
|
||||
message: 'project not found',
|
||||
...options
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class UserNotFoundError extends OError {
|
||||
constructor(options) {
|
||||
super({
|
||||
message: 'user not found',
|
||||
...options
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class UserNotCollaboratorError extends OError {
|
||||
constructor(options) {
|
||||
super({
|
||||
message: 'user not a collaborator',
|
||||
...options
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
OError,
|
||||
BackwardCompatibleError,
|
||||
|
@ -134,5 +161,8 @@ module.exports = {
|
|||
SLInV2Error,
|
||||
ThirdPartyIdentityExistsError,
|
||||
ThirdPartyUserNotFoundError,
|
||||
SubscriptionAdminDeletionError
|
||||
SubscriptionAdminDeletionError,
|
||||
ProjectNotFoundError,
|
||||
UserNotFoundError,
|
||||
UserNotCollaboratorError
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -10,6 +10,7 @@ const ProjectTokenGenerator = require('./ProjectTokenGenerator')
|
|||
const ProjectEntityHandler = require('./ProjectEntityHandler')
|
||||
const ProjectHelper = require('./ProjectHelper')
|
||||
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
|
||||
const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
|
||||
const settings = require('settings-sharelatex')
|
||||
const { callbackify } = require('util')
|
||||
|
||||
|
@ -100,35 +101,40 @@ async function setProjectDescription(projectId, description) {
|
|||
}
|
||||
}
|
||||
|
||||
async function transferOwnership(projectId, userId, suffix = '') {
|
||||
async function transferOwnership(projectId, toUserId, options = {}) {
|
||||
const project = await ProjectGetter.promises.getProject(projectId, {
|
||||
owner_ref: true,
|
||||
name: true
|
||||
owner_ref: 1,
|
||||
collaberator_refs: 1
|
||||
})
|
||||
if (project == null) {
|
||||
throw new Errors.NotFoundError('project not found')
|
||||
throw new Errors.ProjectNotFoundError({ info: { projectId: projectId } })
|
||||
}
|
||||
if (project.owner_ref === userId) {
|
||||
const fromUserId = project.owner_ref
|
||||
if (fromUserId.equals(toUserId)) {
|
||||
return
|
||||
}
|
||||
const user = await UserGetter.promises.getUser(userId)
|
||||
if (user == null) {
|
||||
throw new Errors.NotFoundError('user not found')
|
||||
const toUser = await UserGetter.promises.getUser(toUserId)
|
||||
if (toUser == null) {
|
||||
throw new Errors.UserNotFoundError({ info: { userId: toUserId } })
|
||||
}
|
||||
|
||||
// we make sure the user to which the project is transferred is not a collaborator for the project,
|
||||
// this prevents any conflict during unique name generation
|
||||
await CollaboratorsHandler.promises.removeUserFromProject(projectId, userId)
|
||||
const name = await generateUniqueName(userId, project.name + suffix)
|
||||
const collaboratorIds = project.collaberator_refs || []
|
||||
if (
|
||||
!options.allowTransferToNonCollaborators &&
|
||||
!collaboratorIds.some(collaboratorId => collaboratorId.equals(toUser._id))
|
||||
) {
|
||||
throw new Errors.UserNotCollaboratorError({ info: { userId: toUserId } })
|
||||
}
|
||||
await CollaboratorsHandler.promises.removeUserFromProject(projectId, toUserId)
|
||||
await Project.update(
|
||||
{ _id: projectId },
|
||||
{
|
||||
$set: {
|
||||
owner_ref: userId,
|
||||
name
|
||||
}
|
||||
}
|
||||
{ $set: { owner_ref: toUserId } }
|
||||
).exec()
|
||||
await CollaboratorsHandler.promises.addUserIdToProject(
|
||||
projectId,
|
||||
toUserId,
|
||||
fromUserId,
|
||||
PrivilegeLevels.READ_AND_WRITE
|
||||
)
|
||||
await ProjectEntityHandler.promises.flushProjectToThirdPartyDataStore(
|
||||
projectId
|
||||
)
|
||||
|
|
|
@ -485,7 +485,11 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
|||
AuthorizationMiddleware.ensureUserCanAdminProject,
|
||||
ProjectController.renameProject
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
'/project/:Project_id/transfer-ownership',
|
||||
AuthorizationMiddleware.ensureUserCanAdminProject,
|
||||
ProjectController.transferOwnership
|
||||
)
|
||||
webRouter.get(
|
||||
'/project/:Project_id/updates',
|
||||
AuthorizationMiddleware.ensureUserCanReadProject,
|
||||
|
@ -598,7 +602,6 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
|||
AuthenticationController.requireLogin(),
|
||||
MetaController.broadcastMetadataForDoc
|
||||
)
|
||||
|
||||
privateApiRouter.post(
|
||||
'/internal/expire-deleted-projects-after-duration',
|
||||
AuthenticationController.httpAuth,
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"public": "./public"
|
||||
},
|
||||
"scripts": {
|
||||
"test:acceptance:run_dir": "mocha --recursive --reporter spec --timeout 25000 --exit --grep=$MOCHA_GREP $@",
|
||||
"test:acceptance:run_dir": "mocha --recursive --reporter spec --require test/acceptance/bootstrap.js --timeout 25000 --exit --grep=$MOCHA_GREP $@",
|
||||
"test:unit": "bin/unit_test --grep=$MOCHA_GREP $@",
|
||||
"test:unit:ci": "bin/unit_test --timeout 10000",
|
||||
"test:unit:app": "bin/unit_test_app $@",
|
||||
|
|
2
services/web/test/acceptance/bootstrap.js
vendored
Normal file
2
services/web/test/acceptance/bootstrap.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
const chai = require('chai')
|
||||
chai.use(require('chai-as-promised'))
|
|
@ -278,7 +278,7 @@ describe('Authorization', function() {
|
|||
if (err != null) {
|
||||
return cb(err)
|
||||
}
|
||||
this.site_admin.ensure_admin(cb)
|
||||
return this.site_admin.ensureAdmin(cb)
|
||||
})
|
||||
}
|
||||
],
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
const { expect } = require('chai')
|
||||
const User = require('./helpers/User').promises
|
||||
|
||||
describe('Project ownership transfer', function() {
|
||||
beforeEach(async function() {
|
||||
this.ownerSession = new User()
|
||||
this.collaboratorSession = new User()
|
||||
this.strangerSession = new User()
|
||||
this.adminSession = new User()
|
||||
await this.adminSession.ensureUserExists()
|
||||
await this.adminSession.ensureAdmin()
|
||||
await this.ownerSession.login()
|
||||
await this.collaboratorSession.login()
|
||||
await this.strangerSession.login()
|
||||
await this.adminSession.login()
|
||||
this.owner = await this.ownerSession.get()
|
||||
this.collaborator = await this.collaboratorSession.get()
|
||||
this.stranger = await this.strangerSession.get()
|
||||
this.admin = await this.adminSession.get()
|
||||
this.projectId = await this.ownerSession.createProject('Test project')
|
||||
await this.ownerSession.addUserToProject(
|
||||
this.projectId,
|
||||
this.collaborator,
|
||||
'readAndWrite'
|
||||
)
|
||||
})
|
||||
|
||||
describe('happy path', function() {
|
||||
beforeEach(async function() {
|
||||
await this.ownerSession.transferProjectOwnership(
|
||||
this.projectId,
|
||||
this.collaborator._id
|
||||
)
|
||||
})
|
||||
|
||||
it('changes the project owner', async function() {
|
||||
const project = await this.collaboratorSession.getProject(this.projectId)
|
||||
expect(project.owner_ref.toString()).to.equal(
|
||||
this.collaborator._id.toString()
|
||||
)
|
||||
})
|
||||
|
||||
it('adds the previous owner as a read/write collaborator', async function() {
|
||||
const project = await this.collaboratorSession.getProject(this.projectId)
|
||||
expect(project.collaberator_refs.map(x => x.toString())).to.have.members([
|
||||
this.owner._id.toString()
|
||||
])
|
||||
})
|
||||
|
||||
it('lets the new owner open the project', async function() {
|
||||
await this.collaboratorSession.openProject(this.projectId)
|
||||
})
|
||||
|
||||
it('lets the previous owner open the project', async function() {
|
||||
await this.ownerSession.openProject(this.projectId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validation', function() {
|
||||
it('lets only the project owner transfer ownership', async function() {
|
||||
await expect(
|
||||
this.collaboratorSession.transferProjectOwnership(
|
||||
this.projectId,
|
||||
this.collaborator._id
|
||||
)
|
||||
).to.be.rejectedWith('Unexpected status code: 403')
|
||||
})
|
||||
|
||||
it('prevents transfers to a non-collaborator', async function() {
|
||||
await expect(
|
||||
this.ownerSession.transferProjectOwnership(
|
||||
this.projectId,
|
||||
this.stranger._id
|
||||
)
|
||||
).to.be.rejectedWith('Unexpected status code: 403')
|
||||
})
|
||||
|
||||
it('allows an admin to transfer to any project to a non-collaborator', async function() {
|
||||
await expect(
|
||||
this.adminSession.transferProjectOwnership(
|
||||
this.projectId,
|
||||
this.stranger._id
|
||||
)
|
||||
).to.be.fulfilled
|
||||
})
|
||||
})
|
||||
})
|
|
@ -143,8 +143,8 @@ describe('Registration', function() {
|
|||
return (this.password = 'password11')
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
return this.user.full_delete_user(this.email)
|
||||
afterEach(function(done) {
|
||||
return this.user.fullDeleteUser(this.email, done)
|
||||
})
|
||||
|
||||
it('should register with the csrf token', function(done) {
|
||||
|
|
|
@ -128,7 +128,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
const url = `/metrics/institutions/${this.institution.v1Id}`
|
||||
async.series(
|
||||
[
|
||||
this.user.ensure_admin.bind(this.user),
|
||||
this.user.ensureAdmin.bind(this.user),
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -317,7 +317,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
async.series(
|
||||
[
|
||||
expectAccess(this.user, url, 403),
|
||||
this.user.ensure_admin.bind(this.user),
|
||||
this.user.ensureAdmin.bind(this.user),
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -329,7 +329,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
const url = '/metrics/templates/789'
|
||||
async.series(
|
||||
[
|
||||
this.user.ensure_admin.bind(this.user),
|
||||
this.user.ensureAdmin.bind(this.user),
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 404)
|
||||
],
|
||||
|
@ -345,7 +345,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
[
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 403),
|
||||
this.user.ensure_admin.bind(this.user),
|
||||
this.user.ensureAdmin.bind(this.user),
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, url, 200)
|
||||
],
|
||||
|
@ -382,7 +382,7 @@ describe('UserMembershipAuthorization', function() {
|
|||
it('should allow admin users', function(done) {
|
||||
async.series(
|
||||
[
|
||||
this.user.ensure_admin.bind(this.user),
|
||||
this.user.ensureAdmin.bind(this.user),
|
||||
this.user.login.bind(this.user),
|
||||
expectAccess(this.user, '/metrics/admin', 200)
|
||||
],
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('ThirdPartyIdentityManager', function() {
|
|||
})
|
||||
|
||||
afterEach(function(done) {
|
||||
return this.user.full_delete_user(this.user.email, done)
|
||||
return this.user.fullDeleteUser(this.user.email, done)
|
||||
})
|
||||
|
||||
describe('login', function() {
|
||||
|
@ -175,7 +175,7 @@ describe('ThirdPartyIdentityManager', function() {
|
|||
// return done()
|
||||
// }
|
||||
// )
|
||||
// this.user2.full_delete_user(this.user2.email, done)
|
||||
// this.user2.fullDeleteUser(this.user2.email, done)
|
||||
// })
|
||||
// })
|
||||
})
|
||||
|
|
|
@ -1,22 +1,4 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
handle-callback-err,
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-undef,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const request = require('./request')
|
||||
const _ = require('underscore')
|
||||
const settings = require('settings-sharelatex')
|
||||
const { db, ObjectId } = require('../../../../app/src/infrastructure/mongojs')
|
||||
const UserModel = require('../../../../app/src/models/User').User
|
||||
|
@ -53,42 +35,30 @@ class User {
|
|||
this.id = user._id.toString()
|
||||
this._id = user._id.toString()
|
||||
this.first_name = user.first_name
|
||||
return (this.referal_id = user.referal_id)
|
||||
this.referal_id = user.referal_id
|
||||
}
|
||||
|
||||
get(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, user) {}
|
||||
}
|
||||
return db.users.findOne({ _id: ObjectId(this._id) }, callback)
|
||||
db.users.findOne({ _id: ObjectId(this._id) }, callback)
|
||||
}
|
||||
|
||||
mongoUpdate(updateOp, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return db.users.update({ _id: ObjectId(this._id) }, updateOp, callback)
|
||||
db.users.update({ _id: ObjectId(this._id) }, updateOp, callback)
|
||||
}
|
||||
|
||||
register(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, user) {}
|
||||
}
|
||||
return this.registerWithQuery('', callback)
|
||||
this.registerWithQuery('', callback)
|
||||
}
|
||||
|
||||
registerWithQuery(query, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, user) {}
|
||||
}
|
||||
if (this._id != null) {
|
||||
return callback(new Error('User already registered'))
|
||||
}
|
||||
return this.getCsrfToken(error => {
|
||||
this.getCsrfToken(error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return this.request.post(
|
||||
this.request.post(
|
||||
{
|
||||
url: `/register${query}`,
|
||||
json: { email: this.email, password: this.password }
|
||||
|
@ -97,12 +67,12 @@ class User {
|
|||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return db.users.findOne({ email: this.email }, (error, user) => {
|
||||
db.users.findOne({ email: this.email }, (error, user) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
this.setExtraAttributes(user)
|
||||
return callback(null, user)
|
||||
callback(null, user)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -110,25 +80,19 @@ class User {
|
|||
}
|
||||
|
||||
login(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return this.loginWith(this.email, callback)
|
||||
this.loginWith(this.email, callback)
|
||||
}
|
||||
|
||||
loginWith(email, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return this.ensureUserExists(error => {
|
||||
this.ensureUserExists(error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return this.getCsrfToken(error => {
|
||||
this.getCsrfToken(error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return this.request.post(
|
||||
this.request.post(
|
||||
{
|
||||
url: settings.enableLegacyLogin ? '/login/legacy' : '/login',
|
||||
json: { email, password: this.password }
|
||||
|
@ -140,23 +104,20 @@ class User {
|
|||
}
|
||||
|
||||
ensureUserExists(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
const filter = { email: this.email }
|
||||
const options = { upsert: true, new: true, setDefaultsOnInsert: true }
|
||||
return UserModel.findOneAndUpdate(filter, {}, options, (error, user) => {
|
||||
UserModel.findOneAndUpdate(filter, {}, options, (error, user) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return AuthenticationManager.setUserPasswordInV2(
|
||||
AuthenticationManager.setUserPasswordInV2(
|
||||
user._id,
|
||||
this.password,
|
||||
error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return UserUpdater.updateUser(
|
||||
UserUpdater.updateUser(
|
||||
user._id,
|
||||
{ $set: { emails: this.emails } },
|
||||
error => {
|
||||
|
@ -164,7 +125,7 @@ class User {
|
|||
return callback(error)
|
||||
}
|
||||
this.setExtraAttributes(user)
|
||||
return callback(null, this.password)
|
||||
callback(null, this.password)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -173,37 +134,24 @@ class User {
|
|||
}
|
||||
|
||||
setFeatures(features, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
const update = {}
|
||||
for (let key in features) {
|
||||
const value = features[key]
|
||||
update[`features.${key}`] = value
|
||||
}
|
||||
return UserModel.update({ _id: this.id }, update, callback)
|
||||
UserModel.update({ _id: this.id }, update, callback)
|
||||
}
|
||||
|
||||
setOverleafId(overleaf_id, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return UserModel.update(
|
||||
{ _id: this.id },
|
||||
{ 'overleaf.id': overleaf_id },
|
||||
callback
|
||||
)
|
||||
setOverleafId(overleafId, callback) {
|
||||
UserModel.update({ _id: this.id }, { 'overleaf.id': overleafId }, callback)
|
||||
}
|
||||
|
||||
logout(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return this.getCsrfToken(error => {
|
||||
this.getCsrfToken(error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return this.request.post(
|
||||
this.request.post(
|
||||
{
|
||||
url: '/logout',
|
||||
json: {
|
||||
|
@ -215,17 +163,16 @@ class User {
|
|||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return db.users.findOne({ email: this.email }, (error, user) => {
|
||||
db.users.findOne({ email: this.email }, (error, user) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
this.id = __guard__(user != null ? user._id : undefined, x =>
|
||||
x.toString()
|
||||
)
|
||||
this._id = __guard__(user != null ? user._id : undefined, x1 =>
|
||||
x1.toString()
|
||||
)
|
||||
return callback()
|
||||
if (user == null) {
|
||||
return callback()
|
||||
}
|
||||
this.id = user._id.toString()
|
||||
this._id = user._id.toString()
|
||||
callback()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -233,31 +180,22 @@ class User {
|
|||
}
|
||||
|
||||
addEmail(email, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
this.emails.push({ email, createdAt: new Date() })
|
||||
return UserUpdater.addEmailAddress(this.id, email, callback)
|
||||
UserUpdater.addEmailAddress(this.id, email, callback)
|
||||
}
|
||||
|
||||
confirmEmail(email, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
for (let idx = 0; idx < this.emails.length; idx++) {
|
||||
const emailData = this.emails[idx]
|
||||
if (emailData.email === email) {
|
||||
this.emails[idx].confirmedAt = new Date()
|
||||
}
|
||||
}
|
||||
return UserUpdater.confirmEmail(this.id, email, callback)
|
||||
UserUpdater.confirmEmail(this.id, email, callback)
|
||||
}
|
||||
|
||||
ensure_admin(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return db.users.update(
|
||||
ensureAdmin(callback) {
|
||||
db.users.update(
|
||||
{ _id: ObjectId(this.id) },
|
||||
{ $set: { isAdmin: true } },
|
||||
callback
|
||||
|
@ -267,13 +205,10 @@ class User {
|
|||
ensureStaffAccess(flag, callback) {
|
||||
const update = { $set: {} }
|
||||
update.$set[`staffAccess.${flag}`] = true
|
||||
return db.users.update({ _id: ObjectId(this.id) }, update, callback)
|
||||
db.users.update({ _id: ObjectId(this.id) }, update, callback)
|
||||
}
|
||||
|
||||
upgradeFeatures(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
const features = {
|
||||
collaborators: -1, // Infinite
|
||||
versioning: true,
|
||||
|
@ -285,7 +220,7 @@ class User {
|
|||
trackChanges: true,
|
||||
trackChangesVisible: true
|
||||
}
|
||||
return db.users.update(
|
||||
db.users.update(
|
||||
{ _id: ObjectId(this.id) },
|
||||
{ $set: { features } },
|
||||
callback
|
||||
|
@ -293,9 +228,6 @@ class User {
|
|||
}
|
||||
|
||||
downgradeFeatures(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
const features = {
|
||||
collaborators: 1,
|
||||
versioning: false,
|
||||
|
@ -307,7 +239,7 @@ class User {
|
|||
trackChanges: false,
|
||||
trackChangesVisible: false
|
||||
}
|
||||
return db.users.update(
|
||||
db.users.update(
|
||||
{ _id: ObjectId(this.id) },
|
||||
{ $set: { features } },
|
||||
callback
|
||||
|
@ -315,11 +247,8 @@ class User {
|
|||
}
|
||||
|
||||
defaultFeatures(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
const features = settings.defaultFeatures
|
||||
return db.users.update(
|
||||
db.users.update(
|
||||
{ _id: ObjectId(this.id) },
|
||||
{ $set: { features } },
|
||||
callback
|
||||
|
@ -327,31 +256,30 @@ class User {
|
|||
}
|
||||
|
||||
getFeatures(callback) {
|
||||
const features = settings.defaultFeatures
|
||||
return db.users.findOne(
|
||||
db.users.findOne(
|
||||
{ _id: ObjectId(this.id) },
|
||||
{ features: 1 },
|
||||
(error, user) => callback(error, user && user.features)
|
||||
)
|
||||
}
|
||||
|
||||
full_delete_user(email, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return db.users.findOne({ email }, (error, user) => {
|
||||
fullDeleteUser(email, callback) {
|
||||
db.users.findOne({ email }, (error, user) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
if (user == null) {
|
||||
return callback()
|
||||
}
|
||||
const user_id = user._id
|
||||
return db.projects.remove(
|
||||
{ owner_ref: ObjectId(user_id) },
|
||||
const userId = user._id
|
||||
db.projects.remove(
|
||||
{ owner_ref: ObjectId(userId) },
|
||||
{ multi: true },
|
||||
err => {
|
||||
if (err != null) {
|
||||
callback(err)
|
||||
}
|
||||
return db.users.remove({ _id: ObjectId(user_id) }, callback)
|
||||
db.users.remove({ _id: ObjectId(userId) }, callback)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -383,33 +311,21 @@ class User {
|
|||
})
|
||||
}
|
||||
|
||||
getProject(project_id, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, project) {}
|
||||
}
|
||||
return db.projects.findOne(
|
||||
{ _id: ObjectId(project_id.toString()) },
|
||||
callback
|
||||
)
|
||||
getProject(projectId, callback) {
|
||||
db.projects.findOne({ _id: ObjectId(projectId.toString()) }, callback)
|
||||
}
|
||||
|
||||
saveProject(project, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return db.projects.update({ _id: project._id }, project, callback)
|
||||
db.projects.update({ _id: project._id }, project, callback)
|
||||
}
|
||||
|
||||
createProject(name, options, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, oroject_id) {}
|
||||
}
|
||||
if (typeof options === 'function') {
|
||||
callback = options
|
||||
options = {}
|
||||
}
|
||||
|
||||
return this.request.post(
|
||||
this.request.post(
|
||||
{
|
||||
url: '/project/new',
|
||||
json: Object.assign({ projectName: name }, options)
|
||||
|
@ -429,49 +345,38 @@ class User {
|
|||
body
|
||||
])
|
||||
)
|
||||
return callback(error)
|
||||
callback(error)
|
||||
} else {
|
||||
return callback(null, body.project_id)
|
||||
callback(null, body.project_id)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
deleteProject(project_id, callback) {
|
||||
if (callback == null) {
|
||||
callback = error
|
||||
}
|
||||
return this.request.delete(
|
||||
deleteProject(projectId, callback) {
|
||||
this.request.delete(
|
||||
{
|
||||
url: `/project/${project_id}?forever=true`
|
||||
url: `/project/${projectId}?forever=true`
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return callback(null)
|
||||
callback(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
deleteProjects(callback) {
|
||||
if (callback == null) {
|
||||
callback = error
|
||||
}
|
||||
return db.projects.remove(
|
||||
{ owner_ref: ObjectId(this.id) },
|
||||
{ multi: true },
|
||||
err => callback(err)
|
||||
db.projects.remove({ owner_ref: ObjectId(this.id) }, { multi: true }, err =>
|
||||
callback(err)
|
||||
)
|
||||
}
|
||||
|
||||
openProject(project_id, callback) {
|
||||
if (callback == null) {
|
||||
callback = error
|
||||
}
|
||||
return this.request.get(
|
||||
openProject(projectId, callback) {
|
||||
this.request.get(
|
||||
{
|
||||
url: `/project/${project_id}`
|
||||
url: `/project/${projectId}`
|
||||
},
|
||||
(error, response, body) => {
|
||||
if (error != null) {
|
||||
|
@ -483,56 +388,50 @@ class User {
|
|||
)
|
||||
return callback(err)
|
||||
}
|
||||
return callback(null)
|
||||
callback(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
createDocInProject(project_id, parent_folder_id, name, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, doc_id) {}
|
||||
}
|
||||
return this.getCsrfToken(error => {
|
||||
createDocInProject(projectId, parentFolderId, name, callback) {
|
||||
this.getCsrfToken(error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return this.request.post(
|
||||
this.request.post(
|
||||
{
|
||||
url: `/project/${project_id}/doc`,
|
||||
url: `/project/${projectId}/doc`,
|
||||
json: {
|
||||
name,
|
||||
parent_folder_id
|
||||
parentFolderId
|
||||
}
|
||||
},
|
||||
(error, response, body) => {
|
||||
return callback(null, body._id)
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
callback(null, body._id)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
addUserToProject(project_id, user, privileges, callback) {
|
||||
addUserToProject(projectId, user, privileges, callback) {
|
||||
let updateOp
|
||||
if (callback == null) {
|
||||
callback = function(error, user) {}
|
||||
}
|
||||
if (privileges === 'readAndWrite') {
|
||||
updateOp = { $addToSet: { collaberator_refs: user._id.toString() } }
|
||||
updateOp = { $addToSet: { collaberator_refs: user._id } }
|
||||
} else if (privileges === 'readOnly') {
|
||||
updateOp = { $addToSet: { readOnly_refs: user._id.toString() } }
|
||||
updateOp = { $addToSet: { readOnly_refs: user._id } }
|
||||
}
|
||||
return db.projects.update({ _id: db.ObjectId(project_id) }, updateOp, err =>
|
||||
db.projects.update({ _id: db.ObjectId(projectId) }, updateOp, err =>
|
||||
callback(err)
|
||||
)
|
||||
}
|
||||
|
||||
makePublic(project_id, level, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return this.request.post(
|
||||
makePublic(projectId, level, callback) {
|
||||
this.request.post(
|
||||
{
|
||||
url: `/project/${project_id}/settings/admin`,
|
||||
url: `/project/${projectId}/settings/admin`,
|
||||
json: {
|
||||
publicAccessLevel: level
|
||||
}
|
||||
|
@ -541,18 +440,15 @@ class User {
|
|||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return callback(null)
|
||||
callback(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
makePrivate(project_id, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return this.request.post(
|
||||
makePrivate(projectId, callback) {
|
||||
this.request.post(
|
||||
{
|
||||
url: `/project/${project_id}/settings/admin`,
|
||||
url: `/project/${projectId}/settings/admin`,
|
||||
json: {
|
||||
publicAccessLevel: 'private'
|
||||
}
|
||||
|
@ -561,18 +457,15 @@ class User {
|
|||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return callback(null)
|
||||
callback(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
makeTokenBased(project_id, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return this.request.post(
|
||||
makeTokenBased(projectId, callback) {
|
||||
this.request.post(
|
||||
{
|
||||
url: `/project/${project_id}/settings/admin`,
|
||||
url: `/project/${projectId}/settings/admin`,
|
||||
json: {
|
||||
publicAccessLevel: 'tokenBased'
|
||||
}
|
||||
|
@ -581,16 +474,13 @@ class User {
|
|||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return callback(null)
|
||||
callback(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
getCsrfToken(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return this.request.get(
|
||||
this.request.get(
|
||||
{
|
||||
url: '/dev/csrf'
|
||||
},
|
||||
|
@ -604,20 +494,17 @@ class User {
|
|||
'x-csrf-token': this.csrfToken
|
||||
}
|
||||
})
|
||||
return callback()
|
||||
callback()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
changePassword(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return this.getCsrfToken(error => {
|
||||
this.getCsrfToken(error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return this.request.post(
|
||||
this.request.post(
|
||||
{
|
||||
url: '/user/password/update',
|
||||
json: {
|
||||
|
@ -630,48 +517,42 @@ class User {
|
|||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return db.users.findOne({ email: this.email }, (error, user) => {
|
||||
db.users.findOne({ email: this.email }, (error, user) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return callback()
|
||||
callback()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
reconfirmAccountRequest(user_email, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return this.getCsrfToken(error => {
|
||||
reconfirmAccountRequest(userEmail, callback) {
|
||||
this.getCsrfToken(error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return this.request.post(
|
||||
this.request.post(
|
||||
{
|
||||
url: '/user/reconfirm',
|
||||
json: {
|
||||
email: user_email
|
||||
email: userEmail
|
||||
}
|
||||
},
|
||||
(error, response, body) => {
|
||||
return callback(error, response)
|
||||
callback(error, response)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
getUserSettingsPage(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, statusCode) {}
|
||||
}
|
||||
return this.getCsrfToken(error => {
|
||||
this.getCsrfToken(error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return this.request.get(
|
||||
this.request.get(
|
||||
{
|
||||
url: '/user/settings'
|
||||
},
|
||||
|
@ -679,21 +560,18 @@ class User {
|
|||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return callback(null, response.statusCode)
|
||||
callback(null, response.statusCode)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
activateSudoMode(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error) {}
|
||||
}
|
||||
return this.getCsrfToken(error => {
|
||||
this.getCsrfToken(error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return this.request.post(
|
||||
this.request.post(
|
||||
{
|
||||
uri: '/confirm-password',
|
||||
json: {
|
||||
|
@ -706,14 +584,11 @@ class User {
|
|||
}
|
||||
|
||||
updateSettings(newSettings, callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, response, body) {}
|
||||
}
|
||||
return this.getCsrfToken(error => {
|
||||
this.getCsrfToken(error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return this.request.post(
|
||||
this.request.post(
|
||||
{
|
||||
url: '/user/settings',
|
||||
json: newSettings
|
||||
|
@ -724,14 +599,11 @@ class User {
|
|||
}
|
||||
|
||||
getProjectListPage(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, statusCode) {}
|
||||
}
|
||||
return this.getCsrfToken(error => {
|
||||
this.getCsrfToken(error => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return this.request.get(
|
||||
this.request.get(
|
||||
{
|
||||
url: '/project'
|
||||
},
|
||||
|
@ -739,26 +611,23 @@ class User {
|
|||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
return callback(null, response.statusCode)
|
||||
callback(null, response.statusCode)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
isLoggedIn(callback) {
|
||||
if (callback == null) {
|
||||
callback = function(error, loggedIn) {}
|
||||
}
|
||||
return this.request.get('/user/personal_info', (error, response, body) => {
|
||||
this.request.get('/user/personal_info', (error, response, body) => {
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
if (response.statusCode === 200) {
|
||||
return callback(null, true)
|
||||
callback(null, true)
|
||||
} else if (response.statusCode === 302) {
|
||||
return callback(null, false)
|
||||
callback(null, false)
|
||||
} else {
|
||||
return callback(
|
||||
callback(
|
||||
new Error(
|
||||
`unexpected status code from /user/personal_info: ${
|
||||
response.statusCode
|
||||
|
@ -769,8 +638,35 @@ class User {
|
|||
})
|
||||
}
|
||||
|
||||
transferProjectOwnership(projectId, userId, callback) {
|
||||
this.getCsrfToken(err => {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
this.request.post(
|
||||
{
|
||||
url: `/project/${projectId.toString()}/transfer-ownership`,
|
||||
json: {
|
||||
user_id: userId.toString()
|
||||
}
|
||||
},
|
||||
(err, response) => {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
if (response.statusCode !== 204) {
|
||||
return callback(
|
||||
new Error(`Unexpected status code: ${response.statusCode}`)
|
||||
)
|
||||
}
|
||||
callback()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
setV1Id(v1Id, callback) {
|
||||
return UserModel.update(
|
||||
UserModel.update(
|
||||
{
|
||||
_id: this._id
|
||||
},
|
||||
|
@ -809,9 +705,3 @@ Object.getOwnPropertyNames(User.prototype).forEach(methodName => {
|
|||
})
|
||||
|
||||
module.exports = User
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
||||
|
|
|
@ -1,33 +1,22 @@
|
|||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const should = require('chai').should()
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const assert = require('assert')
|
||||
const path = require('path')
|
||||
const sinon = require('sinon')
|
||||
const modulePath = path.join(
|
||||
const { expect } = require('chai')
|
||||
const HttpErrors = require('@overleaf/o-error/http')
|
||||
const { ObjectId } = require('mongodb')
|
||||
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
||||
|
||||
const MODULE_PATH = path.join(
|
||||
__dirname,
|
||||
'../../../../app/src/Features/Project/ProjectController'
|
||||
)
|
||||
const { expect } = require('chai')
|
||||
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
||||
|
||||
describe('ProjectController', function() {
|
||||
beforeEach(function() {
|
||||
this.project_id = '123213jlkj9kdlsaj'
|
||||
this.project_id = ObjectId('abcdefabcdefabcdefabcdef')
|
||||
|
||||
this.user = {
|
||||
_id: '588f3ddae8ebc1bac07c9fa4',
|
||||
_id: ObjectId('123456123456123456123456'),
|
||||
first_name: 'bjkdsjfk',
|
||||
features: {}
|
||||
}
|
||||
|
@ -79,6 +68,9 @@ describe('ProjectController', function() {
|
|||
findAllUsersProjects: sinon.stub(),
|
||||
getProject: sinon.stub()
|
||||
}
|
||||
this.ProjectDetailsHandler = {
|
||||
transferOwnership: sinon.stub().yields()
|
||||
}
|
||||
this.ProjectHelper = {
|
||||
isArchived: sinon.stub(),
|
||||
isTrashed: sinon.stub(),
|
||||
|
@ -121,7 +113,7 @@ describe('ProjectController', function() {
|
|||
}
|
||||
this.getUserAffiliations = sinon.stub().callsArgWith(1, null, [])
|
||||
|
||||
this.ProjectController = SandboxedModule.require(modulePath, {
|
||||
this.ProjectController = SandboxedModule.require(MODULE_PATH, {
|
||||
globals: {
|
||||
console: console
|
||||
},
|
||||
|
@ -137,6 +129,7 @@ describe('ProjectController', function() {
|
|||
},
|
||||
inc() {}
|
||||
},
|
||||
'@overleaf/o-error/http': HttpErrors,
|
||||
'./ProjectDeleter': this.ProjectDeleter,
|
||||
'./ProjectDuplicator': this.ProjectDuplicator,
|
||||
'./ProjectCreationHandler': this.ProjectCreationHandler,
|
||||
|
@ -155,6 +148,7 @@ describe('ProjectController', function() {
|
|||
'../ReferencesSearch/ReferencesSearchHandler': this
|
||||
.ReferencesSearchHandler,
|
||||
'./ProjectGetter': this.ProjectGetter,
|
||||
'./ProjectDetailsHandler': this.ProjectDetailsHandler,
|
||||
'../Authentication/AuthenticationController': this
|
||||
.AuthenticationController,
|
||||
'../Analytics/AnalyticsManager': this.AnalyticsManager,
|
||||
|
@ -196,12 +190,12 @@ describe('ProjectController', function() {
|
|||
},
|
||||
ip: '192.170.18.1'
|
||||
}
|
||||
return (this.res = {
|
||||
this.res = {
|
||||
locals: {
|
||||
jsPath: 'js path here'
|
||||
},
|
||||
setTimeout: sinon.stub()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('updateProjectSettings', function() {
|
||||
|
@ -213,9 +207,9 @@ describe('ProjectController', function() {
|
|||
.calledWith(this.project_id, this.name)
|
||||
.should.equal(true)
|
||||
code.should.equal(204)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.updateProjectSettings(this.req, this.res)
|
||||
this.ProjectController.updateProjectSettings(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should update the compiler', function(done) {
|
||||
|
@ -226,9 +220,9 @@ describe('ProjectController', function() {
|
|||
.calledWith(this.project_id, this.compiler)
|
||||
.should.equal(true)
|
||||
code.should.equal(204)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.updateProjectSettings(this.req, this.res)
|
||||
this.ProjectController.updateProjectSettings(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should update the imageName', function(done) {
|
||||
|
@ -239,9 +233,9 @@ describe('ProjectController', function() {
|
|||
.calledWith(this.project_id, this.imageName)
|
||||
.should.equal(true)
|
||||
code.should.equal(204)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.updateProjectSettings(this.req, this.res)
|
||||
this.ProjectController.updateProjectSettings(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should update the spell check language', function(done) {
|
||||
|
@ -252,9 +246,9 @@ describe('ProjectController', function() {
|
|||
.calledWith(this.project_id, this.languageCode)
|
||||
.should.equal(true)
|
||||
code.should.equal(204)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.updateProjectSettings(this.req, this.res)
|
||||
this.ProjectController.updateProjectSettings(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should update the root doc', function(done) {
|
||||
|
@ -265,9 +259,9 @@ describe('ProjectController', function() {
|
|||
.calledWith(this.project_id, this.rootDocId)
|
||||
.should.equal(true)
|
||||
code.should.equal(204)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.updateProjectSettings(this.req, this.res)
|
||||
this.ProjectController.updateProjectSettings(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -282,12 +276,9 @@ describe('ProjectController', function() {
|
|||
.calledWith(this.project_id, this.publicAccessLevel)
|
||||
.should.equal(true)
|
||||
code.should.equal(204)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.updateProjectAdminSettings(
|
||||
this.req,
|
||||
this.res
|
||||
)
|
||||
this.ProjectController.updateProjectAdminSettings(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -298,9 +289,9 @@ describe('ProjectController', function() {
|
|||
.calledWith(this.project_id)
|
||||
.should.equal(true)
|
||||
code.should.equal(200)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.deleteProject(this.req, this.res)
|
||||
this.ProjectController.deleteProject(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should tell the project deleter to delete when forever=true', function(done) {
|
||||
|
@ -313,9 +304,9 @@ describe('ProjectController', function() {
|
|||
})
|
||||
.should.equal(true)
|
||||
code.should.equal(200)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.deleteProject(this.req, this.res)
|
||||
this.ProjectController.deleteProject(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -326,9 +317,9 @@ describe('ProjectController', function() {
|
|||
.calledWith(this.project_id)
|
||||
.should.equal(true)
|
||||
code.should.equal(200)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.restoreProject(this.req, this.res)
|
||||
this.ProjectController.restoreProject(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -339,9 +330,9 @@ describe('ProjectController', function() {
|
|||
.calledWith(this.user, this.project_id, this.projectName)
|
||||
.should.equal(true)
|
||||
json.project_id.should.equal(this.project_id)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.cloneProject(this.req, this.res)
|
||||
this.ProjectController.cloneProject(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -355,9 +346,9 @@ describe('ProjectController', function() {
|
|||
this.ProjectCreationHandler.createBasicProject.called.should.equal(
|
||||
false
|
||||
)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.newProject(this.req, this.res)
|
||||
this.ProjectController.newProject(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should call the projectCreationHandler with createBasicProject', function(done) {
|
||||
|
@ -369,9 +360,9 @@ describe('ProjectController', function() {
|
|||
this.ProjectCreationHandler.createBasicProject
|
||||
.calledWith(this.user._id, this.projectName)
|
||||
.should.equal(true)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.newProject(this.req, this.res)
|
||||
this.ProjectController.newProject(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -411,10 +402,10 @@ describe('ProjectController', function() {
|
|||
}
|
||||
this.users[this.user._id] = this.user // Owner
|
||||
this.UserModel.findById = (id, fields, callback) => {
|
||||
return callback(null, this.users[id])
|
||||
callback(null, this.users[id])
|
||||
}
|
||||
this.UserGetter.getUserOrUserStubById = (id, fields, callback) => {
|
||||
return callback(null, this.users[id])
|
||||
callback(null, this.users[id])
|
||||
}
|
||||
|
||||
this.LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false)
|
||||
|
@ -427,7 +418,7 @@ describe('ProjectController', function() {
|
|||
null,
|
||||
this.allProjects
|
||||
)
|
||||
return this.Modules.hooks.fire
|
||||
this.Modules.hooks.fire
|
||||
.withArgs('findAllV1Projects', this.user._id)
|
||||
.yields(undefined)
|
||||
}) // Without integration module hook, cb returns undefined
|
||||
|
@ -435,26 +426,26 @@ describe('ProjectController', function() {
|
|||
it('should render the project/list page', function(done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
pageName.should.equal('project/list')
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send the tags', function(done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.tags.length.should.equal(this.tags.length)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should create trigger ip matcher notifications', function(done) {
|
||||
this.settings.overleaf = true
|
||||
this.res.render = (pageName, opts) => {
|
||||
this.NotificationBuilder.ipMatcherAffiliation.called.should.equal(true)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send the projects', function(done) {
|
||||
|
@ -466,17 +457,17 @@ describe('ProjectController', function() {
|
|||
this.tokenReadAndWrite.length +
|
||||
this.tokenReadOnly.length
|
||||
)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send the user', function(done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.user.should.deep.equal(this.user)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should inject the users', function(done) {
|
||||
|
@ -490,17 +481,17 @@ describe('ProjectController', function() {
|
|||
opts.projects[1].lastUpdatedBy.should.equal(
|
||||
this.users[this.projects[1].lastUpdatedBy]
|
||||
)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send hasSubscription == false when no subscription', function(done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.hasSubscription.should.equal(false)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send hasSubscription == true when there is a subscription', function(done) {
|
||||
|
@ -509,16 +500,16 @@ describe('ProjectController', function() {
|
|||
.callsArgWith(1, null, true)
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.hasSubscription.should.equal(true)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
describe('front widget', function(done) {
|
||||
beforeEach(function() {
|
||||
return (this.settings.overleaf = {
|
||||
this.settings.overleaf = {
|
||||
front_chat_widget_room_id: 'chat-room-id'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('should show for paid users', function(done) {
|
||||
|
@ -528,29 +519,29 @@ describe('ProjectController', function() {
|
|||
opts.frontChatWidgetRoomId.should.equal(
|
||||
this.settings.overleaf.front_chat_widget_room_id
|
||||
)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should show for sample users', function(done) {
|
||||
this.user._id = '588f3ddae8ebc1bac07c9f00' // last two digits
|
||||
this.user._id = ObjectId('588f3ddae8ebc1bac07c9f00') // last two digits
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.frontChatWidgetRoomId.should.equal(
|
||||
this.settings.overleaf.front_chat_widget_room_id
|
||||
)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should not show for non sample users', function(done) {
|
||||
this.user._id = '588f3ddae8ebc1bac07c9fff' // last two digits
|
||||
this.user._id = ObjectId('588f3ddae8ebc1bac07c9fff') // last two digits
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.frontChatWidgetRoomId).to.equal(undefined)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -579,7 +570,7 @@ describe('ProjectController', function() {
|
|||
],
|
||||
tags: [{ name: 'mock tag', project_ids: ['123mockV1Id'] }]
|
||||
}
|
||||
return this.Modules.hooks.fire
|
||||
this.Modules.hooks.fire
|
||||
.withArgs('findAllV1Projects', this.user._id)
|
||||
.yields(null, [this.V1Response])
|
||||
}) // Need to wrap response in array, as multiple hooks could fire
|
||||
|
@ -600,11 +591,11 @@ describe('ProjectController', function() {
|
|||
expect(p).to.have.property('name')
|
||||
expect(p).to.have.property('lastUpdated')
|
||||
expect(p).to.have.property('accessLevel')
|
||||
return expect(p).to.have.property('archived')
|
||||
expect(p).to.have.property('archived')
|
||||
})
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should include V1 tags', function(done) {
|
||||
|
@ -614,19 +605,19 @@ describe('ProjectController', function() {
|
|||
)
|
||||
opts.tags.forEach(t => {
|
||||
expect(t).to.have.property('name')
|
||||
return expect(t).to.have.property('project_ids')
|
||||
expect(t).to.have.property('project_ids')
|
||||
})
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should have isShowingV1Projects flag', function(done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.isShowingV1Projects.should.equal(true)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -670,7 +661,7 @@ describe('ProjectController', function() {
|
|||
}
|
||||
this.users[this.user._id] = this.user // Owner
|
||||
this.UserModel.findById = (id, fields, callback) => {
|
||||
return callback(null, this.users[id])
|
||||
callback(null, this.users[id])
|
||||
}
|
||||
|
||||
this.LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false)
|
||||
|
@ -683,7 +674,7 @@ describe('ProjectController', function() {
|
|||
null,
|
||||
this.allProjects
|
||||
)
|
||||
return this.Modules.hooks.fire
|
||||
this.Modules.hooks.fire
|
||||
.withArgs('findAllV1Projects', this.user._id)
|
||||
.yields(undefined)
|
||||
}) // Without integration module hook, cb returns undefined
|
||||
|
@ -691,9 +682,9 @@ describe('ProjectController', function() {
|
|||
it('should render the project/list page', function(done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
pageName.should.equal('project/list')
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should omit one of the projects', function(done) {
|
||||
|
@ -706,16 +697,16 @@ describe('ProjectController', function() {
|
|||
this.tokenReadOnly.length -
|
||||
1
|
||||
)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectListPage(this.req, this.res)
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('renameProject', function() {
|
||||
beforeEach(function() {
|
||||
this.newProjectName = 'my supper great new project'
|
||||
return (this.req.body.newProjectName = this.newProjectName)
|
||||
this.req.body.newProjectName = this.newProjectName
|
||||
})
|
||||
|
||||
it('should call the editor controller', function(done) {
|
||||
|
@ -725,9 +716,9 @@ describe('ProjectController', function() {
|
|||
this.EditorController.renameProject
|
||||
.calledWith(this.project_id, this.newProjectName)
|
||||
.should.equal(true)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.renameProject(this.req, this.res)
|
||||
this.ProjectController.renameProject(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send an error to next() if there is a problem', function(done) {
|
||||
|
@ -738,9 +729,9 @@ describe('ProjectController', function() {
|
|||
)
|
||||
const next = e => {
|
||||
e.should.equal(error)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.renameProject(this.req, this.res, next)
|
||||
this.ProjectController.renameProject(this.req, this.res, next)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -777,32 +768,32 @@ describe('ProjectController', function() {
|
|||
this.ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()
|
||||
this.InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1)
|
||||
this.AnalyticsManager.getLastOccurrence.yields(null, { mock: 'event' })
|
||||
return this.ProjectUpdateHandler.markAsOpened.callsArgWith(1)
|
||||
this.ProjectUpdateHandler.markAsOpened.callsArgWith(1)
|
||||
})
|
||||
|
||||
it('should render the project/editor page', function(done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
pageName.should.equal('project/editor')
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.loadEditor(this.req, this.res)
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should add user', function(done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.user.email.should.equal(this.user.email)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.loadEditor(this.req, this.res)
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should add on userSettings', function(done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.userSettings.fontSize.should.equal(this.user.ace.fontSize)
|
||||
opts.userSettings.editorTheme.should.equal(this.user.ace.theme)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.loadEditor(this.req, this.res)
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should add isRestrictedTokenMember', function(done) {
|
||||
|
@ -848,9 +839,9 @@ describe('ProjectController', function() {
|
|||
this.settings.editorIsOpen = false
|
||||
this.res.render = (pageName, opts) => {
|
||||
pageName.should.equal('general/closed')
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.loadEditor(this.req, this.res)
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should not render the page if the project can not be accessed', function(done) {
|
||||
|
@ -859,9 +850,9 @@ describe('ProjectController', function() {
|
|||
.callsArgWith(3, null, null)
|
||||
this.res.sendStatus = (resCode, opts) => {
|
||||
resCode.should.equal(401)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.loadEditor(this.req, this.res)
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should reactivateProjectIfRequired', function(done) {
|
||||
|
@ -869,9 +860,9 @@ describe('ProjectController', function() {
|
|||
this.InactiveProjectManager.reactivateProjectIfRequired
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.loadEditor(this.req, this.res)
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should mark project as opened', function(done) {
|
||||
|
@ -879,9 +870,9 @@ describe('ProjectController', function() {
|
|||
this.ProjectUpdateHandler.markAsOpened
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.loadEditor(this.req, this.res)
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should call the brand variations handler for branded projects', function(done) {
|
||||
|
@ -890,9 +881,9 @@ describe('ProjectController', function() {
|
|||
this.BrandVariationsHandler.getBrandVariationById
|
||||
.calledWith()
|
||||
.should.equal(true)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.loadEditor(this.req, this.res)
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should not call the brand variations handler for unbranded projects', function(done) {
|
||||
|
@ -900,18 +891,18 @@ describe('ProjectController', function() {
|
|||
this.BrandVariationsHandler.getBrandVariationById.called.should.equal(
|
||||
false
|
||||
)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.loadEditor(this.req, this.res)
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should expose the brand variation details as locals for branded projects', function(done) {
|
||||
this.ProjectGetter.getProject.callsArgWith(2, null, this.brandedProject)
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.brandVariation.should.deep.equal(this.brandVariationDetails)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.loadEditor(this.req, this.res)
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -971,7 +962,7 @@ describe('ProjectController', function() {
|
|||
this.AuthenticationController.getLoggedInUserId = sinon
|
||||
.stub()
|
||||
.returns(this.user._id)
|
||||
return done()
|
||||
done()
|
||||
})
|
||||
|
||||
it('should produce a list of projects', function(done) {
|
||||
|
@ -982,13 +973,9 @@ describe('ProjectController', function() {
|
|||
{ _id: 'd', name: 'D', accessLevel: 'd' }
|
||||
]
|
||||
})
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.userProjectsJson(
|
||||
this.req,
|
||||
this.res,
|
||||
this.next
|
||||
)
|
||||
this.ProjectController.userProjectsJson(this.req, this.res, this.next)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1007,9 +994,9 @@ describe('ProjectController', function() {
|
|||
this.ProjectGetter.getProject = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, this.project)
|
||||
return (this.ProjectEntityHandler.getAllEntitiesFromProject = sinon
|
||||
this.ProjectEntityHandler.getAllEntitiesFromProject = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, this.docs, this.files))
|
||||
.callsArgWith(1, null, this.docs, this.files)
|
||||
})
|
||||
|
||||
it('should produce a list of entities', function(done) {
|
||||
|
@ -1026,13 +1013,9 @@ describe('ProjectController', function() {
|
|||
expect(
|
||||
this.ProjectEntityHandler.getAllEntitiesFromProject.callCount
|
||||
).to.equal(1)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
return this.ProjectController.projectEntitiesJson(
|
||||
this.req,
|
||||
this.res,
|
||||
this.next
|
||||
)
|
||||
this.ProjectController.projectEntitiesJson(this.req, this.res, this.next)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1115,7 +1098,7 @@ describe('ProjectController', function() {
|
|||
})
|
||||
describe('_isInPercentageRollout', function() {
|
||||
before(function() {
|
||||
return (this.ids = [
|
||||
this.ids = [
|
||||
'5a05cd7621f9fe22be131740',
|
||||
'5a05cd7821f9fe22be131741',
|
||||
'5a05cd7921f9fe22be131742',
|
||||
|
@ -1136,14 +1119,14 @@ describe('ProjectController', function() {
|
|||
'5a05cd8421f9fe22be131751',
|
||||
'5a05cd8421f9fe22be131752',
|
||||
'5a05cd8521f9fe22be131753'
|
||||
])
|
||||
]
|
||||
})
|
||||
|
||||
it('should produce the expected results', function() {
|
||||
expect(
|
||||
this.ids.map(i => {
|
||||
return this.ProjectController._isInPercentageRollout('abcd', i, 50)
|
||||
})
|
||||
this.ids.map(i =>
|
||||
this.ProjectController._isInPercentageRollout('abcd', i, 50)
|
||||
)
|
||||
).to.deep.equal([
|
||||
false,
|
||||
false,
|
||||
|
@ -1166,10 +1149,10 @@ describe('ProjectController', function() {
|
|||
false,
|
||||
true
|
||||
])
|
||||
return expect(
|
||||
this.ids.map(i => {
|
||||
return this.ProjectController._isInPercentageRollout('efgh', i, 50)
|
||||
})
|
||||
expect(
|
||||
this.ids.map(i =>
|
||||
this.ProjectController._isInPercentageRollout('efgh', i, 50)
|
||||
)
|
||||
).to.deep.equal([
|
||||
false,
|
||||
false,
|
||||
|
@ -1194,4 +1177,46 @@ describe('ProjectController', function() {
|
|||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('transferOwnership', function() {
|
||||
beforeEach(function() {
|
||||
this.req.body = { user_id: this.user._id.toString() }
|
||||
})
|
||||
|
||||
it('validates the request body', function(done) {
|
||||
this.req.body = {}
|
||||
this.ProjectController.transferOwnership(this.req, this.res, err => {
|
||||
expect(err).to.be.instanceof(HttpErrors.BadRequestError)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns 204 on success', function(done) {
|
||||
this.res.sendStatus = status => {
|
||||
expect(status).to.equal(204)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.transferOwnership(this.req, this.res)
|
||||
})
|
||||
|
||||
it('returns 404 if the project does not exist', function(done) {
|
||||
this.ProjectDetailsHandler.transferOwnership.yields(
|
||||
new Errors.ProjectNotFoundError()
|
||||
)
|
||||
this.ProjectController.transferOwnership(this.req, this.res, err => {
|
||||
expect(err).to.be.instanceof(HttpErrors.NotFoundError)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns 404 if the user does not exist', function(done) {
|
||||
this.ProjectDetailsHandler.transferOwnership.yields(
|
||||
new Errors.UserNotFoundError()
|
||||
)
|
||||
this.ProjectController.transferOwnership(this.req, this.res, err => {
|
||||
expect(err).to.be.instanceof(HttpErrors.NotFoundError)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,22 +1,30 @@
|
|||
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const { ObjectId } = require('mongodb')
|
||||
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
||||
const PrivilegeLevels = require('../../../../app/src/Features/Authorization/PrivilegeLevels')
|
||||
|
||||
const MODULE_PATH = '../../../../app/src/Features/Project/ProjectDetailsHandler'
|
||||
|
||||
describe('ProjectDetailsHandler', function() {
|
||||
beforeEach(function() {
|
||||
this.project_id = '321l3j1kjkjl'
|
||||
this.user_id = 'user-id-123'
|
||||
this.user = {
|
||||
_id: ObjectId('abcdefabcdefabcdefabcdef'),
|
||||
features: 'mock-features'
|
||||
}
|
||||
this.collaborator = {
|
||||
_id: ObjectId('123456123456123456123456')
|
||||
}
|
||||
this.project = {
|
||||
_id: ObjectId('5d5dabdbb351de090cdff0b2'),
|
||||
name: 'project',
|
||||
description: 'this is a great project',
|
||||
something: 'should not exist',
|
||||
compiler: 'latexxxxxx',
|
||||
owner_ref: this.user_id
|
||||
owner_ref: this.user._id,
|
||||
collaberator_refs: [this.collaborator._id]
|
||||
}
|
||||
this.user = { features: 'mock-features' }
|
||||
this.ProjectGetter = {
|
||||
promises: {
|
||||
getProjectWithoutDocLines: sinon.stub().resolves(this.project),
|
||||
|
@ -52,7 +60,8 @@ describe('ProjectDetailsHandler', function() {
|
|||
}
|
||||
this.CollaboratorsHandler = {
|
||||
promises: {
|
||||
removeUserFromProject: sinon.stub().resolves()
|
||||
removeUserFromProject: sinon.stub().resolves(),
|
||||
addUserIdToProject: sinon.stub().resolves()
|
||||
}
|
||||
}
|
||||
this.ProjectTokenGenerator = {
|
||||
|
@ -91,7 +100,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
describe('getDetails', function() {
|
||||
it('should find the project and owner', async function() {
|
||||
const details = await this.handler.promises.getDetails(this.project_id)
|
||||
const details = await this.handler.promises.getDetails(this.project._id)
|
||||
details.name.should.equal(this.project.name)
|
||||
details.description.should.equal(this.project.description)
|
||||
details.compiler.should.equal(this.project.compiler)
|
||||
|
@ -101,7 +110,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should find overleaf metadata if it exists', async function() {
|
||||
this.project.overleaf = { id: 'id' }
|
||||
const details = await this.handler.promises.getDetails(this.project_id)
|
||||
const details = await this.handler.promises.getDetails(this.project._id)
|
||||
details.overleaf.should.equal(this.project.overleaf)
|
||||
expect(details.something).to.be.undefined
|
||||
})
|
||||
|
@ -115,95 +124,122 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should return the default features if no owner found', async function() {
|
||||
this.UserGetter.promises.getUser.resolves(null)
|
||||
const details = await this.handler.promises.getDetails(this.project_id)
|
||||
const details = await this.handler.promises.getDetails(this.project._id)
|
||||
details.features.should.equal(this.settings.defaultFeatures)
|
||||
})
|
||||
|
||||
it('should rethrow any error', async function() {
|
||||
this.ProjectGetter.promises.getProject.rejects(new Error('boom'))
|
||||
await expect(this.handler.promises.getDetails(this.project_id)).to.be
|
||||
await expect(this.handler.promises.getDetails(this.project._id)).to.be
|
||||
.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('transferOwnership', function() {
|
||||
beforeEach(function() {
|
||||
this.ProjectGetter.promises.findAllUsersProjects.resolves({
|
||||
owned: [{ name: this.project.name }],
|
||||
readAndWrite: [],
|
||||
readOnly: [],
|
||||
tokenReadAndWrite: [],
|
||||
tokenReadOnly: []
|
||||
})
|
||||
this.UserGetter.promises.getUser.resolves(this.collaborator)
|
||||
})
|
||||
|
||||
it("should return a not found error if the project can't be found", async function() {
|
||||
this.ProjectGetter.promises.getProject.resolves(null)
|
||||
await expect(
|
||||
this.handler.promises.transferOwnership('abc', '123')
|
||||
).to.be.rejectedWith(Errors.NotFoundError)
|
||||
this.handler.promises.transferOwnership('abc', this.collaborator._id)
|
||||
).to.be.rejectedWith(Errors.ProjectNotFoundError)
|
||||
})
|
||||
|
||||
it("should return a not found error if the user can't be found", async function() {
|
||||
this.UserGetter.promises.getUser.resolves(null)
|
||||
await expect(
|
||||
this.handler.promises.transferOwnership('abc', '123')
|
||||
).to.be.rejectedWith(Errors.NotFoundError)
|
||||
this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
).to.be.rejectedWith(Errors.UserNotFoundError)
|
||||
})
|
||||
|
||||
it('should return an error if user cannot be removed as collaborator ', async function() {
|
||||
this.CollaboratorsHandler.promises.removeUserFromProject.rejects(
|
||||
new Error('user-cannot-be-removed')
|
||||
)
|
||||
await expect(this.handler.promises.transferOwnership('abc', '123')).to.be
|
||||
.rejected
|
||||
await expect(
|
||||
this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
|
||||
it('should transfer ownership of the project', async function() {
|
||||
await this.handler.promises.transferOwnership('abc', '123')
|
||||
expect(this.ProjectModel.update).to.have.been.calledWith(
|
||||
{ _id: 'abc' },
|
||||
sinon.match({ $set: { owner_ref: '123' } })
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
expect(this.ProjectModel.update).to.have.been.calledWith(
|
||||
{ _id: this.project._id },
|
||||
sinon.match({ $set: { owner_ref: this.collaborator._id } })
|
||||
)
|
||||
})
|
||||
|
||||
it('should do nothing if transferring back to the owner', async function() {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.user._id
|
||||
)
|
||||
expect(this.ProjectModel.update).not.to.have.been.called
|
||||
})
|
||||
|
||||
it("should remove the user from the project's collaborators", async function() {
|
||||
await this.handler.promises.transferOwnership('abc', '123')
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
expect(
|
||||
this.CollaboratorsHandler.promises.removeUserFromProject
|
||||
).to.have.been.calledWith('abc', '123')
|
||||
).to.have.been.calledWith(this.project._id, this.collaborator._id)
|
||||
})
|
||||
|
||||
it('should add the former project owner as a read/write collaborator', async function() {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
expect(
|
||||
this.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
this.project._id,
|
||||
this.collaborator._id,
|
||||
this.user._id,
|
||||
PrivilegeLevels.READ_AND_WRITE
|
||||
)
|
||||
})
|
||||
|
||||
it('should flush the project to tpds', async function() {
|
||||
await this.handler.promises.transferOwnership('abc', '123')
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
expect(
|
||||
this.ProjectEntityHandler.promises.flushProjectToThirdPartyDataStore
|
||||
).to.have.been.calledWith('abc')
|
||||
).to.have.been.calledWith(this.project._id)
|
||||
})
|
||||
|
||||
it('should generate a unique name for the project', async function() {
|
||||
await this.handler.promises.transferOwnership('abc', '123')
|
||||
expect(this.ProjectModel.update).to.have.been.calledWith(
|
||||
{ _id: 'abc' },
|
||||
sinon.match({ $set: { name: `${this.project.name} (1)` } })
|
||||
)
|
||||
})
|
||||
|
||||
it('should append the supplied suffix to the project name, if passed', async function() {
|
||||
await this.handler.promises.transferOwnership('abc', '123', ' wombat')
|
||||
expect(this.ProjectModel.update).to.have.been.calledWith(
|
||||
{ _id: 'abc' },
|
||||
sinon.match({ $set: { name: `${this.project.name} wombat` } })
|
||||
)
|
||||
it('should decline to transfer ownership to a non-collaborator', async function() {
|
||||
this.project.collaberator_refs = []
|
||||
await expect(
|
||||
this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
).to.be.rejectedWith(Errors.UserNotCollaboratorError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getProjectDescription', function() {
|
||||
it('should make a call to mongo just for the description', async function() {
|
||||
this.ProjectGetter.promises.getProject.resolves()
|
||||
await this.handler.promises.getProjectDescription(this.project_id)
|
||||
await this.handler.promises.getProjectDescription(this.project._id)
|
||||
expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
|
||||
this.project_id,
|
||||
this.project._id,
|
||||
{ description: true }
|
||||
)
|
||||
})
|
||||
|
@ -214,7 +250,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
description: expectedDescription
|
||||
})
|
||||
const description = await this.handler.promises.getProjectDescription(
|
||||
this.project_id
|
||||
this.project._id
|
||||
)
|
||||
expect(description).to.equal(expectedDescription)
|
||||
})
|
||||
|
@ -227,11 +263,11 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should update the project detials', async function() {
|
||||
await this.handler.promises.setProjectDescription(
|
||||
this.project_id,
|
||||
this.project._id,
|
||||
this.description
|
||||
)
|
||||
expect(this.ProjectModel.update).to.have.been.calledWith(
|
||||
{ _id: this.project_id },
|
||||
{ _id: this.project._id },
|
||||
{ description: this.description }
|
||||
)
|
||||
})
|
||||
|
@ -243,18 +279,18 @@ describe('ProjectDetailsHandler', function() {
|
|||
})
|
||||
|
||||
it('should update the project with the new name', async function() {
|
||||
await this.handler.promises.renameProject(this.project_id, this.newName)
|
||||
await this.handler.promises.renameProject(this.project._id, this.newName)
|
||||
expect(this.ProjectModel.update).to.have.been.calledWith(
|
||||
{ _id: this.project_id },
|
||||
{ _id: this.project._id },
|
||||
{ name: this.newName }
|
||||
)
|
||||
})
|
||||
|
||||
it('should tell the TpdsUpdateSender', async function() {
|
||||
await this.handler.promises.renameProject(this.project_id, this.newName)
|
||||
await this.handler.promises.renameProject(this.project._id, this.newName)
|
||||
expect(this.TpdsUpdateSender.promises.moveEntity).to.have.been.calledWith(
|
||||
{
|
||||
project_id: this.project_id,
|
||||
project_id: this.project._id,
|
||||
project_name: this.project.name,
|
||||
newProjectName: this.newName
|
||||
}
|
||||
|
@ -262,7 +298,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
})
|
||||
|
||||
it('should not do anything with an invalid name', async function() {
|
||||
await expect(this.handler.promises.renameProject(this.project_id)).to.be
|
||||
await expect(this.handler.promises.renameProject(this.project._id)).to.be
|
||||
.rejected
|
||||
expect(this.TpdsUpdateSender.promises.moveEntity).not.to.have.been.called
|
||||
expect(this.ProjectModel.update).not.to.have.been.called
|
||||
|
@ -358,7 +394,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should leave a unique name unchanged', async function() {
|
||||
const name = await this.handler.promises.generateUniqueName(
|
||||
this.user_id,
|
||||
this.user._id,
|
||||
'unique-name',
|
||||
['-test-suffix']
|
||||
)
|
||||
|
@ -367,7 +403,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should append a suffix to an existing name', async function() {
|
||||
const name = await this.handler.promises.generateUniqueName(
|
||||
this.user_id,
|
||||
this.user._id,
|
||||
'name1',
|
||||
['-test-suffix']
|
||||
)
|
||||
|
@ -376,7 +412,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should fallback to a second suffix when needed', async function() {
|
||||
const name = await this.handler.promises.generateUniqueName(
|
||||
this.user_id,
|
||||
this.user._id,
|
||||
'name1',
|
||||
['1', '-test-suffix']
|
||||
)
|
||||
|
@ -385,7 +421,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should truncate the name when append a suffix if the result is too long', async function() {
|
||||
const name = await this.handler.promises.generateUniqueName(
|
||||
this.user_id,
|
||||
this.user._id,
|
||||
this.longName,
|
||||
['-test-suffix']
|
||||
)
|
||||
|
@ -397,7 +433,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should use a numeric index if no suffix is supplied', async function() {
|
||||
const name = await this.handler.promises.generateUniqueName(
|
||||
this.user_id,
|
||||
this.user._id,
|
||||
'name1',
|
||||
[]
|
||||
)
|
||||
|
@ -406,7 +442,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should use a numeric index if all suffixes are exhausted', async function() {
|
||||
const name = await this.handler.promises.generateUniqueName(
|
||||
this.user_id,
|
||||
this.user._id,
|
||||
'name',
|
||||
['1', '11']
|
||||
)
|
||||
|
@ -415,7 +451,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should find the next lowest available numeric index for the base name', async function() {
|
||||
const name = await this.handler.promises.generateUniqueName(
|
||||
this.user_id,
|
||||
this.user._id,
|
||||
'numeric',
|
||||
[]
|
||||
)
|
||||
|
@ -424,7 +460,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should find the next available numeric index when a numeric index is already present', async function() {
|
||||
const name = await this.handler.promises.generateUniqueName(
|
||||
this.user_id,
|
||||
this.user._id,
|
||||
'numeric (5)',
|
||||
[]
|
||||
)
|
||||
|
@ -433,7 +469,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should not find a numeric index lower than the one already present', async function() {
|
||||
const name = await this.handler.promises.generateUniqueName(
|
||||
this.user_id,
|
||||
this.user._id,
|
||||
'numeric (31)',
|
||||
[]
|
||||
)
|
||||
|
@ -472,11 +508,11 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should update the project with the new level', async function() {
|
||||
await this.handler.promises.setPublicAccessLevel(
|
||||
this.project_id,
|
||||
this.project._id,
|
||||
this.accessLevel
|
||||
)
|
||||
expect(this.ProjectModel.update).to.have.been.calledWith(
|
||||
{ _id: this.project_id },
|
||||
{ _id: this.project._id },
|
||||
{ publicAccesLevel: this.accessLevel }
|
||||
)
|
||||
})
|
||||
|
@ -484,7 +520,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
it('should not produce an error', async function() {
|
||||
await expect(
|
||||
this.handler.promises.setPublicAccessLevel(
|
||||
this.project_id,
|
||||
this.project._id,
|
||||
this.accessLevel
|
||||
)
|
||||
).to.be.resolved
|
||||
|
@ -498,7 +534,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
it('should produce an error', async function() {
|
||||
await expect(
|
||||
this.handler.promises.setPublicAccessLevel(
|
||||
this.project_id,
|
||||
this.project._id,
|
||||
this.accessLevel
|
||||
)
|
||||
).to.be.rejected
|
||||
|
@ -510,7 +546,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
describe('when the project has tokens', function() {
|
||||
beforeEach(function() {
|
||||
this.project = {
|
||||
_id: this.project_id,
|
||||
_id: this.project._id,
|
||||
tokens: {
|
||||
readOnly: 'aaa',
|
||||
readAndWrite: '42bbb',
|
||||
|
@ -521,10 +557,10 @@ describe('ProjectDetailsHandler', function() {
|
|||
})
|
||||
|
||||
it('should get the project', async function() {
|
||||
await this.handler.promises.ensureTokensArePresent(this.project_id)
|
||||
await this.handler.promises.ensureTokensArePresent(this.project._id)
|
||||
expect(this.ProjectGetter.promises.getProject).to.have.been.calledOnce
|
||||
expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
|
||||
this.project_id,
|
||||
this.project._id,
|
||||
{
|
||||
tokens: 1
|
||||
}
|
||||
|
@ -532,13 +568,13 @@ describe('ProjectDetailsHandler', function() {
|
|||
})
|
||||
|
||||
it('should not update the project with new tokens', async function() {
|
||||
await this.handler.promises.ensureTokensArePresent(this.project_id)
|
||||
await this.handler.promises.ensureTokensArePresent(this.project._id)
|
||||
expect(this.ProjectModel.update).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('should produce the tokens without error', async function() {
|
||||
const tokens = await this.handler.promises.ensureTokensArePresent(
|
||||
this.project_id
|
||||
this.project._id
|
||||
)
|
||||
expect(tokens).to.deep.equal(this.project.tokens)
|
||||
})
|
||||
|
@ -546,7 +582,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
describe('when tokens are missing', function() {
|
||||
beforeEach(function() {
|
||||
this.project = { _id: this.project_id }
|
||||
this.project = { _id: this.project._id }
|
||||
this.ProjectGetter.promises.getProject.resolves(this.project)
|
||||
this.readOnlyToken = 'abc'
|
||||
this.readAndWriteToken = '42def'
|
||||
|
@ -561,10 +597,10 @@ describe('ProjectDetailsHandler', function() {
|
|||
})
|
||||
|
||||
it('should get the project', async function() {
|
||||
await this.handler.promises.ensureTokensArePresent(this.project_id)
|
||||
await this.handler.promises.ensureTokensArePresent(this.project._id)
|
||||
expect(this.ProjectGetter.promises.getProject).to.have.been.calledOnce
|
||||
expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
|
||||
this.project_id,
|
||||
this.project._id,
|
||||
{
|
||||
tokens: 1
|
||||
}
|
||||
|
@ -572,14 +608,14 @@ describe('ProjectDetailsHandler', function() {
|
|||
})
|
||||
|
||||
it('should update the project with new tokens', async function() {
|
||||
await this.handler.promises.ensureTokensArePresent(this.project_id)
|
||||
await this.handler.promises.ensureTokensArePresent(this.project._id)
|
||||
expect(this.ProjectTokenGenerator.promises.generateUniqueReadOnlyToken)
|
||||
.to.have.been.calledOnce
|
||||
expect(this.ProjectTokenGenerator.readAndWriteToken).to.have.been
|
||||
.calledOnce
|
||||
expect(this.ProjectModel.update).to.have.been.calledOnce
|
||||
expect(this.ProjectModel.update).to.have.been.calledWith(
|
||||
{ _id: this.project_id },
|
||||
{ _id: this.project._id },
|
||||
{
|
||||
$set: {
|
||||
tokens: {
|
||||
|
@ -594,7 +630,7 @@ describe('ProjectDetailsHandler', function() {
|
|||
|
||||
it('should produce the tokens without error', async function() {
|
||||
const tokens = await this.handler.promises.ensureTokensArePresent(
|
||||
this.project_id
|
||||
this.project._id
|
||||
)
|
||||
expect(tokens).to.deep.equal({
|
||||
readOnly: this.readOnlyToken,
|
||||
|
|
Loading…
Add table
Reference in a new issue