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