cb0266035d
11 years ago, the db.projects collection was storing doc lines in the
file-tree/rootFolder. Any operations on the project that did not need
those lines were benefitting from excluding all those entries from the
file-tree. These days, the verbose exclusions are not useful anymore and
merely add load on mongo.
REF: 9805c6a9ff
GitOrigin-RevId: 89f544688934c1ed1ca98877ffbe8baefe66c126
419 lines
13 KiB
JavaScript
419 lines
13 KiB
JavaScript
import { vi, expect } from 'vitest'
|
|
import sinon from 'sinon'
|
|
import mongodb from 'mongodb-legacy'
|
|
const modulePath = '../../../../app/src/Features/Project/ProjectGetter.mjs'
|
|
|
|
const { ObjectId } = mongodb
|
|
|
|
describe('ProjectGetter', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.project = { _id: new ObjectId() }
|
|
ctx.projectIdStr = ctx.project._id.toString()
|
|
ctx.deletedProject = { deleterData: { wombat: 'potato' } }
|
|
ctx.userId = new ObjectId()
|
|
|
|
ctx.DeletedProject = {
|
|
find: sinon.stub().returns({
|
|
exec: sinon.stub().resolves([ctx.deletedProject]),
|
|
}),
|
|
}
|
|
ctx.Project = {
|
|
find: sinon.stub().returns({
|
|
exec: sinon.stub().resolves(),
|
|
populate: sinon.stub().returnsThis(),
|
|
limit: sinon.stub().returnsThis(),
|
|
}),
|
|
findOne: sinon.stub().returns({
|
|
exec: sinon.stub().resolves(ctx.project),
|
|
}),
|
|
exists: sinon.stub().returns({ exec: sinon.stub().resolves(true) }),
|
|
}
|
|
ctx.CollaboratorsGetter = {
|
|
promises: {
|
|
getProjectsUserIsMemberOf: sinon.stub().resolves({
|
|
readAndWrite: [],
|
|
readOnly: [],
|
|
tokenReadAndWrite: [],
|
|
tokenReadOnly: [],
|
|
}),
|
|
},
|
|
}
|
|
ctx.LockManager = {
|
|
promises: {
|
|
runWithLock: sinon
|
|
.stub()
|
|
.callsFake((namespace, id, runner) => runner()),
|
|
},
|
|
}
|
|
ctx.db = {
|
|
projects: {
|
|
findOne: sinon.stub().resolves(ctx.project),
|
|
},
|
|
users: {},
|
|
}
|
|
ctx.ProjectEntityMongoUpdateHandler = {
|
|
lockKey: sinon.stub().returnsArg(0),
|
|
}
|
|
|
|
vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({
|
|
db: ctx.db,
|
|
ObjectId,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/models/Project', () => ({
|
|
Project: ctx.Project,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/models/DeletedProject', () => ({
|
|
DeletedProject: ctx.DeletedProject,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Collaborators/CollaboratorsGetter',
|
|
() => ({
|
|
default: ctx.CollaboratorsGetter,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/infrastructure/LockManager', () => ({
|
|
default: ctx.LockManager,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler',
|
|
() => ({
|
|
default: ctx.ProjectEntityMongoUpdateHandler,
|
|
})
|
|
)
|
|
|
|
ctx.ProjectGetter = (await import(modulePath)).default
|
|
})
|
|
|
|
describe('getProject', function () {
|
|
describe('without projection', function () {
|
|
describe('with project id', function () {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.ProjectGetter.promises.getProject(ctx.projectIdStr)
|
|
})
|
|
|
|
it('should call findOne with the project id', function (ctx) {
|
|
expect(ctx.db.projects.findOne.callCount).to.equal(1)
|
|
expect(
|
|
ctx.db.projects.findOne.lastCall.args[0]._id.toString()
|
|
).to.equal(ctx.projectIdStr)
|
|
})
|
|
})
|
|
|
|
describe('without project id', function () {
|
|
it('should be rejected', function (ctx) {
|
|
expect(
|
|
ctx.ProjectGetter.promises.getProject(null)
|
|
).to.be.rejectedWith('no project id provided')
|
|
expect(ctx.db.projects.findOne.callCount).to.equal(0)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('with projection', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.projection = { _id: 1 }
|
|
})
|
|
|
|
describe('with project id', function () {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.ProjectGetter.promises.getProject(
|
|
ctx.projectIdStr,
|
|
ctx.projection
|
|
)
|
|
})
|
|
|
|
it('should call findOne with the project id', function (ctx) {
|
|
expect(ctx.db.projects.findOne.callCount).to.equal(1)
|
|
expect(
|
|
ctx.db.projects.findOne.lastCall.args[0]._id.toString()
|
|
).to.equal(ctx.projectIdStr)
|
|
expect(ctx.db.projects.findOne.lastCall.args[1]).to.deep.equal({
|
|
projection: ctx.projection,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('without project id', function () {
|
|
it('should be rejected', function (ctx) {
|
|
expect(
|
|
ctx.ProjectGetter.promises.getProject(null)
|
|
).to.be.rejectedWith('no project id provided')
|
|
expect(ctx.db.projects.findOne.callCount).to.equal(0)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getProjectWithoutLock', function () {
|
|
describe('without projection', function () {
|
|
describe('with project id', function () {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.ProjectGetter.promises.getProjectWithoutLock(
|
|
ctx.projectIdStr
|
|
)
|
|
})
|
|
|
|
it('should call findOne with the project id', function (ctx) {
|
|
expect(ctx.db.projects.findOne.callCount).to.equal(1)
|
|
expect(
|
|
ctx.db.projects.findOne.lastCall.args[0]._id.toString()
|
|
).to.equal(ctx.projectIdStr)
|
|
})
|
|
})
|
|
|
|
describe('without project id', function () {
|
|
it('should be rejected', function (ctx) {
|
|
expect(
|
|
ctx.ProjectGetter.promises.getProjectWithoutLock(null)
|
|
).to.be.rejectedWith('no project id provided')
|
|
expect(ctx.db.projects.findOne.callCount).to.equal(0)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('with projection', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.projection = { _id: 1 }
|
|
})
|
|
|
|
describe('with project id', function () {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.ProjectGetter.promises.getProjectWithoutLock(
|
|
ctx.project._id,
|
|
ctx.projection
|
|
)
|
|
})
|
|
|
|
it('should call findOne with the project id', function (ctx) {
|
|
expect(ctx.db.projects.findOne.callCount).to.equal(1)
|
|
expect(
|
|
ctx.db.projects.findOne.lastCall.args[0]._id.toString()
|
|
).to.equal(ctx.projectIdStr)
|
|
expect(ctx.db.projects.findOne.lastCall.args[1]).to.deep.equal({
|
|
projection: ctx.projection,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('without project id', function () {
|
|
it('should be rejected', function (ctx) {
|
|
expect(
|
|
ctx.ProjectGetter.promises.getProjectWithoutLock(null)
|
|
).to.be.rejectedWith('no project id provided')
|
|
expect(ctx.db.projects.findOne.callCount).to.equal(0)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('findAllUsersProjects', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.fields = { mock: 'fields' }
|
|
ctx.projectOwned = { _id: 'mock-owned-projects' }
|
|
ctx.projectRW = { _id: 'mock-rw-projects' }
|
|
ctx.projectReview = { _id: 'mock-review-projects' }
|
|
ctx.projectRO = { _id: 'mock-ro-projects' }
|
|
ctx.projectTokenRW = { _id: 'mock-token-rw-projects' }
|
|
ctx.projectTokenRO = { _id: 'mock-token-ro-projects' }
|
|
ctx.Project.find
|
|
.withArgs({ owner_ref: ctx.userId }, ctx.fields)
|
|
.returns({ exec: sinon.stub().resolves([ctx.projectOwned]) })
|
|
})
|
|
|
|
it('should return a promise with all the projects', async function (ctx) {
|
|
ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
|
|
readAndWrite: [ctx.projectRW],
|
|
readOnly: [ctx.projectRO],
|
|
tokenReadAndWrite: [ctx.projectTokenRW],
|
|
tokenReadOnly: [ctx.projectTokenRO],
|
|
review: [ctx.projectReview],
|
|
})
|
|
const projects = await ctx.ProjectGetter.promises.findAllUsersProjects(
|
|
ctx.userId,
|
|
ctx.fields
|
|
)
|
|
|
|
expect(projects).to.deep.equal({
|
|
owned: [ctx.projectOwned],
|
|
readAndWrite: [ctx.projectRW],
|
|
readOnly: [ctx.projectRO],
|
|
tokenReadAndWrite: [ctx.projectTokenRW],
|
|
tokenReadOnly: [ctx.projectTokenRO],
|
|
review: [ctx.projectReview],
|
|
})
|
|
})
|
|
|
|
it('should remove duplicate projects', async function (ctx) {
|
|
ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
|
|
readAndWrite: [ctx.projectRW, ctx.projectOwned],
|
|
readOnly: [ctx.projectRO, ctx.projectRW],
|
|
tokenReadAndWrite: [ctx.projectTokenRW, ctx.projectRO],
|
|
tokenReadOnly: [ctx.projectTokenRW, ctx.projectTokenRO, ctx.projectRO],
|
|
review: [ctx.projectReview],
|
|
})
|
|
const projects = await ctx.ProjectGetter.promises.findAllUsersProjects(
|
|
ctx.userId,
|
|
ctx.fields
|
|
)
|
|
|
|
expect(projects).to.deep.equal({
|
|
owned: [ctx.projectOwned],
|
|
readAndWrite: [ctx.projectRW],
|
|
readOnly: [ctx.projectRO],
|
|
tokenReadAndWrite: [ctx.projectTokenRW],
|
|
tokenReadOnly: [ctx.projectTokenRO],
|
|
review: [ctx.projectReview],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getProjectIdByReadAndWriteToken', function () {
|
|
describe('when project find returns project', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.projectIdFound =
|
|
await ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
|
|
'token'
|
|
)
|
|
})
|
|
|
|
it('should find project with token', function (ctx) {
|
|
ctx.Project.findOne
|
|
.calledWithMatch({ 'tokens.readAndWrite': 'token' })
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should return the project id', function (ctx) {
|
|
expect(ctx.projectIdFound).to.equal(ctx.project._id)
|
|
})
|
|
})
|
|
|
|
describe('when project not found', function () {
|
|
it('should return undefined', async function (ctx) {
|
|
ctx.Project.findOne.returns({ exec: sinon.stub().resolves(null) })
|
|
const projectId =
|
|
await ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
|
|
'token'
|
|
)
|
|
|
|
expect(projectId).to.equal(undefined)
|
|
})
|
|
})
|
|
|
|
describe('when project find returns error', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.Project.findOne.returns({ exec: sinon.stub().rejects() })
|
|
})
|
|
|
|
it('should be rejected', function (ctx) {
|
|
expect(
|
|
ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken('token')
|
|
).to.be.rejected
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('findUsersProjectsByName', function () {
|
|
it('should perform a case-insensitive search', async function (ctx) {
|
|
ctx.project1 = { _id: 1, name: 'find me!' }
|
|
ctx.project2 = { _id: 2, name: 'not me!' }
|
|
ctx.project3 = { _id: 3, name: 'FIND ME!' }
|
|
ctx.project4 = { _id: 4, name: 'Find Me!' }
|
|
ctx.Project.find.withArgs({ owner_ref: ctx.userId }).returns({
|
|
exec: sinon
|
|
.stub()
|
|
.resolves([ctx.project1, ctx.project2, ctx.project3, ctx.project4]),
|
|
})
|
|
const projects = await ctx.ProjectGetter.promises.findUsersProjectsByName(
|
|
ctx.userId,
|
|
ctx.project1.name
|
|
)
|
|
const projectNames = projects.map(project => project.name)
|
|
expect(projectNames).to.have.members([
|
|
ctx.project1.name,
|
|
ctx.project3.name,
|
|
ctx.project4.name,
|
|
])
|
|
})
|
|
|
|
it('should search collaborations as well', async function (ctx) {
|
|
ctx.project1 = { _id: 1, name: 'find me!' }
|
|
ctx.project2 = { _id: 2, name: 'FIND ME!' }
|
|
ctx.project3 = { _id: 3, name: 'Find Me!' }
|
|
ctx.project4 = { _id: 4, name: 'find ME!' }
|
|
ctx.project5 = { _id: 5, name: 'FIND me!' }
|
|
ctx.Project.find
|
|
.withArgs({ owner_ref: ctx.userId })
|
|
.returns({ exec: sinon.stub().resolves([ctx.project1]) })
|
|
ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
|
|
readAndWrite: [ctx.project2],
|
|
readOnly: [ctx.project3],
|
|
tokenReadAndWrite: [ctx.project4],
|
|
tokenReadOnly: [ctx.project5],
|
|
})
|
|
const projects = await ctx.ProjectGetter.promises.findUsersProjectsByName(
|
|
ctx.userId,
|
|
ctx.project1.name
|
|
)
|
|
expect(projects.map(project => project.name)).to.have.members([
|
|
ctx.project1.name,
|
|
ctx.project2.name,
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('getUsersDeletedProjects', function () {
|
|
it('should look up the deleted projects by deletedProjectOwnerId', async function (ctx) {
|
|
await ctx.ProjectGetter.promises.getUsersDeletedProjects('giraffe')
|
|
sinon.assert.calledWith(ctx.DeletedProject.find, {
|
|
'deleterData.deletedProjectOwnerId': 'giraffe',
|
|
})
|
|
})
|
|
|
|
it('should pass the found projects to the callback', async function (ctx) {
|
|
const docs =
|
|
await ctx.ProjectGetter.promises.getUsersDeletedProjects('giraffe')
|
|
expect(docs).to.deep.equal([ctx.deletedProject])
|
|
})
|
|
})
|
|
|
|
describe('findAllDebugProjects', function () {
|
|
it('should find all projects with overleaf.isDebugCopyOf of type objectId', async function (ctx) {
|
|
await ctx.ProjectGetter.promises.findAllDebugProjects('fields')
|
|
sinon.assert.calledWith(ctx.Project.find, {
|
|
'overleaf.isDebugCopyOf': { $type: 'objectId' },
|
|
})
|
|
sinon.assert.calledWith(ctx.Project.find().populate, 'owner_ref', [
|
|
'email',
|
|
'name',
|
|
])
|
|
sinon.assert.calledOnce(ctx.Project.find().exec)
|
|
})
|
|
})
|
|
|
|
describe('existUsersDebugProjectsOlderThan', function () {
|
|
it('should check for existence of debug projects older than given days', async function (ctx) {
|
|
const days = 10
|
|
const cutoffDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
|
|
|
|
const exists =
|
|
await ctx.ProjectGetter.promises.existUsersDebugProjectsOlderThan(
|
|
ctx.userId,
|
|
days
|
|
)
|
|
|
|
sinon.assert.calledWith(ctx.Project.exists, {
|
|
owner_ref: ctx.userId,
|
|
'overleaf.isDebugCopyOf': { $type: 'objectId' },
|
|
lastUpdated: { $lt: cutoffDate },
|
|
})
|
|
|
|
expect(exists).to.equal(true)
|
|
})
|
|
})
|
|
})
|