Files
Verso/services/web/test/unit/src/Project/ProjectGetter.test.mjs
T
Jakob Ackermann cb0266035d [web] remove unnecessary filtering of rootFolder (#31585)
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
2026-02-19 09:06:13 +00:00

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)
})
})
})