Merge pull request #2640 from overleaf/em-promisify-project-deleter

Finish promisification of ProjectDeleter

GitOrigin-RevId: a426117c9430e2ee66b297b95f67460062a6a809
This commit is contained in:
Eric Mc Sween 2020-03-02 07:31:50 -05:00 committed by Copybot
parent e7f968d370
commit 864394d4ad
5 changed files with 336 additions and 399 deletions

View file

@ -12,15 +12,15 @@
* DS207: Consider shorter variations of null checks * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
let DocstoreManager
const request = require('request').defaults({ jar: false }) const request = require('request').defaults({ jar: false })
const logger = require('logger-sharelatex') const logger = require('logger-sharelatex')
const settings = require('settings-sharelatex') const settings = require('settings-sharelatex')
const Errors = require('../Errors/Errors') const Errors = require('../Errors/Errors')
const { promisifyAll } = require('../../util/promises')
const TIMEOUT = 30 * 1000 // request timeout const TIMEOUT = 30 * 1000 // request timeout
module.exports = DocstoreManager = { const DocstoreManager = {
deleteDoc(project_id, doc_id, callback) { deleteDoc(project_id, doc_id, callback) {
if (callback == null) { if (callback == null) {
callback = function(error) {} callback = function(error) {}
@ -255,3 +255,11 @@ module.exports = DocstoreManager = {
}) })
} }
} }
module.exports = DocstoreManager
module.exports.promises = promisifyAll(DocstoreManager, {
multiResult: {
getDoc: ['lines', 'rev', 'version', 'ranges'],
updateDoc: ['modified', 'rev']
}
})

View file

@ -416,14 +416,6 @@ const EditorController = {
) )
}, },
notifyUsersProjectHasBeenDeletedOrRenamed(project_id, callback) {
EditorRealTimeController.emitToRoom(
project_id,
'projectRenamedOrDeletedByExternalSource'
)
return callback()
},
updateProjectDescription(project_id, description, callback) { updateProjectDescription(project_id, description, callback) {
if (callback == null) { if (callback == null) {
callback = function() {} callback = function() {}

View file

@ -1,115 +1,99 @@
const { db, ObjectId } = require('../../infrastructure/mongojs') const { db, ObjectId } = require('../../infrastructure/mongojs')
const { promisify, callbackify } = require('util') const { callbackify } = require('util')
const { Project } = require('../../models/Project') const { Project } = require('../../models/Project')
const { DeletedProject } = require('../../models/DeletedProject') const { DeletedProject } = require('../../models/DeletedProject')
const Errors = require('../Errors/Errors') const Errors = require('../Errors/Errors')
const logger = require('logger-sharelatex') const logger = require('logger-sharelatex')
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
const TagsHandler = require('../Tags/TagsHandler') const TagsHandler = require('../Tags/TagsHandler')
const async = require('async')
const ProjectHelper = require('./ProjectHelper') const ProjectHelper = require('./ProjectHelper')
const ProjectDetailsHandler = require('./ProjectDetailsHandler') const ProjectDetailsHandler = require('./ProjectDetailsHandler')
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
const DocstoreManager = require('../Docstore/DocstoreManager') const DocstoreManager = require('../Docstore/DocstoreManager')
const EditorRealTimeController = require('../Editor/EditorRealTimeController')
const moment = require('moment') const moment = require('moment')
const { promiseMapWithLimit } = require('../../util/promises')
function logWarningOnError(msg) { const EXPIRE_PROJECTS_AFTER_DAYS = 90
return function(err) {
if (err) { module.exports = {
logger.warn({ err }, msg) markAsDeletedByExternalSource: callbackify(markAsDeletedByExternalSource),
} unmarkAsDeletedByExternalSource: callbackify(unmarkAsDeletedByExternalSource),
deleteUsersProjects: callbackify(deleteUsersProjects),
expireDeletedProjectsAfterDuration: callbackify(
expireDeletedProjectsAfterDuration
),
restoreProject: callbackify(restoreProject),
archiveProject: callbackify(archiveProject),
unarchiveProject: callbackify(unarchiveProject),
trashProject: callbackify(trashProject),
untrashProject: callbackify(untrashProject),
deleteProject: callbackify(deleteProject),
undeleteProject: callbackify(undeleteProject),
expireDeletedProject: callbackify(expireDeletedProject),
promises: {
archiveProject,
unarchiveProject,
trashProject,
untrashProject,
deleteProject,
undeleteProject,
expireDeletedProject,
markAsDeletedByExternalSource,
unmarkAsDeletedByExternalSource,
deleteUsersProjects,
expireDeletedProjectsAfterDuration,
restoreProject
} }
} }
const ProjectDeleter = { async function markAsDeletedByExternalSource(projectId) {
markAsDeletedByExternalSource(projectId, callback) {
callback =
callback ||
logWarningOnError('error marking project as deleted by external source')
logger.log( logger.log(
{ project_id: projectId }, { project_id: projectId },
'marking project as deleted by external data source' 'marking project as deleted by external data source'
) )
const conditions = { _id: projectId } await Project.update(
const update = { deletedByExternalDataSource: true } { _id: projectId },
{ deletedByExternalDataSource: true }
Project.update(conditions, update, {}, err => { ).exec()
if (err) { EditorRealTimeController.emitToRoom(
return callback(err)
}
require('../Editor/EditorController').notifyUsersProjectHasBeenDeletedOrRenamed(
projectId, projectId,
() => callback() // don't return error, as project has been updated 'projectRenamedOrDeletedByExternalSource'
) )
})
},
unmarkAsDeletedByExternalSource(projectId, callback) {
callback =
callback ||
logWarningOnError('error unmarking project as deleted by external source')
const conditions = { _id: projectId }
const update = { deletedByExternalDataSource: false }
Project.update(conditions, update, {}, callback)
},
deleteUsersProjects(userId, callback) {
Project.find({ owner_ref: userId }, function(error, projects) {
if (error) {
return callback(error)
} }
async.eachLimit(
projects,
5,
(project, cb) => ProjectDeleter.deleteProject(project._id, cb),
function(err) {
if (err) {
return callback(err)
}
CollaboratorsHandler.removeUserFromAllProjects(userId, callback)
}
)
})
},
expireDeletedProjectsAfterDuration(callback) { async function unmarkAsDeletedByExternalSource(projectId) {
const DURATION = 90 await Project.update(
DeletedProject.find( { _id: projectId },
{ { deletedByExternalDataSource: false }
).exec()
}
async function deleteUsersProjects(userId) {
const projects = await Project.find({ owner_ref: userId }).exec()
await promiseMapWithLimit(5, projects, project => deleteProject(project._id))
await CollaboratorsHandler.promises.removeUserFromAllProjects(userId)
}
async function expireDeletedProjectsAfterDuration() {
const deletedProjects = await DeletedProject.find({
'deleterData.deletedAt': { 'deleterData.deletedAt': {
$lt: new Date(moment().subtract(DURATION, 'days')) $lt: new Date(moment().subtract(EXPIRE_PROJECTS_AFTER_DAYS, 'days'))
}, },
project: { project: { $ne: null }
$ne: null })
} for (const deletedProject of deletedProjects) {
}, await expireDeletedProject(deletedProject.deleterData.deletedProjectId)
function(err, deletedProjects) {
if (err) {
logger.err({ err }, 'Problem with finding deletedProject')
return callback(err)
}
async.eachSeries(
deletedProjects,
function(deletedProject, cb) {
ProjectDeleter.expireDeletedProject(
deletedProject.deleterData.deletedProjectId,
cb
)
},
callback
)
}
)
},
restoreProject(projectId, callback) {
Project.update({ _id: projectId }, { $unset: { archived: true } }, callback)
} }
} }
// Async methods async function restoreProject(projectId) {
await Project.update(
{ _id: projectId },
{ $unset: { archived: true } }
).exec()
}
async function archiveProject(projectId, userId) { async function archiveProject(projectId, userId) {
try { try {
@ -231,17 +215,23 @@ async function deleteProject(projectId, options = {}) {
{ upsert: true } { upsert: true }
) )
const flushProjectToMongoAndDelete = promisify( await DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete(
DocumentUpdaterHandler.flushProjectToMongoAndDelete projectId
) )
await flushProjectToMongoAndDelete(projectId)
const memberIds = await CollaboratorsGetter.promises.getMemberIds(projectId) const memberIds = await CollaboratorsGetter.promises.getMemberIds(projectId)
// fire these jobs in the background // fire these jobs in the background
Array.from(memberIds).forEach(memberId => for (const memberId of memberIds) {
TagsHandler.removeProjectFromAllTags(memberId, projectId, () => {}) TagsHandler.promises
.removeProjectFromAllTags(memberId, projectId)
.catch(err => {
logger.err(
{ err, memberId, projectId },
'failed to remove project from tags'
) )
})
}
await Project.remove({ _id: projectId }).exec() await Project.remove({ _id: projectId }).exec()
} catch (err) { } catch (err) {
@ -309,8 +299,7 @@ async function expireDeletedProject(projectId) {
return return
} }
const destroyProject = promisify(DocstoreManager.destroyProject) await DocstoreManager.promises.destroyProject(deletedProject.project._id)
await destroyProject(deletedProject.project._id)
await DeletedProject.update( await DeletedProject.update(
{ {
@ -328,30 +317,3 @@ async function expireDeletedProject(projectId) {
throw error throw error
} }
} }
// Exported class
const promises = {
archiveProject: archiveProject,
unarchiveProject: unarchiveProject,
trashProject: trashProject,
untrashProject: untrashProject,
deleteProject: deleteProject,
undeleteProject: undeleteProject,
expireDeletedProject: expireDeletedProject,
deleteUsersProjects: promisify(ProjectDeleter.deleteUsersProjects),
unmarkAsDeletedByExternalSource: promisify(
ProjectDeleter.unmarkAsDeletedByExternalSource
)
}
ProjectDeleter.promises = promises
ProjectDeleter.archiveProject = callbackify(archiveProject)
ProjectDeleter.unarchiveProject = callbackify(unarchiveProject)
ProjectDeleter.trashProject = callbackify(trashProject)
ProjectDeleter.untrashProject = callbackify(untrashProject)
ProjectDeleter.deleteProject = callbackify(deleteProject)
ProjectDeleter.undeleteProject = callbackify(undeleteProject)
ProjectDeleter.expireDeletedProject = callbackify(expireDeletedProject)
module.exports = ProjectDeleter

View file

@ -632,23 +632,6 @@ describe('EditorController', function() {
}) })
}) })
describe('notifyUsersProjectHasBeenDeletedOrRenamed', function() {
it('should emmit a message to all users in a project', function(done) {
return this.EditorController.notifyUsersProjectHasBeenDeletedOrRenamed(
this.project_id,
err => {
this.EditorRealTimeController.emitToRoom
.calledWith(
this.project_id,
'projectRenamedOrDeletedByExternalSource'
)
.should.equal(true)
return done()
}
)
})
})
describe('updateProjectDescription', function() { describe('updateProjectDescription', function() {
beforeEach(function() { beforeEach(function() {
this.description = 'new description' this.description = 'new description'

View file

@ -8,46 +8,13 @@ const moment = require('moment')
const { Project } = require('../helpers/models/Project') const { Project } = require('../helpers/models/Project')
const { DeletedProject } = require('../helpers/models/DeletedProject') const { DeletedProject } = require('../helpers/models/DeletedProject')
const { ObjectId } = require('mongoose').Types const { ObjectId } = require('mongoose').Types
const Errors = require('../../../../app/src/Features/Errors/Errors')
describe('ProjectDeleter', function() { describe('ProjectDeleter', function() {
beforeEach(function() { beforeEach(function() {
tk.freeze(Date.now()) tk.freeze(Date.now())
this.project_id = ObjectId('588fffffffffffffffffffff')
this.ip = '192.170.18.1' this.ip = '192.170.18.1'
this.project = { this.project = dummyProject()
_id: this.project_id,
lastUpdated: new Date(),
rootFolder: [],
collaberator_refs: [
ObjectId('5b895d372f4189011c2f5afc'),
ObjectId('5b8d40073ead20011caca726')
],
readOnly_refs: [
ObjectId('5b8d4602b2f786011c4fb244'),
ObjectId('5b8d4a57c1cd2b011c39161c')
],
tokenAccessReadAndWrite_refs: [
ObjectId('5b8d5663a26df3035ea0d5a1'),
ObjectId('5b8e8676373c14011c2cad3c')
],
tokenAccessReadOnly_refs: [
ObjectId('5b9b801b5dd22c011ba5d9b3'),
ObjectId('5bc5adae1cad8d011fd4060a')
],
owner_ref: ObjectId('588aaaaaaaaaaaaaaaaaaaaa'),
tokens: {
readOnly: 'wombat',
readAndWrite: 'potato'
},
overleaf: {
id: 1234,
history: {
id: 5678
}
},
name: 'a very scientific analysis of spooky ghosts'
}
this.user = { this.user = {
_id: '588f3ddae8ebc1bac07c9fa4', _id: '588f3ddae8ebc1bac07c9fa4',
first_name: 'bjkdsjfk', first_name: 'bjkdsjfk',
@ -94,23 +61,29 @@ describe('ProjectDeleter', function() {
} }
] ]
this.documentUpdaterHandler = { this.DocumentUpdaterHandler = {
flushProjectToMongoAndDelete: sinon.stub().callsArgWith(1) promises: {
flushProjectToMongoAndDelete: sinon.stub().resolves()
} }
this.editorController = { }
notifyUsersProjectHasBeenDeletedOrRenamed: sinon.stub().callsArgWith(1) this.EditorRealTimeController = {
emitToRoom: sinon.stub()
} }
this.TagsHandler = { this.TagsHandler = {
removeProjectFromAllTags: sinon.stub().callsArgWith(2) promises: {
removeProjectFromAllTags: sinon.stub().resolves()
}
} }
this.CollaboratorsHandler = { this.CollaboratorsHandler = {
removeUserFromAllProjects: sinon.stub().yields() promises: {
removeUserFromAllProjects: sinon.stub().resolves()
}
} }
this.CollaboratorsGetter = { this.CollaboratorsGetter = {
promises: { promises: {
getMemberIds: sinon getMemberIds: sinon
.stub() .stub()
.withArgs(this.project_id) .withArgs(this.project._id)
.resolves(['member-id-1', 'member-id-2']) .resolves(['member-id-1', 'member-id-2'])
} }
} }
@ -138,7 +111,9 @@ describe('ProjectDeleter', function() {
} }
this.DocstoreManager = { this.DocstoreManager = {
destroyProject: sinon.stub().yields() promises: {
destroyProject: sinon.stub().resolves()
}
} }
this.ProjectMock = sinon.mock(Project) this.ProjectMock = sinon.mock(Project)
@ -146,12 +121,12 @@ describe('ProjectDeleter', function() {
this.ProjectDeleter = SandboxedModule.require(modulePath, { this.ProjectDeleter = SandboxedModule.require(modulePath, {
requires: { requires: {
'../Editor/EditorController': this.editorController, '../Editor/EditorRealTimeController': this.EditorRealTimeController,
'../../models/Project': { Project: Project }, '../../models/Project': { Project: Project },
'./ProjectHelper': this.ProjectHelper, './ProjectHelper': this.ProjectHelper,
'../../models/DeletedProject': { DeletedProject: DeletedProject }, '../../models/DeletedProject': { DeletedProject: DeletedProject },
'../DocumentUpdater/DocumentUpdaterHandler': this '../DocumentUpdater/DocumentUpdaterHandler': this
.documentUpdaterHandler, .DocumentUpdaterHandler,
'../Tags/TagsHandler': this.TagsHandler, '../Tags/TagsHandler': this.TagsHandler,
'../FileStore/FileStoreHandler': (this.FileStoreHandler = {}), '../FileStore/FileStoreHandler': (this.FileStoreHandler = {}),
'../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
@ -159,7 +134,8 @@ describe('ProjectDeleter', function() {
'../Docstore/DocstoreManager': this.DocstoreManager, '../Docstore/DocstoreManager': this.DocstoreManager,
'./ProjectDetailsHandler': this.ProjectDetailsHandler, './ProjectDetailsHandler': this.ProjectDetailsHandler,
'../../infrastructure/mongojs': { db: this.db, ObjectId }, '../../infrastructure/mongojs': { db: this.db, ObjectId },
'logger-sharelatex': this.logger 'logger-sharelatex': this.logger,
'../Errors/Errors': Errors
}, },
globals: { globals: {
console: console console: console
@ -177,38 +153,43 @@ describe('ProjectDeleter', function() {
beforeEach(function() { beforeEach(function() {
this.ProjectMock.expects('update') this.ProjectMock.expects('update')
.withArgs( .withArgs(
{ _id: this.project_id }, { _id: this.project._id },
{ deletedByExternalDataSource: true } { deletedByExternalDataSource: true }
) )
.yields() .chain('exec')
.resolves()
}) })
it('should update the project with the flag set to true', function(done) { it('should update the project with the flag set to true', async function() {
this.ProjectDeleter.markAsDeletedByExternalSource(this.project_id, () => { await this.ProjectDeleter.promises.markAsDeletedByExternalSource(
this.project._id
)
this.ProjectMock.verify() this.ProjectMock.verify()
done() })
it('should tell the editor controler so users are notified', async function() {
await this.ProjectDeleter.promises.markAsDeletedByExternalSource(
this.project._id
)
expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith(
this.project._id,
'projectRenamedOrDeletedByExternalSource'
)
}) })
}) })
it('should tell the editor controler so users are notified', function(done) { describe('unmarkAsDeletedByExternalSource', function() {
this.ProjectDeleter.markAsDeletedByExternalSource(this.project_id, () => { beforeEach(async function() {
this.editorController.notifyUsersProjectHasBeenDeletedOrRenamed
.calledWith(this.project_id)
.should.equal(true)
done()
})
})
})
describe('unmarkAsDeletedByExternalSource', function(done) {
beforeEach(function() {
this.ProjectMock.expects('update') this.ProjectMock.expects('update')
.withArgs( .withArgs(
{ _id: this.project_id }, { _id: this.project._id },
{ deletedByExternalDataSource: false } { deletedByExternalDataSource: false }
) )
.yields() .chain('exec')
this.ProjectDeleter.unmarkAsDeletedByExternalSource(this.project_id, done) .resolves()
await this.ProjectDeleter.promises.unmarkAsDeletedByExternalSource(
this.project._id
)
}) })
it('should remove the flag from the project', function() { it('should remove the flag from the project', function() {
@ -218,40 +199,48 @@ describe('ProjectDeleter', function() {
describe('deleteUsersProjects', function() { describe('deleteUsersProjects', function() {
beforeEach(function() { beforeEach(function() {
this.projects = [dummyProject(), dummyProject()]
this.ProjectMock.expects('find') this.ProjectMock.expects('find')
.withArgs({ owner_ref: this.user._id }) .withArgs({ owner_ref: this.user._id })
.yields(null, [{ _id: 'wombat' }, { _id: 'potato' }]) .chain('exec')
.resolves(this.projects)
this.ProjectDeleter.deleteProject = sinon.stub().yields() for (const project of this.projects) {
this.ProjectMock.expects('findOne')
.withArgs({ _id: project._id })
.chain('exec')
.resolves(project)
this.ProjectMock.expects('remove')
.withArgs({ _id: project._id })
.chain('exec')
.resolves()
this.DeletedProjectMock.expects('update')
.withArgs(
{ 'deleterData.deletedProjectId': project._id },
{
project,
deleterData: sinon.match.object
},
{ upsert: true }
)
.resolves()
}
}) })
it('should find all the projects owned by the user_id', function(done) { it('should delete all projects owned by the user', async function() {
this.ProjectDeleter.deleteUsersProjects(this.user._id, () => { await this.ProjectDeleter.promises.deleteUsersProjects(this.user._id)
this.ProjectMock.verify() this.ProjectMock.verify()
done() this.DeletedProjectMock.verify()
})
}) })
it('should call deleteProject once for each project', function(done) { it('should remove any collaboration from this user', async function() {
this.ProjectDeleter.deleteUsersProjects(this.user._id, () => { await this.ProjectDeleter.promises.deleteUsersProjects(this.user._id)
sinon.assert.calledTwice(this.ProjectDeleter.deleteProject)
sinon.assert.calledWith(this.ProjectDeleter.deleteProject, 'wombat')
sinon.assert.calledWith(this.ProjectDeleter.deleteProject, 'potato')
done()
})
})
it('should remove all the projects the user is a collaborator of', function(done) {
this.ProjectDeleter.deleteUsersProjects(this.user._id, () => {
sinon.assert.calledWith( sinon.assert.calledWith(
this.CollaboratorsHandler.removeUserFromAllProjects, this.CollaboratorsHandler.promises.removeUserFromAllProjects,
this.user._id this.user._id
) )
sinon.assert.calledOnce( sinon.assert.calledOnce(
this.CollaboratorsHandler.removeUserFromAllProjects this.CollaboratorsHandler.promises.removeUserFromAllProjects
) )
done()
})
}) })
}) })
@ -275,12 +264,12 @@ describe('ProjectDeleter', function() {
} }
this.ProjectMock.expects('findOne') this.ProjectMock.expects('findOne')
.withArgs({ _id: this.project_id }) .withArgs({ _id: this.project._id })
.chain('exec') .chain('exec')
.resolves(this.project) .resolves(this.project)
}) })
it('should save a DeletedProject with additional deleterData', function(done) { it('should save a DeletedProject with additional deleterData', async function() {
this.deleterData.deleterIpAddress = this.ip this.deleterData.deleterIpAddress = this.ip
this.deleterData.deleterId = this.user._id this.deleterData.deleterId = this.user._id
@ -298,76 +287,61 @@ describe('ProjectDeleter', function() {
) )
.resolves() .resolves()
this.ProjectDeleter.deleteProject( await this.ProjectDeleter.promises.deleteProject(this.project._id, {
this.project_id, deleterUser: this.user,
{ deleterUser: this.user, ipAddress: this.ip }, ipAddress: this.ip
err => { })
expect(err).not.to.exist
this.DeletedProjectMock.verify() this.DeletedProjectMock.verify()
done()
}
)
}) })
it('should flushProjectToMongoAndDelete in doc updater', function(done) { it('should flushProjectToMongoAndDelete in doc updater', async function() {
this.ProjectMock.expects('remove') this.ProjectMock.expects('remove')
.chain('exec') .chain('exec')
.resolves() .resolves()
this.DeletedProjectMock.expects('update').resolves() this.DeletedProjectMock.expects('update').resolves()
this.ProjectDeleter.deleteProject( await this.ProjectDeleter.promises.deleteProject(this.project._id, {
this.project_id, deleterUser: this.user,
{ deleterUser: this.user, ipAddress: this.ip }, ipAddress: this.ip
() => { })
this.documentUpdaterHandler.flushProjectToMongoAndDelete this.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete
.calledWith(this.project_id) .calledWith(this.project._id)
.should.equal(true) .should.equal(true)
done()
}
)
}) })
it('should removeProjectFromAllTags', function(done) { it('should removeProjectFromAllTags', async function() {
this.ProjectMock.expects('remove') this.ProjectMock.expects('remove')
.chain('exec') .chain('exec')
.resolves() .resolves()
this.DeletedProjectMock.expects('update').resolves() this.DeletedProjectMock.expects('update').resolves()
this.ProjectDeleter.deleteProject(this.project_id, () => { await this.ProjectDeleter.promises.deleteProject(this.project._id)
sinon.assert.calledWith( sinon.assert.calledWith(
this.TagsHandler.removeProjectFromAllTags, this.TagsHandler.promises.removeProjectFromAllTags,
'member-id-1', 'member-id-1',
this.project_id this.project._id
) )
sinon.assert.calledWith( sinon.assert.calledWith(
this.TagsHandler.removeProjectFromAllTags, this.TagsHandler.promises.removeProjectFromAllTags,
'member-id-2', 'member-id-2',
this.project_id this.project._id
) )
done()
})
}) })
it('should remove the project from Mongo', function(done) { it('should remove the project from Mongo', async function() {
this.ProjectMock.expects('remove') this.ProjectMock.expects('remove')
.withArgs({ _id: this.project_id }) .withArgs({ _id: this.project._id })
.chain('exec') .chain('exec')
.resolves() .resolves()
this.DeletedProjectMock.expects('update').resolves() this.DeletedProjectMock.expects('update').resolves()
this.ProjectDeleter.deleteProject(this.project_id, () => { await this.ProjectDeleter.promises.deleteProject(this.project._id)
this.ProjectMock.verify() this.ProjectMock.verify()
done()
})
}) })
}) })
describe('expireDeletedProjectsAfterDuration', function() { describe('expireDeletedProjectsAfterDuration', function() {
beforeEach(function(done) { beforeEach(async function() {
this.ProjectDeleter.expireDeletedProject = sinon
.stub()
.callsArgWith(1, null)
this.DeletedProjectMock.expects('find') this.DeletedProjectMock.expects('find')
.withArgs({ .withArgs({
'deleterData.deletedAt': { 'deleterData.deletedAt': {
@ -377,25 +351,42 @@ describe('ProjectDeleter', function() {
$ne: null $ne: null
} }
}) })
.yields(null, this.deletedProjects) .chain('exec')
.resolves(this.deletedProjects)
this.ProjectDeleter.expireDeletedProjectsAfterDuration(done) for (const deletedProject of this.deletedProjects) {
this.DeletedProjectMock.expects('findOne')
.withArgs({
'deleterData.deletedProjectId': deletedProject.project._id
}) })
.chain('exec')
it('should call find with a date 90 days earlier than today', function() { .resolves(deletedProject)
this.DeletedProjectMock.verify() this.DeletedProjectMock.expects('update')
}) .withArgs(
{
it('should call expireDeletedProject', function(done) { _id: deletedProject._id
expect(this.ProjectDeleter.expireDeletedProject).to.have.been.calledWith( },
this.deletedProjects[0].deleterData.deletedProjectId {
$set: {
'deleterData.deleterIpAddress': null,
project: null
}
}
) )
done() .chain('exec')
.resolves()
}
await this.ProjectDeleter.promises.expireDeletedProjectsAfterDuration()
})
it('should expire projects older than 90 days', function() {
this.DeletedProjectMock.verify()
}) })
}) })
describe('expireDeletedProject', function() { describe('expireDeletedProject', function() {
beforeEach(function(done) { beforeEach(async function() {
this.DeletedProjectMock.expects('update') this.DeletedProjectMock.expects('update')
.withArgs( .withArgs(
{ {
@ -418,9 +409,8 @@ describe('ProjectDeleter', function() {
.chain('exec') .chain('exec')
.resolves(this.deletedProjects[0]) .resolves(this.deletedProjects[0])
this.ProjectDeleter.expireDeletedProject( await this.ProjectDeleter.promises.expireDeletedProject(
this.deletedProjects[0].project._id, this.deletedProjects[0].project._id
done
) )
}) })
@ -429,9 +419,9 @@ describe('ProjectDeleter', function() {
}) })
it('should destroy the docs in docstore', function() { it('should destroy the docs in docstore', function() {
expect(this.DocstoreManager.destroyProject).to.have.been.calledWith( expect(
this.deletedProjects[0].project._id this.DocstoreManager.promises.destroyProject
) ).to.have.been.calledWith(this.deletedProjects[0].project._id)
}) })
}) })
@ -441,13 +431,13 @@ describe('ProjectDeleter', function() {
this.ProjectHelper.calculateArchivedArray.returns(archived) this.ProjectHelper.calculateArchivedArray.returns(archived)
this.ProjectMock.expects('findOne') this.ProjectMock.expects('findOne')
.withArgs({ _id: this.project_id }) .withArgs({ _id: this.project._id })
.chain('exec') .chain('exec')
.resolves(this.project) .resolves(this.project)
this.ProjectMock.expects('update') this.ProjectMock.expects('update')
.withArgs( .withArgs(
{ _id: this.project_id }, { _id: this.project._id },
{ {
$set: { archived: archived }, $set: { archived: archived },
$pull: { trashed: ObjectId(this.user._id) } $pull: { trashed: ObjectId(this.user._id) }
@ -458,7 +448,7 @@ describe('ProjectDeleter', function() {
it('should update the project', async function() { it('should update the project', async function() {
await this.ProjectDeleter.promises.archiveProject( await this.ProjectDeleter.promises.archiveProject(
this.project_id, this.project._id,
this.user._id this.user._id
) )
this.ProjectMock.verify() this.ProjectMock.verify()
@ -471,18 +461,18 @@ describe('ProjectDeleter', function() {
this.ProjectHelper.calculateArchivedArray.returns(archived) this.ProjectHelper.calculateArchivedArray.returns(archived)
this.ProjectMock.expects('findOne') this.ProjectMock.expects('findOne')
.withArgs({ _id: this.project_id }) .withArgs({ _id: this.project._id })
.chain('exec') .chain('exec')
.resolves(this.project) .resolves(this.project)
this.ProjectMock.expects('update') this.ProjectMock.expects('update')
.withArgs({ _id: this.project_id }, { $set: { archived: archived } }) .withArgs({ _id: this.project._id }, { $set: { archived: archived } })
.resolves() .resolves()
}) })
it('should update the project', async function() { it('should update the project', async function() {
await this.ProjectDeleter.promises.unarchiveProject( await this.ProjectDeleter.promises.unarchiveProject(
this.project_id, this.project._id,
this.user._id this.user._id
) )
this.ProjectMock.verify() this.ProjectMock.verify()
@ -492,13 +482,13 @@ describe('ProjectDeleter', function() {
describe('trashProject', function() { describe('trashProject', function() {
beforeEach(function() { beforeEach(function() {
this.ProjectMock.expects('findOne') this.ProjectMock.expects('findOne')
.withArgs({ _id: this.project_id }) .withArgs({ _id: this.project._id })
.chain('exec') .chain('exec')
.resolves(this.project) .resolves(this.project)
this.ProjectMock.expects('update') this.ProjectMock.expects('update')
.withArgs( .withArgs(
{ _id: this.project_id }, { _id: this.project._id },
{ {
$addToSet: { trashed: ObjectId(this.user._id) }, $addToSet: { trashed: ObjectId(this.user._id) },
$pull: { archived: ObjectId(this.user._id) } $pull: { archived: ObjectId(this.user._id) }
@ -509,7 +499,7 @@ describe('ProjectDeleter', function() {
it('should update the project', async function() { it('should update the project', async function() {
await this.ProjectDeleter.promises.trashProject( await this.ProjectDeleter.promises.trashProject(
this.project_id, this.project._id,
this.user._id this.user._id
) )
this.ProjectMock.verify() this.ProjectMock.verify()
@ -519,13 +509,13 @@ describe('ProjectDeleter', function() {
describe('untrashProject', function() { describe('untrashProject', function() {
beforeEach(function() { beforeEach(function() {
this.ProjectMock.expects('findOne') this.ProjectMock.expects('findOne')
.withArgs({ _id: this.project_id }) .withArgs({ _id: this.project._id })
.chain('exec') .chain('exec')
.resolves(this.project) .resolves(this.project)
this.ProjectMock.expects('update') this.ProjectMock.expects('update')
.withArgs( .withArgs(
{ _id: this.project_id }, { _id: this.project._id },
{ $pull: { trashed: ObjectId(this.user._id) } } { $pull: { trashed: ObjectId(this.user._id) } }
) )
.resolves() .resolves()
@ -533,7 +523,7 @@ describe('ProjectDeleter', function() {
it('should update the project', async function() { it('should update the project', async function() {
await this.ProjectDeleter.promises.untrashProject( await this.ProjectDeleter.promises.untrashProject(
this.project_id, this.project._id,
this.user._id this.user._id
) )
this.ProjectMock.verify() this.ProjectMock.verify()
@ -545,20 +535,18 @@ describe('ProjectDeleter', function() {
this.ProjectMock.expects('update') this.ProjectMock.expects('update')
.withArgs( .withArgs(
{ {
_id: this.project_id _id: this.project._id
}, },
{ {
$unset: { archived: true } $unset: { archived: true }
} }
) )
.yields() .chain('exec')
.resolves()
}) })
it('should unset the archive attribute', function(done) { it('should unset the archive attribute', async function() {
this.ProjectDeleter.restoreProject(this.project_id, () => { await this.ProjectDeleter.promises.restoreProject(this.project._id)
this.ProjectMock.verify()
done()
})
}) })
}) })
@ -597,26 +585,20 @@ describe('ProjectDeleter', function() {
.resolves() .resolves()
}) })
it('should return not found if the project does not exist', function(done) { it('should return not found if the project does not exist', async function() {
this.ProjectDeleter.undeleteProject('wombat', err => { await expect(
expect(err).to.exist this.ProjectDeleter.promises.undeleteProject('wombat')
expect(err.name).to.equal('NotFoundError') ).to.be.rejectedWith(Errors.NotFoundError, 'project_not_found')
expect(err.message).to.equal('project_not_found')
done()
})
}) })
it('should return not found if the project has been expired', function(done) { it('should return not found if the project has been expired', async function() {
this.ProjectDeleter.undeleteProject('purgedProject', err => { await expect(
expect(err.name).to.equal('NotFoundError') this.ProjectDeleter.promises.undeleteProject('purgedProject')
expect(err.message).to.equal('project_too_old_to_restore') ).to.be.rejectedWith(Errors.NotFoundError, 'project_too_old_to_restore')
done()
})
}) })
it('should insert the project into the collection', function(done) { it('should insert the project into the collection', async function() {
this.ProjectDeleter.undeleteProject(this.project._id, err => { await this.ProjectDeleter.promises.undeleteProject(this.project._id)
expect(err).not.to.exist
sinon.assert.calledWith( sinon.assert.calledWith(
this.db.projects.insert, this.db.projects.insert,
sinon.match({ sinon.match({
@ -624,46 +606,35 @@ describe('ProjectDeleter', function() {
name: this.project.name name: this.project.name
}) })
) )
done()
})
}) })
it('should clear the archive bit', function(done) { it('should clear the archive bit', async function() {
this.project.archived = true this.project.archived = true
this.ProjectDeleter.undeleteProject(this.project._id, err => { await this.ProjectDeleter.promises.undeleteProject(this.project._id)
expect(err).not.to.exist
sinon.assert.calledWith( sinon.assert.calledWith(
this.db.projects.insert, this.db.projects.insert,
sinon.match({ archived: undefined }) sinon.match({ archived: undefined })
) )
done()
})
}) })
it('should generate a unique name for the project', function(done) { it('should generate a unique name for the project', async function() {
this.ProjectDeleter.undeleteProject(this.project._id, err => { await this.ProjectDeleter.promises.undeleteProject(this.project._id)
expect(err).not.to.exist
sinon.assert.calledWith( sinon.assert.calledWith(
this.ProjectDetailsHandler.promises.generateUniqueName, this.ProjectDetailsHandler.promises.generateUniqueName,
this.project.owner_ref this.project.owner_ref
) )
done()
})
}) })
it('should add a suffix to the project name', function(done) { it('should add a suffix to the project name', async function() {
this.ProjectDeleter.undeleteProject(this.project._id, err => { await this.ProjectDeleter.promises.undeleteProject(this.project._id)
expect(err).not.to.exist
sinon.assert.calledWith( sinon.assert.calledWith(
this.ProjectDetailsHandler.promises.generateUniqueName, this.ProjectDetailsHandler.promises.generateUniqueName,
this.project.owner_ref, this.project.owner_ref,
this.project.name + ' (Restored)' this.project.name + ' (Restored)'
) )
done()
})
}) })
it('should remove the DeletedProject', function(done) { it('should remove the DeletedProject', async function() {
// need to change the mock just to include the methods we want // need to change the mock just to include the methods we want
this.DeletedProjectMock.restore() this.DeletedProjectMock.restore()
this.DeletedProjectMock = sinon.mock(DeletedProject) this.DeletedProjectMock = sinon.mock(DeletedProject)
@ -676,11 +647,32 @@ describe('ProjectDeleter', function() {
.chain('exec') .chain('exec')
.resolves() .resolves()
this.ProjectDeleter.undeleteProject(this.project._id, err => { await this.ProjectDeleter.promises.undeleteProject(this.project._id)
expect(err).not.to.exist
this.DeletedProjectMock.verify() this.DeletedProjectMock.verify()
done()
})
}) })
}) })
}) })
function dummyProject() {
return {
_id: new ObjectId(),
lastUpdated: new Date(),
rootFolder: [],
collaberator_refs: [new ObjectId(), new ObjectId()],
readOnly_refs: [new ObjectId(), new ObjectId()],
tokenAccessReadAndWrite_refs: [new ObjectId(), new ObjectId()],
tokenAccessReadOnly_refs: [new ObjectId(), new ObjectId()],
owner_ref: new ObjectId(),
tokens: {
readOnly: 'wombat',
readAndWrite: 'potato'
},
overleaf: {
id: 1234,
history: {
id: 5678
}
},
name: 'a very scientific analysis of spooky ghosts'
}
}