Merge pull request #2255 from overleaf/em-audit-log

Project audit logs

GitOrigin-RevId: 439add2959be140c4f56ce9b41b9f59d432c494d
This commit is contained in:
Eric Mc Sween
2019-10-23 12:59:04 +00:00
committed by sharelatex
parent f6e4be616c
commit 06de9233b8
13 changed files with 611 additions and 374 deletions
@@ -0,0 +1,80 @@
const sinon = require('sinon')
const { expect } = require('chai')
const { ObjectId } = require('mongodb')
const SandboxedModule = require('sandboxed-module')
const { Project } = require('../helpers/models/Project')
const MODULE_PATH =
'../../../../app/src/Features/Project/ProjectAuditLogHandler'
describe('ProjectAuditLogHandler', function() {
beforeEach(function() {
this.projectId = ObjectId()
this.userId = ObjectId()
this.ProjectMock = sinon.mock(Project)
this.ProjectAuditLogHandler = SandboxedModule.require(MODULE_PATH, {
requires: {
'../../models/Project': { Project }
}
})
})
afterEach(function() {
this.ProjectMock.restore()
})
describe('addEntry', function() {
describe('success', function() {
beforeEach(async function() {
this.dbUpdate = this.ProjectMock.expects('updateOne').withArgs(
{ _id: this.projectId },
{
$push: {
auditLog: {
$each: [
{
operation: 'translate',
initiatorId: this.userId,
info: { destinationLanguage: 'tagalog' },
timestamp: sinon.match.typeOf('date')
}
],
$slice: -200
}
}
}
)
this.dbUpdate.chain('exec').resolves({ nModified: 1 })
this.operationId = await this.ProjectAuditLogHandler.promises.addEntry(
this.projectId,
'translate',
this.userId,
{ destinationLanguage: 'tagalog' }
)
})
it('writes a log', async function() {
this.ProjectMock.verify()
})
})
describe('when the project does not exist', function() {
beforeEach(function() {
this.ProjectMock.expects('updateOne')
.chain('exec')
.resolves({ nModified: 0 })
})
it('throws an error', async function() {
await expect(
this.ProjectAuditLogHandler.promises.addEntry(
this.projectId,
'translate',
this.userId,
{ destinationLanguage: 'tagalog' }
)
).to.be.rejected
})
})
})
})
@@ -73,9 +73,6 @@ describe('ProjectController', function() {
findAllUsersProjects: sinon.stub(),
getProject: sinon.stub()
}
this.ProjectDetailsHandler = {
transferOwnership: sinon.stub().yields()
}
this.ProjectHelper = {
isArchived: sinon.stub(),
isTrashed: sinon.stub(),
@@ -1268,46 +1265,4 @@ 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()
})
})
})
})
@@ -3,23 +3,22 @@ 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.user = {
_id: ObjectId('abcdefabcdefabcdefabcdef'),
_id: ObjectId(),
email: 'user@example.com',
features: 'mock-features'
}
this.collaborator = {
_id: ObjectId('123456123456123456123456'),
_id: ObjectId(),
email: 'collaborator@example.com'
}
this.project = {
_id: ObjectId('5d5dabdbb351de090cdff0b2'),
_id: ObjectId(),
name: 'project',
description: 'this is a great project',
something: 'should not exist',
@@ -55,22 +54,6 @@ describe('ProjectDetailsHandler', function() {
moveEntity: sinon.stub().resolves()
}
}
this.ProjectEntityHandler = {
promises: {
flushProjectToThirdPartyDataStore: sinon.stub().resolves()
}
}
this.CollaboratorsHandler = {
promises: {
removeUserFromProject: sinon.stub().resolves(),
addUserIdToProject: sinon.stub().resolves()
}
}
this.EmailHandler = {
promises: {
sendEmail: sinon.stub().resolves()
}
}
this.ProjectTokenGenerator = {
readAndWriteToken: sinon.stub(),
promises: {
@@ -91,9 +74,6 @@ describe('ProjectDetailsHandler', function() {
},
'../User/UserGetter': this.UserGetter,
'../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
'./ProjectEntityHandler': this.ProjectEntityHandler,
'../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
'../Email/EmailHandler': this.EmailHandler,
'logger-sharelatex': {
log() {},
warn() {},
@@ -143,135 +123,6 @@ describe('ProjectDetailsHandler', function() {
})
})
describe('transferOwnership', function() {
beforeEach(function() {
this.UserGetter.promises.getUser
.withArgs(this.user._id)
.resolves(this.user)
this.UserGetter.promises.getUser
.withArgs(this.collaborator._id)
.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', 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
.withArgs(this.collaborator._id)
.resolves(null)
await expect(
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(
this.project._id,
this.collaborator._id
)
).to.be.rejected
})
it('should transfer ownership of the project', async function() {
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(
this.project._id,
this.collaborator._id
)
expect(
this.CollaboratorsHandler.promises.removeUserFromProject
).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(
this.project._id,
this.collaborator._id
)
expect(
this.ProjectEntityHandler.promises.flushProjectToThirdPartyDataStore
).to.have.been.calledWith(this.project._id)
})
it('should send an email notification', async function() {
await this.handler.promises.transferOwnership(
this.project._id,
this.collaborator._id
)
expect(this.EmailHandler.promises.sendEmail).to.have.been.calledWith(
'ownershipTransferConfirmationPreviousOwner',
{
to: this.user.email,
project: this.project,
newOwner: this.collaborator
}
)
expect(this.EmailHandler.promises.sendEmail).to.have.been.calledWith(
'ownershipTransferConfirmationNewOwner',
{
to: this.collaborator.email,
project: this.project,
previousOwner: this.user
}
)
})
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()