Convert tests to ESM

GitOrigin-RevId: 20585e01dee90e691476a0d47fd5c63b0412e4a6
This commit is contained in:
Andrew Rumble
2025-10-17 10:50:36 +01:00
committed by Copybot
parent b0a80a2f3c
commit 339a7b91ed
37 changed files with 9076 additions and 8299 deletions

View File

@@ -1,4 +1,4 @@
import { expect, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as path from 'node:path'
import sinon from 'sinon'
import MockResponse from '../../../../../test/unit/src/helpers/MockResponse.js'

View File

@@ -476,13 +476,10 @@ describe('CollaboratorsController', function () {
})
it('returns 204 on success', async function (ctx) {
await new Promise(resolve => {
ctx.res.sendStatus = status => {
expect(status).to.equal(204)
resolve()
}
ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res)
})
ctx.res.sendStatus = vi.fn()
await ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res)
expect(ctx.res.sendStatus).toHaveBeenCalledWith(204)
})
it('returns 404 if the project does not exist', async function (ctx) {

View File

@@ -1,46 +1,53 @@
const Path = require('path')
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const { ObjectId } = require('mongodb-legacy')
const { Project } = require('../helpers/models/Project')
const Errors = require('../../../../app/src/Features/Errors/Errors')
import { vi, expect } from 'vitest'
import Path from 'path'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'
import indirectlyImportModels from '../helpers/indirectlyImportModels.js'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
const { Project } = indirectlyImportModels(['Project'])
const { ObjectId } = mongodb
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
const MODULE_PATH = Path.join(
__dirname,
import.meta.dirname,
'../../../../app/src/Features/Collaborators/CollaboratorsGetter'
)
describe('CollaboratorsGetter', function () {
beforeEach(function () {
this.userId = 'efb93a186e9a06f15fea5abd'
this.ownerRef = new ObjectId()
this.readOnlyRef1 = new ObjectId()
this.readOnlyRef2 = new ObjectId()
this.pendingEditorRef = new ObjectId()
this.pendingReviewerRef = new ObjectId()
this.readWriteRef1 = new ObjectId()
this.readWriteRef2 = new ObjectId()
this.reviewer1Ref = new ObjectId()
this.reviewer2Ref = new ObjectId()
this.readOnlyTokenRef = new ObjectId()
this.readWriteTokenRef = new ObjectId()
this.nonMemberRef = new ObjectId()
this.project = {
beforeEach(async function (ctx) {
ctx.userId = 'efb93a186e9a06f15fea5abd'
ctx.ownerRef = new ObjectId()
ctx.readOnlyRef1 = new ObjectId()
ctx.readOnlyRef2 = new ObjectId()
ctx.pendingEditorRef = new ObjectId()
ctx.pendingReviewerRef = new ObjectId()
ctx.readWriteRef1 = new ObjectId()
ctx.readWriteRef2 = new ObjectId()
ctx.reviewer1Ref = new ObjectId()
ctx.reviewer2Ref = new ObjectId()
ctx.readOnlyTokenRef = new ObjectId()
ctx.readWriteTokenRef = new ObjectId()
ctx.nonMemberRef = new ObjectId()
ctx.project = {
_id: new ObjectId(),
owner_ref: [this.ownerRef],
owner_ref: [ctx.ownerRef],
readOnly_refs: [
this.readOnlyRef1,
this.readOnlyRef2,
this.pendingEditorRef,
this.pendingReviewerRef,
ctx.readOnlyRef1,
ctx.readOnlyRef2,
ctx.pendingEditorRef,
ctx.pendingReviewerRef,
],
pendingEditor_refs: [this.pendingEditorRef],
pendingReviewer_refs: [this.pendingReviewerRef],
collaberator_refs: [this.readWriteRef1, this.readWriteRef2],
reviewer_refs: [this.reviewer1Ref, this.reviewer2Ref],
tokenAccessReadAndWrite_refs: [this.readWriteTokenRef],
tokenAccessReadOnly_refs: [this.readOnlyTokenRef],
pendingEditor_refs: [ctx.pendingEditorRef],
pendingReviewer_refs: [ctx.pendingReviewerRef],
collaberator_refs: [ctx.readWriteRef1, ctx.readWriteRef2],
reviewer_refs: [ctx.reviewer1Ref, ctx.reviewer2Ref],
tokenAccessReadAndWrite_refs: [ctx.readWriteTokenRef],
tokenAccessReadOnly_refs: [ctx.readOnlyTokenRef],
publicAccesLevel: 'tokenBased',
tokens: {
readOnly: 'ro',
@@ -49,98 +56,114 @@ describe('CollaboratorsGetter', function () {
},
}
this.UserGetter = {
ctx.UserGetter = {
promises: {
getUser: sinon.stub().resolves(null),
getUsers: sinon.stub().resolves([]),
},
}
this.ProjectMock = sinon.mock(Project)
this.ProjectGetter = {
ctx.ProjectMock = sinon.mock(Project)
ctx.ProjectGetter = {
promises: {
getProject: sinon.stub().resolves(this.project),
getProject: sinon.stub().resolves(ctx.project),
},
}
this.ProjectEditorHandler = {
ctx.ProjectEditorHandler = {
buildUserModelView: sinon.stub(),
}
this.CollaboratorsGetter = SandboxedModule.require(MODULE_PATH, {
requires: {
'mongodb-legacy': { ObjectId },
'../User/UserGetter': this.UserGetter,
'../../models/Project': { Project },
'../Project/ProjectGetter': this.ProjectGetter,
'../Project/ProjectEditorHandler': this.ProjectEditorHandler,
},
})
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
vi.doMock('../../../../app/src/models/Project', () => ({
Project,
}))
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: ctx.ProjectGetter,
}))
vi.doMock(
'../../../../app/src/Features/Project/ProjectEditorHandler',
() => ({
default: ctx.ProjectEditorHandler,
})
)
ctx.CollaboratorsGetter = (await import(MODULE_PATH)).default
})
afterEach(function () {
this.ProjectMock.verify()
afterEach(function (ctx) {
ctx.ProjectMock.verify()
})
describe('getMemberIdsWithPrivilegeLevels', function () {
describe('with project', function () {
it('should return an array of member ids with their privilege levels', async function () {
it('should return an array of member ids with their privilege levels', async function (ctx) {
const result =
await this.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels(
this.project._id
await ctx.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels(
ctx.project._id
)
expect(result).to.have.deep.members([
{
id: this.ownerRef.toString(),
id: ctx.ownerRef.toString(),
privilegeLevel: 'owner',
source: 'owner',
},
{
id: this.readWriteRef1.toString(),
id: ctx.readWriteRef1.toString(),
privilegeLevel: 'readAndWrite',
source: 'invite',
},
{
id: this.readWriteRef2.toString(),
id: ctx.readWriteRef2.toString(),
privilegeLevel: 'readAndWrite',
source: 'invite',
},
{
id: this.readOnlyRef1.toString(),
id: ctx.readOnlyRef1.toString(),
privilegeLevel: 'readOnly',
source: 'invite',
},
{
id: this.readOnlyRef2.toString(),
id: ctx.readOnlyRef2.toString(),
privilegeLevel: 'readOnly',
source: 'invite',
},
{
id: this.pendingEditorRef.toString(),
id: ctx.pendingEditorRef.toString(),
privilegeLevel: 'readOnly',
source: 'invite',
pendingEditor: true,
},
{
id: this.pendingReviewerRef.toString(),
id: ctx.pendingReviewerRef.toString(),
privilegeLevel: 'readOnly',
source: 'invite',
pendingReviewer: true,
},
{
id: this.readOnlyTokenRef.toString(),
id: ctx.readOnlyTokenRef.toString(),
privilegeLevel: 'readOnly',
source: 'token',
},
{
id: this.readWriteTokenRef.toString(),
id: ctx.readWriteTokenRef.toString(),
privilegeLevel: 'readAndWrite',
source: 'token',
},
{
id: this.reviewer1Ref.toString(),
id: ctx.reviewer1Ref.toString(),
privilegeLevel: 'review',
source: 'invite',
},
{
id: this.reviewer2Ref.toString(),
id: ctx.reviewer2Ref.toString(),
privilegeLevel: 'review',
source: 'invite',
},
@@ -149,14 +172,14 @@ describe('CollaboratorsGetter', function () {
})
describe('with a missing project', function () {
beforeEach(function () {
this.ProjectGetter.promises.getProject.resolves(null)
beforeEach(function (ctx) {
ctx.ProjectGetter.promises.getProject.resolves(null)
})
it('should return a NotFoundError', async function () {
it('should return a NotFoundError', async function (ctx) {
await expect(
this.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels(
this.project._id
ctx.CollaboratorsGetter.promises.getMemberIdsWithPrivilegeLevels(
ctx.project._id
)
).to.be.rejectedWith(Errors.NotFoundError)
})
@@ -164,89 +187,89 @@ describe('CollaboratorsGetter', function () {
})
describe('getMemberIds', function () {
it('should return the ids', async function () {
const memberIds = await this.CollaboratorsGetter.promises.getMemberIds(
this.project._id
it('should return the ids', async function (ctx) {
const memberIds = await ctx.CollaboratorsGetter.promises.getMemberIds(
ctx.project._id
)
expect(memberIds).to.have.members([
this.ownerRef.toString(),
this.readOnlyRef1.toString(),
this.readOnlyRef2.toString(),
this.readWriteRef1.toString(),
this.readWriteRef2.toString(),
this.pendingEditorRef.toString(),
this.pendingReviewerRef.toString(),
this.readWriteTokenRef.toString(),
this.readOnlyTokenRef.toString(),
this.reviewer1Ref.toString(),
this.reviewer2Ref.toString(),
ctx.ownerRef.toString(),
ctx.readOnlyRef1.toString(),
ctx.readOnlyRef2.toString(),
ctx.readWriteRef1.toString(),
ctx.readWriteRef2.toString(),
ctx.pendingEditorRef.toString(),
ctx.pendingReviewerRef.toString(),
ctx.readWriteTokenRef.toString(),
ctx.readOnlyTokenRef.toString(),
ctx.reviewer1Ref.toString(),
ctx.reviewer2Ref.toString(),
])
})
})
describe('getInvitedMemberIds', function () {
it('should return the invited ids', async function () {
it('should return the invited ids', async function (ctx) {
const memberIds =
await this.CollaboratorsGetter.promises.getInvitedMemberIds(
this.project._id
await ctx.CollaboratorsGetter.promises.getInvitedMemberIds(
ctx.project._id
)
expect(memberIds).to.have.members([
this.ownerRef.toString(),
this.readOnlyRef1.toString(),
this.readOnlyRef2.toString(),
this.readWriteRef1.toString(),
this.readWriteRef2.toString(),
this.pendingEditorRef.toString(),
this.pendingReviewerRef.toString(),
this.reviewer1Ref.toString(),
this.reviewer2Ref.toString(),
ctx.ownerRef.toString(),
ctx.readOnlyRef1.toString(),
ctx.readOnlyRef2.toString(),
ctx.readWriteRef1.toString(),
ctx.readWriteRef2.toString(),
ctx.pendingEditorRef.toString(),
ctx.pendingReviewerRef.toString(),
ctx.reviewer1Ref.toString(),
ctx.reviewer2Ref.toString(),
])
})
})
describe('getMemberIdPrivilegeLevel', function () {
it('should return the privilege level if it exists', async function () {
it('should return the privilege level if it exists', async function (ctx) {
const level =
await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
this.readOnlyRef1,
this.project._id
await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
ctx.readOnlyRef1,
ctx.project._id
)
expect(level).to.equal('readOnly')
})
it('should return review privilege level', async function () {
it('should return review privilege level', async function (ctx) {
const level =
await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
this.reviewer1Ref,
this.project._id
await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
ctx.reviewer1Ref,
ctx.project._id
)
expect(level).to.equal('review')
})
it('should return false if the member has no privilege level', async function () {
it('should return false if the member has no privilege level', async function (ctx) {
const level =
await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
this.nonMemberRef,
this.project._id
await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
ctx.nonMemberRef,
ctx.project._id
)
expect(level).to.equal(false)
})
it('should return review privilege level when user is both reviewer and token member', async function () {
it('should return review privilege level when user is both reviewer and token member', async function (ctx) {
const userWhoIsBothReviewerAndToken = new ObjectId()
const projectWithDuplicateUser = {
...this.project,
...ctx.project,
reviewer_refs: [userWhoIsBothReviewerAndToken],
tokenAccessReadAndWrite_refs: [userWhoIsBothReviewerAndToken],
}
this.ProjectGetter.promises.getProject.resolves(projectWithDuplicateUser)
ctx.ProjectGetter.promises.getProject.resolves(projectWithDuplicateUser)
const level =
await this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
await ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
userWhoIsBothReviewerAndToken,
this.project._id
ctx.project._id
)
expect(level).to.equal('review')
@@ -255,20 +278,20 @@ describe('CollaboratorsGetter', function () {
describe('isUserInvitedMemberOfProject', function () {
describe('when user is a member of the project', function () {
it('should return true and the privilegeLevel', async function () {
it('should return true and the privilegeLevel', async function (ctx) {
const isMember =
await this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
this.readOnlyRef1
await ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
ctx.readOnlyRef1
)
expect(isMember).to.equal(true)
})
})
describe('when user is not a member of the project', function () {
it('should return false', async function () {
it('should return false', async function (ctx) {
const isMember =
await this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
this.nonMemberRef
await ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
ctx.nonMemberRef
)
expect(isMember).to.equal(false)
})
@@ -277,30 +300,30 @@ describe('CollaboratorsGetter', function () {
describe('isUserInvitedReadWriteMemberOfProject', function () {
describe('when user is a read write member of the project', function () {
it('should return true', async function () {
it('should return true', async function (ctx) {
const isMember =
await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
this.readWriteRef1
await ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
ctx.readWriteRef1
)
expect(isMember).to.equal(true)
})
})
describe('when user is a read only member of the project', function () {
it('should return false', async function () {
it('should return false', async function (ctx) {
const isMember =
await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
this.readOnlyRef1
await ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
ctx.readOnlyRef1
)
expect(isMember).to.equal(false)
})
})
describe('when user is not a member of the project', function () {
it('should return false', async function () {
it('should return false', async function (ctx) {
const isMember =
await this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
this.nonMemberRef
await ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
ctx.nonMemberRef
)
expect(isMember).to.equal(false)
})
@@ -308,42 +331,42 @@ describe('CollaboratorsGetter', function () {
})
describe('getProjectsUserIsMemberOf', function () {
beforeEach(function () {
this.fields = 'mock fields'
this.ProjectMock.expects('find')
.withArgs({ collaberator_refs: this.userId }, this.fields)
beforeEach(function (ctx) {
ctx.fields = 'mock fields'
ctx.ProjectMock.expects('find')
.withArgs({ collaberator_refs: ctx.userId }, ctx.fields)
.chain('exec')
.resolves(['mock-read-write-project-1', 'mock-read-write-project-2'])
this.ProjectMock.expects('find')
.withArgs({ readOnly_refs: this.userId }, this.fields)
ctx.ProjectMock.expects('find')
.withArgs({ readOnly_refs: ctx.userId }, ctx.fields)
.chain('exec')
.resolves(['mock-read-only-project-1', 'mock-read-only-project-2'])
this.ProjectMock.expects('find')
.withArgs({ reviewer_refs: this.userId }, this.fields)
ctx.ProjectMock.expects('find')
.withArgs({ reviewer_refs: ctx.userId }, ctx.fields)
.chain('exec')
.resolves(['mock-review-project-1', 'mock-review-project-2'])
this.ProjectMock.expects('find')
ctx.ProjectMock.expects('find')
.withArgs(
{
tokenAccessReadAndWrite_refs: this.userId,
tokenAccessReadAndWrite_refs: ctx.userId,
publicAccesLevel: 'tokenBased',
},
this.fields
ctx.fields
)
.chain('exec')
.resolves([
'mock-token-read-write-project-1',
'mock-token-read-write-project-2',
])
this.ProjectMock.expects('find')
ctx.ProjectMock.expects('find')
.withArgs(
{
tokenAccessReadOnly_refs: this.userId,
tokenAccessReadOnly_refs: ctx.userId,
publicAccesLevel: 'tokenBased',
},
this.fields
ctx.fields
)
.chain('exec')
.resolves([
@@ -352,11 +375,11 @@ describe('CollaboratorsGetter', function () {
])
})
it('should call the callback with the projects', async function () {
it('should call the callback with the projects', async function (ctx) {
const projects =
await this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf(
this.userId,
this.fields
await ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf(
ctx.userId,
ctx.fields
)
expect(projects).to.deep.equal({
readAndWrite: [
@@ -378,101 +401,98 @@ describe('CollaboratorsGetter', function () {
})
describe('getAllInvitedMembers', function () {
beforeEach(async function () {
this.owningUser = {
_id: this.ownerRef,
beforeEach(async function (ctx) {
ctx.owningUser = {
_id: ctx.ownerRef,
email: 'owner@example.com',
features: { a: 1 },
}
this.readWriteUser = {
_id: this.readWriteRef1,
ctx.readWriteUser = {
_id: ctx.readWriteRef1,
email: 'readwrite@example.com',
}
this.reviewUser = {
_id: this.reviewer1Ref,
ctx.reviewUser = {
_id: ctx.reviewer1Ref,
email: 'review@example.com',
}
this.members = [
{ user: this.owningUser, privilegeLevel: 'owner' },
{ user: this.readWriteUser, privilegeLevel: 'readAndWrite' },
{ user: this.reviewUser, privilegeLevel: 'review' },
ctx.members = [
{ user: ctx.owningUser, privilegeLevel: 'owner' },
{ user: ctx.readWriteUser, privilegeLevel: 'readAndWrite' },
{ user: ctx.reviewUser, privilegeLevel: 'review' },
]
this.memberViews = [
{ _id: this.readWriteUser._id, email: this.readWriteUser.email },
{ _id: this.reviewUser._id, email: this.reviewUser.email },
ctx.memberViews = [
{ _id: ctx.readWriteUser._id, email: ctx.readWriteUser.email },
{ _id: ctx.reviewUser._id, email: ctx.reviewUser.email },
]
this.UserGetter.promises.getUsers.resolves([
this.owningUser,
this.readWriteUser,
this.reviewUser,
ctx.UserGetter.promises.getUsers.resolves([
ctx.owningUser,
ctx.readWriteUser,
ctx.reviewUser,
])
this.ProjectEditorHandler.buildUserModelView
.withArgs(this.members[1])
.returns(this.memberViews[0])
this.ProjectEditorHandler.buildUserModelView
.withArgs(this.members[2])
.returns(this.memberViews[1])
this.result =
await this.CollaboratorsGetter.promises.getAllInvitedMembers(
this.project._id
)
ctx.ProjectEditorHandler.buildUserModelView
.withArgs(ctx.members[1])
.returns(ctx.memberViews[0])
ctx.ProjectEditorHandler.buildUserModelView
.withArgs(ctx.members[2])
.returns(ctx.memberViews[1])
ctx.result = await ctx.CollaboratorsGetter.promises.getAllInvitedMembers(
ctx.project._id
)
})
it('should produce a list of members', function () {
expect(this.result).to.deep.equal(this.memberViews)
it('should produce a list of members', function (ctx) {
expect(ctx.result).to.deep.equal(ctx.memberViews)
})
it('should call ProjectEditorHandler.buildUserModelView', function () {
expect(this.ProjectEditorHandler.buildUserModelView).to.have.been
it('should call ProjectEditorHandler.buildUserModelView', function (ctx) {
expect(ctx.ProjectEditorHandler.buildUserModelView).to.have.been
.calledTwice
expect(
this.ProjectEditorHandler.buildUserModelView
).to.have.been.calledWith(this.members[1])
ctx.ProjectEditorHandler.buildUserModelView
).to.have.been.calledWith(ctx.members[1])
expect(
this.ProjectEditorHandler.buildUserModelView
).to.have.been.calledWith(this.members[2])
ctx.ProjectEditorHandler.buildUserModelView
).to.have.been.calledWith(ctx.members[2])
})
})
describe('userIsTokenMember', function () {
it('should return true when the project is found', async function () {
this.ProjectMock.expects('findOne').chain('exec').resolves(this.project)
const isMember =
await this.CollaboratorsGetter.promises.userIsTokenMember(
this.userId,
this.project._id
)
it('should return true when the project is found', async function (ctx) {
ctx.ProjectMock.expects('findOne').chain('exec').resolves(ctx.project)
const isMember = await ctx.CollaboratorsGetter.promises.userIsTokenMember(
ctx.userId,
ctx.project._id
)
expect(isMember).to.be.true
})
it('should return false when the project is not found', async function () {
this.ProjectMock.expects('findOne').chain('exec').resolves(null)
const isMember =
await this.CollaboratorsGetter.promises.userIsTokenMember(
this.userId,
this.project._id
)
it('should return false when the project is not found', async function (ctx) {
ctx.ProjectMock.expects('findOne').chain('exec').resolves(null)
const isMember = await ctx.CollaboratorsGetter.promises.userIsTokenMember(
ctx.userId,
ctx.project._id
)
expect(isMember).to.be.false
})
})
describe('userIsReadWriteTokenMember', function () {
it('should return true when the project is found', async function () {
this.ProjectMock.expects('findOne').chain('exec').resolves(this.project)
it('should return true when the project is found', async function (ctx) {
ctx.ProjectMock.expects('findOne').chain('exec').resolves(ctx.project)
const isMember =
await this.CollaboratorsGetter.promises.userIsReadWriteTokenMember(
this.userId,
this.project._id
await ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember(
ctx.userId,
ctx.project._id
)
expect(isMember).to.be.true
})
it('should return false when the project is not found', async function () {
this.ProjectMock.expects('findOne').chain('exec').resolves(null)
it('should return false when the project is not found', async function (ctx) {
ctx.ProjectMock.expects('findOne').chain('exec').resolves(null)
const isMember =
await this.CollaboratorsGetter.promises.userIsReadWriteTokenMember(
this.userId,
this.project._id
await ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember(
ctx.userId,
ctx.project._id
)
expect(isMember).to.be.false
})
@@ -481,53 +501,53 @@ describe('CollaboratorsGetter', function () {
describe('getPublicShareTokens', function () {
const userMock = new ObjectId()
it('should return null when the project is not found', async function () {
this.ProjectMock.expects('findOne').chain('exec').resolves(undefined)
it('should return null when the project is not found', async function (ctx) {
ctx.ProjectMock.expects('findOne').chain('exec').resolves(undefined)
const tokens =
await this.CollaboratorsGetter.promises.getPublicShareTokens(
await ctx.CollaboratorsGetter.promises.getPublicShareTokens(
userMock,
this.project._id
ctx.project._id
)
expect(tokens).to.be.null
})
it('should return an empty object when the user is not owner or read-only collaborator', async function () {
this.ProjectMock.expects('findOne').chain('exec').resolves(this.project)
it('should return an empty object when the user is not owner or read-only collaborator', async function (ctx) {
ctx.ProjectMock.expects('findOne').chain('exec').resolves(ctx.project)
const tokens =
await this.CollaboratorsGetter.promises.getPublicShareTokens(
await ctx.CollaboratorsGetter.promises.getPublicShareTokens(
userMock,
this.project._id
ctx.project._id
)
expect(tokens).to.deep.equal({})
})
describe('when the user is a read-only token collaborator', function () {
it('should return the read-only token', async function () {
this.ProjectMock.expects('findOne')
it('should return the read-only token', async function (ctx) {
ctx.ProjectMock.expects('findOne')
.chain('exec')
.resolves({ hasTokenReadOnlyAccess: true, ...this.project })
.resolves({ hasTokenReadOnlyAccess: true, ...ctx.project })
const tokens =
await this.CollaboratorsGetter.promises.getPublicShareTokens(
await ctx.CollaboratorsGetter.promises.getPublicShareTokens(
userMock,
this.project._id
ctx.project._id
)
expect(tokens).to.deep.equal({ readOnly: tokens.readOnly })
})
})
describe('when the user is the owner of the project', function () {
beforeEach(function () {
this.ProjectMock.expects('findOne')
beforeEach(function (ctx) {
ctx.ProjectMock.expects('findOne')
.chain('exec')
.resolves({ isOwner: true, ...this.project })
.resolves({ isOwner: true, ...ctx.project })
})
it('should return all the tokens', async function () {
it('should return all the tokens', async function (ctx) {
const tokens =
await this.CollaboratorsGetter.promises.getPublicShareTokens(
await ctx.CollaboratorsGetter.promises.getPublicShareTokens(
userMock,
this.project._id
ctx.project._id
)
expect(tokens).to.deep.equal(tokens)
})
@@ -535,20 +555,20 @@ describe('CollaboratorsGetter', function () {
})
describe('getInvitedEditCollaboratorCount', function () {
it('should return the count of invited edit collaborators (readAndWrite, review)', async function () {
it('should return the count of invited edit collaborators (readAndWrite, review)', async function (ctx) {
const count =
await this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
this.project._id
await ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
ctx.project._id
)
expect(count).to.equal(4)
})
})
describe('getInvitedPendingEditorCount', function () {
it('should return the count of pending editors and reviewers', async function () {
it('should return the count of pending editors and reviewers', async function (ctx) {
const count =
await this.CollaboratorsGetter.promises.getInvitedPendingEditorCount(
this.project._id
await ctx.CollaboratorsGetter.promises.getInvitedPendingEditorCount(
ctx.project._id
)
expect(count).to.equal(2)
})
@@ -556,11 +576,11 @@ describe('CollaboratorsGetter', function () {
describe('ProjectAccess', function () {
describe('privilegeLevelForUser', function () {
it('should return reviewer privilege when user is both reviewer and token member', function () {
it('should return reviewer privilege when user is both reviewer and token member', function (ctx) {
const userWhoIsBothReviewerAndToken = new ObjectId()
const projectWithDuplicateUser = {
owner_ref: this.ownerRef,
owner_ref: ctx.ownerRef,
collaberator_refs: [],
readOnly_refs: [],
tokenAccessReadAndWrite_refs: [userWhoIsBothReviewerAndToken],
@@ -571,7 +591,7 @@ describe('CollaboratorsGetter', function () {
pendingReviewer_refs: [],
}
const projectAccess = new this.CollaboratorsGetter.ProjectAccess(
const projectAccess = new ctx.CollaboratorsGetter.ProjectAccess(
projectWithDuplicateUser
)
const privilegeLevel = projectAccess.privilegeLevelForUser(
@@ -581,11 +601,11 @@ describe('CollaboratorsGetter', function () {
expect(privilegeLevel).to.equal('review')
})
it('should return readOnly privilege when user is both readOnly and token readAndWrite member', function () {
it('should return readOnly privilege when user is both readOnly and token readAndWrite member', function (ctx) {
const userWhoIsBothReadOnlyAndTokenRW = new ObjectId()
const projectWithDuplicateUser = {
owner_ref: this.ownerRef,
owner_ref: ctx.ownerRef,
collaberator_refs: [],
readOnly_refs: [userWhoIsBothReadOnlyAndTokenRW],
tokenAccessReadAndWrite_refs: [userWhoIsBothReadOnlyAndTokenRW],
@@ -596,7 +616,7 @@ describe('CollaboratorsGetter', function () {
pendingReviewer_refs: [],
}
const projectAccess = new this.CollaboratorsGetter.ProjectAccess(
const projectAccess = new ctx.CollaboratorsGetter.ProjectAccess(
projectWithDuplicateUser
)
const privilegeLevel = projectAccess.privilegeLevelForUser(
@@ -607,12 +627,12 @@ describe('CollaboratorsGetter', function () {
expect(privilegeLevel).to.equal('readOnly')
})
it('should return none for non-members', function () {
const projectAccess = new this.CollaboratorsGetter.ProjectAccess(
this.project
it('should return none for non-members', function (ctx) {
const projectAccess = new ctx.CollaboratorsGetter.ProjectAccess(
ctx.project
)
const privilegeLevel = projectAccess.privilegeLevelForUser(
this.nonMemberRef
ctx.nonMemberRef
)
expect(privilegeLevel).to.equal(false)

View File

@@ -22,9 +22,12 @@ describe('DocumentUpdaterController', function () {
default: ctx.settings,
}))
vi.doMock('../../../../app/src/Features/Project/ProjectLocator.mjs', () => ({
default: ctx.ProjectLocator,
}))
vi.doMock(
'../../../../app/src/Features/Project/ProjectLocator.mjs',
() => ({
default: ctx.ProjectLocator,
})
)
vi.doMock(
'../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.mjs',

View File

@@ -43,9 +43,12 @@ describe('ProjectZipStreamManager', function () {
})
)
vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({
default: (ctx.HistoryManager = {}),
}))
vi.doMock(
'../../../../app/src/Features/History/HistoryManager.mjs',
() => ({
default: (ctx.HistoryManager = {}),
})
)
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: (ctx.ProjectGetter = {}),

File diff suppressed because it is too large Load Diff

View File

@@ -168,9 +168,12 @@ describe('EditorHttpController', function () {
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
vi.doMock('../../../../app/src/Features/Project/ProjectDeleter.mjs', () => ({
default: ctx.ProjectDeleter,
}))
vi.doMock(
'../../../../app/src/Features/Project/ProjectDeleter.mjs',
() => ({
default: ctx.ProjectDeleter,
})
)
vi.doMock('../../../../app/src/Features/Project/ProjectGetter.mjs', () => ({
default: ctx.ProjectGetter,
}))

View File

@@ -1,13 +1,13 @@
const { expect } = require('chai')
const sinon = require('sinon')
const SandboxedModule = require('sandboxed-module')
import { beforeEach, describe, it, vi, expect } from 'vitest'
import sinon from 'sinon'
const MODULE_PATH = '../../../../app/src/Features/FileStore/FileStoreHandler.js'
const MODULE_PATH =
'../../../../app/src/Features/FileStore/FileStoreHandler.mjs'
describe('FileStoreHandler', function () {
beforeEach(function () {
this.fileSize = 999
this.fs = {
beforeEach(async function (ctx) {
ctx.fileSize = 999
ctx.fs = {
createReadStream: sinon.stub(),
lstat: sinon.stub().callsArgWith(1, null, {
isFile() {
@@ -16,10 +16,10 @@ describe('FileStoreHandler', function () {
isDirectory() {
return false
},
size: this.fileSize,
size: ctx.fileSize,
}),
}
this.writeStream = {
ctx.writeStream = {
my: 'writeStream',
on(type, fn) {
if (type === 'response') {
@@ -27,25 +27,24 @@ describe('FileStoreHandler', function () {
}
},
}
this.readStream = { my: 'readStream', on: sinon.stub() }
this.request = sinon.stub()
this.request.head = sinon.stub()
this.filestoreUrl = 'http://filestore.overleaf.test'
this.settings = {
apis: { filestore: { url: this.filestoreUrl } },
ctx.readStream = { my: 'readStream', on: sinon.stub() }
ctx.request = sinon.stub()
ctx.request.head = sinon.stub()
ctx.filestoreUrl = 'http://filestore.overleaf.test'
ctx.settings = {
apis: { filestore: { url: ctx.filestoreUrl } },
}
this.hashValue = '0123456789'
this.fileArgs = { name: 'upload-filename' }
this.fileId = 'file_id_here'
this.projectId = '1312312312'
this.historyId = 123
this.hashValue = '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'
this.fsPath = 'uploads/myfile.eps'
this.getFileUrl = (projectId, fileId) =>
`${this.filestoreUrl}/project/${projectId}/file/${fileId}`
this.getProjectUrl = projectId =>
`${this.filestoreUrl}/project/${projectId}`
this.FileModel = class File {
ctx.hashValue = '0123456789'
ctx.fileArgs = { name: 'upload-filename' }
ctx.fileId = 'file_id_here'
ctx.projectId = '1312312312'
ctx.historyId = 123
ctx.hashValue = '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'
ctx.fsPath = 'uploads/myfile.eps'
ctx.getFileUrl = (projectId, fileId) =>
`${ctx.filestoreUrl}/project/${projectId}/file/${fileId}`
ctx.getProjectUrl = projectId => `${ctx.filestoreUrl}/project/${projectId}`
ctx.FileModel = class File {
constructor(options) {
;({ name: this.name, hash: this.hash } = options)
this._id = 'file_id_here'
@@ -55,53 +54,75 @@ describe('FileStoreHandler', function () {
}
}
}
this.FileHashManager = {
computeHash: sinon.stub().callsArgWith(1, null, this.hashValue),
ctx.FileHashManager = {
computeHash: sinon.stub().callsArgWith(1, null, ctx.hashValue),
}
this.HistoryManager = {
ctx.HistoryManager = {
uploadBlobFromDisk: sinon.stub().callsArg(4),
}
this.ProjectDetailsHandler = {
ctx.ProjectDetailsHandler = {
getDetails: sinon.stub().callsArgWith(1, null, {
overleaf: { history: { id: this.historyId } },
overleaf: { history: { id: ctx.historyId } },
}),
}
this.Features = {
ctx.Features = {
hasFeature: sinon.stub(),
}
this.Modules = {
ctx.Modules = {
hooks: {
fire: sinon.stub().callsArgWith(2, null),
},
}
this.handler = SandboxedModule.require(MODULE_PATH, {
requires: {
'@overleaf/settings': this.settings,
request: this.request,
'../History/HistoryManager': this.HistoryManager,
'../Project/ProjectDetailsHandler': this.ProjectDetailsHandler,
'./FileHashManager': this.FileHashManager,
'../../infrastructure/Features': this.Features,
'../../infrastructure/Modules': this.Modules,
// FIXME: need to stub File object here
'../../models/File': {
File: this.FileModel,
},
fs: this.fs,
},
})
vi.doMock('@overleaf/settings', () => ({
default: ctx.settings,
}))
vi.doMock('request', () => ({
default: ctx.request,
}))
vi.doMock('../../../../app/src/Features/History/HistoryManager', () => ({
default: ctx.HistoryManager,
}))
vi.doMock(
'../../../../app/src/Features/Project/ProjectDetailsHandler',
() => ({
default: ctx.ProjectDetailsHandler,
})
)
vi.doMock('../../../../app/src/Features/FileStore/FileHashManager', () => ({
default: ctx.FileHashManager,
}))
vi.doMock('../../../../app/src/infrastructure/Features', () => ({
default: ctx.Features,
}))
vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
default: ctx.Modules,
}))
vi.doMock('../../../../app/src/models/File', () => ({
File: ctx.FileModel,
}))
vi.doMock('node:fs', () => ({ default: ctx.fs }))
ctx.handler = (await import(MODULE_PATH)).default
})
describe('uploadFileFromDisk', function () {
beforeEach(function () {
this.request.returns(this.writeStream)
beforeEach(function (ctx) {
ctx.request.returns(ctx.writeStream)
})
it('should get the project details', async function () {
this.fs.createReadStream.returns({
it('should get the project details', async function (ctx) {
ctx.fs.createReadStream.returns({
pipe() {},
on(type, cb) {
if (type === 'open') {
@@ -109,18 +130,18 @@ describe('FileStoreHandler', function () {
}
},
})
await this.handler.promises.uploadFileFromDisk(
this.projectId,
this.fileArgs,
this.fsPath
await ctx.handler.promises.uploadFileFromDisk(
ctx.projectId,
ctx.fileArgs,
ctx.fsPath
)
this.ProjectDetailsHandler.getDetails
.calledWith(this.projectId)
ctx.ProjectDetailsHandler.getDetails
.calledWith(ctx.projectId)
.should.equal(true)
})
it('should compute the file hash', async function () {
this.fs.createReadStream.returns({
it('should compute the file hash', async function (ctx) {
ctx.fs.createReadStream.returns({
pipe() {},
on(type, cb) {
if (type === 'open') {
@@ -128,18 +149,16 @@ describe('FileStoreHandler', function () {
}
},
})
await this.handler.promises.uploadFileFromDisk(
this.projectId,
this.fileArgs,
this.fsPath
await ctx.handler.promises.uploadFileFromDisk(
ctx.projectId,
ctx.fileArgs,
ctx.fsPath
)
this.FileHashManager.computeHash
.calledWith(this.fsPath)
.should.equal(true)
ctx.FileHashManager.computeHash.calledWith(ctx.fsPath).should.equal(true)
})
it('should call the preUploadFile hook', async function () {
this.fs.createReadStream.returns({
it('should call the preUploadFile hook', async function (ctx) {
ctx.fs.createReadStream.returns({
pipe() {},
on(type, cb) {
if (type === 'open') {
@@ -147,24 +166,24 @@ describe('FileStoreHandler', function () {
}
},
})
await this.handler.promises.uploadFileFromDisk(
this.projectId,
this.fileArgs,
this.fsPath
await ctx.handler.promises.uploadFileFromDisk(
ctx.projectId,
ctx.fileArgs,
ctx.fsPath
)
this.Modules.hooks.fire
ctx.Modules.hooks.fire
.calledWith('preUploadFile', {
projectId: this.projectId,
historyId: this.historyId,
fileArgs: this.fileArgs,
fsPath: this.fsPath,
size: this.fileSize,
projectId: ctx.projectId,
historyId: ctx.historyId,
fileArgs: ctx.fileArgs,
fsPath: ctx.fsPath,
size: ctx.fileSize,
})
.should.equal(true)
})
it('should upload the file to the history store as a blob', async function () {
this.fs.createReadStream.returns({
it('should upload the file to the history store as a blob', async function (ctx) {
ctx.fs.createReadStream.returns({
pipe() {},
on(type, cb) {
if (type === 'open') {
@@ -172,37 +191,37 @@ describe('FileStoreHandler', function () {
}
},
})
await this.handler.promises.uploadFileFromDisk(
this.projectId,
this.fileArgs,
this.fsPath
await ctx.handler.promises.uploadFileFromDisk(
ctx.projectId,
ctx.fileArgs,
ctx.fsPath
)
this.HistoryManager.uploadBlobFromDisk
.calledWith(this.historyId, this.hashValue, this.fileSize, this.fsPath)
ctx.HistoryManager.uploadBlobFromDisk
.calledWith(ctx.historyId, ctx.hashValue, ctx.fileSize, ctx.fsPath)
.should.equal(true)
})
it('should not open file handle', async function () {
await this.handler.promises.uploadFileFromDisk(
this.projectId,
this.fileArgs,
this.fsPath
it('should not open file handle', async function (ctx) {
await ctx.handler.promises.uploadFileFromDisk(
ctx.projectId,
ctx.fileArgs,
ctx.fsPath
)
expect(this.fs.createReadStream).to.not.have.been.called
expect(ctx.fs.createReadStream).to.not.have.been.called
})
it('should not talk to filestore', async function () {
await this.handler.promises.uploadFileFromDisk(
this.projectId,
this.fileArgs,
this.fsPath
it('should not talk to filestore', async function (ctx) {
await ctx.handler.promises.uploadFileFromDisk(
ctx.projectId,
ctx.fileArgs,
ctx.fsPath
)
expect(this.request).to.not.have.been.called
expect(ctx.request).to.not.have.been.called
})
it('should call the postUploadFile hook', async function () {
this.fs.createReadStream.returns({
it('should call the postUploadFile hook', async function (ctx) {
ctx.fs.createReadStream.returns({
pipe() {},
on(type, cb) {
if (type === 'open') {
@@ -210,33 +229,33 @@ describe('FileStoreHandler', function () {
}
},
})
await this.handler.promises.uploadFileFromDisk(
this.projectId,
this.fileArgs,
this.fsPath
await ctx.handler.promises.uploadFileFromDisk(
ctx.projectId,
ctx.fileArgs,
ctx.fsPath
)
this.Modules.hooks.fire
ctx.Modules.hooks.fire
.calledWith('postUploadFile', {
projectId: this.projectId,
fileRef: sinon.match.instanceOf(this.FileModel),
size: this.fileSize,
projectId: ctx.projectId,
fileRef: sinon.match.instanceOf(ctx.FileModel),
size: ctx.fileSize,
})
.should.equal(true)
})
it('should resolve with the url and fileRef', async function () {
const { fileRef } = await this.handler.promises.uploadFileFromDisk(
this.projectId,
this.fileArgs,
this.fsPath
it('should resolve with the url and fileRef', async function (ctx) {
const { fileRef } = await ctx.handler.promises.uploadFileFromDisk(
ctx.projectId,
ctx.fileArgs,
ctx.fsPath
)
expect(fileRef._id).to.equal(this.fileId)
expect(fileRef.hash).to.equal(this.hashValue)
expect(fileRef._id).to.equal(ctx.fileId)
expect(fileRef.hash).to.equal(ctx.hashValue)
})
describe('symlink', function () {
it('should not read file if it is symlink', async function () {
this.fs.lstat = sinon.stub().callsArgWith(1, null, {
it('should not read file if it is symlink', async function (ctx) {
ctx.fs.lstat = sinon.stub().callsArgWith(1, null, {
isFile() {
return false
},
@@ -248,10 +267,10 @@ describe('FileStoreHandler', function () {
let error
try {
await this.handler.promises.uploadFileFromDisk(
this.projectId,
this.fileArgs,
this.fsPath
await ctx.handler.promises.uploadFileFromDisk(
ctx.projectId,
ctx.fileArgs,
ctx.fsPath
)
} catch (err) {
error = err
@@ -259,18 +278,18 @@ describe('FileStoreHandler', function () {
expect(error).to.exist
this.fs.createReadStream.called.should.equal(false)
ctx.fs.createReadStream.called.should.equal(false)
})
it('should not read file stat returns nothing', async function () {
this.fs.lstat = sinon.stub().callsArgWith(1, null, null)
it('should not read file stat returns nothing', async function (ctx) {
ctx.fs.lstat = sinon.stub().callsArgWith(1, null, null)
let error
try {
await this.handler.promises.uploadFileFromDisk(
this.projectId,
this.fileArgs,
this.fsPath
await ctx.handler.promises.uploadFileFromDisk(
ctx.projectId,
ctx.fileArgs,
ctx.fsPath
)
} catch (err) {
error = err
@@ -278,7 +297,7 @@ describe('FileStoreHandler', function () {
expect(error).to.exist
this.fs.createReadStream.called.should.equal(false)
ctx.fs.createReadStream.called.should.equal(false)
})
})
})

View File

@@ -1,32 +1,35 @@
const { expect } = require('chai')
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const MockRequest = require('../helpers/MockRequest')
const MockResponse = require('../helpers/MockResponse')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import MockRequest from '../helpers/MockRequest.js'
import MockResponse from '../helpers/MockResponse.js'
const modulePath =
'../../../../app/src/Features/Helpers/AdminAuthorizationHelper'
describe('AdminAuthorizationHelper', function () {
beforeEach(function () {
this.fireHook = sinon.stub().resolves([])
this.settings = {
beforeEach(async function (ctx) {
ctx.fireHook = sinon.stub().resolves([])
ctx.settings = {
adminPrivilegeAvailable: true,
adminUrl: 'https://admin.overleaf.com',
adminRolesEnabled: true,
}
this.AdminAuthorizationHelper = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/settings': this.settings,
'../../infrastructure/Modules': {
promises: {
hooks: {
fire: this.fireHook,
},
vi.doMock('@overleaf/settings', () => ({
default: ctx.settings,
}))
vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
default: {
promises: {
hooks: {
fire: ctx.fireHook,
},
},
},
})
}))
ctx.AdminAuthorizationHelper = (await import(modulePath)).default
})
describe('getAdminCapabilities', function () {
describe('when modules return capabilities', function () {
@@ -34,9 +37,9 @@ describe('AdminAuthorizationHelper', function () {
const module1Capabilities = ['capability1', 'capability2']
const module2Capabilities = ['capability2', 'capability3']
beforeEach(async function () {
this.fireHook.resolves([module1Capabilities, module2Capabilities])
result = await this.AdminAuthorizationHelper.getAdminCapabilities({})
beforeEach(async function (ctx) {
ctx.fireHook.resolves([module1Capabilities, module2Capabilities])
result = await ctx.AdminAuthorizationHelper.getAdminCapabilities({})
})
it('returns true for adminCapabilitiesAvailable', async function () {
expect(result.adminCapabilitiesAvailable).to.be.true
@@ -49,8 +52,8 @@ describe('AdminAuthorizationHelper', function () {
})
describe('when no module returns capabilities', function () {
let result
beforeEach(async function () {
result = await this.AdminAuthorizationHelper.getAdminCapabilities({})
beforeEach(async function (ctx) {
result = await ctx.AdminAuthorizationHelper.getAdminCapabilities({})
})
it('returns false for adminCapabilitiesAvailable', function () {
@@ -64,204 +67,203 @@ describe('AdminAuthorizationHelper', function () {
describe('useAdminCapabilities', function () {
describe('when admin capabilities are not available', function () {
describe('user is null', function () {
beforeEach(async function () {
this.req = new MockRequest()
this.res = new MockResponse()
this.next = sinon.stub()
beforeEach(async function (ctx) {
ctx.req = new MockRequest()
ctx.res = new MockResponse()
ctx.next = sinon.stub()
this.req.session = {
ctx.req.session = {
user: null,
}
await this.AdminAuthorizationHelper.useAdminCapabilities(
this.req,
this.res,
this.next
await ctx.AdminAuthorizationHelper.useAdminCapabilities(
ctx.req,
ctx.res,
ctx.next
)
})
it('does not define adminCapabilitiesAvailable on req', function () {
expect(this.req).not.to.have.property('adminCapabilitiesAvailable')
it('does not define adminCapabilitiesAvailable on req', function (ctx) {
expect(ctx.req).not.to.have.property('adminCapabilitiesAvailable')
})
it('defines adminCapabilities as an empty array on req', function () {
expect(this.req).to.have.property('adminCapabilities')
expect(this.req.adminCapabilities).to.be.an('array')
expect(this.req.adminCapabilities).to.be.empty
it('defines adminCapabilities as an empty array on req', function (ctx) {
expect(ctx.req).to.have.property('adminCapabilities')
expect(ctx.req.adminCapabilities).to.be.an('array')
expect(ctx.req.adminCapabilities).to.be.empty
})
})
describe('user is not an admin', function () {
beforeEach(async function () {
this.req = new MockRequest()
this.res = new MockResponse()
this.next = sinon.stub()
beforeEach(async function (ctx) {
ctx.req = new MockRequest()
ctx.res = new MockResponse()
ctx.next = sinon.stub()
this.user = {
ctx.user = {
isAdmin: false,
}
this.req.session = {
user: this.user,
ctx.req.session = {
user: ctx.user,
}
await this.AdminAuthorizationHelper.useAdminCapabilities(
this.req,
this.res,
this.next
await ctx.AdminAuthorizationHelper.useAdminCapabilities(
ctx.req,
ctx.res,
ctx.next
)
})
it('does not define adminCapabilitiesAvailable on req', function () {
expect(this.req).not.to.have.property('adminCapabilitiesAvailable')
it('does not define adminCapabilitiesAvailable on req', function (ctx) {
expect(ctx.req).not.to.have.property('adminCapabilitiesAvailable')
})
it('defines adminCapabilities as an empty array on req', function () {
expect(this.req).to.have.property('adminCapabilities')
expect(this.req.adminCapabilities).to.be.an('array')
expect(this.req.adminCapabilities).to.be.empty
it('defines adminCapabilities as an empty array on req', function (ctx) {
expect(ctx.req).to.have.property('adminCapabilities')
expect(ctx.req.adminCapabilities).to.be.an('array')
expect(ctx.req.adminCapabilities).to.be.empty
})
})
describe('user is an admin', function () {
beforeEach(async function () {
this.req = new MockRequest()
this.res = new MockResponse()
this.next = sinon.stub()
beforeEach(async function (ctx) {
ctx.req = new MockRequest()
ctx.res = new MockResponse()
ctx.next = sinon.stub()
this.user = {
ctx.user = {
isAdmin: true,
}
this.req.session = {
user: this.user,
ctx.req.session = {
user: ctx.user,
}
await this.AdminAuthorizationHelper.useAdminCapabilities(
this.req,
this.res,
this.next
await ctx.AdminAuthorizationHelper.useAdminCapabilities(
ctx.req,
ctx.res,
ctx.next
)
})
it('defines adminCapabilitiesAvailable as false on req', function () {
expect(this.req).to.have.property('adminCapabilitiesAvailable', false)
it('defines adminCapabilitiesAvailable as false on req', function (ctx) {
expect(ctx.req).to.have.property('adminCapabilitiesAvailable', false)
})
it('defines adminCapabilities as an empty array', function () {
expect(this.req).to.have.property('adminCapabilities')
expect(this.req.adminCapabilities).to.be.an('array')
expect(this.req.adminCapabilities).to.be.empty
it('defines adminCapabilities as an empty array', function (ctx) {
expect(ctx.req).to.have.property('adminCapabilities')
expect(ctx.req.adminCapabilities).to.be.an('array')
expect(ctx.req.adminCapabilities).to.be.empty
})
})
})
describe('when admin capabilities are available', function () {
beforeEach(function () {
this.fireHook.resolves(['capability1', 'capability2'])
beforeEach(function (ctx) {
ctx.fireHook.resolves(['capability1', 'capability2'])
})
describe('user is not an admin', function () {
beforeEach(async function () {
this.req = new MockRequest()
this.res = new MockResponse()
this.next = sinon.stub()
beforeEach(async function (ctx) {
ctx.req = new MockRequest()
ctx.res = new MockResponse()
ctx.next = sinon.stub()
this.user = {
ctx.user = {
isAdmin: false,
}
this.req.session = {
user: this.user,
ctx.req.session = {
user: ctx.user,
}
await this.AdminAuthorizationHelper.useAdminCapabilities(
this.req,
this.res,
this.next
await ctx.AdminAuthorizationHelper.useAdminCapabilities(
ctx.req,
ctx.res,
ctx.next
)
})
it('does not define adminCapabilitiesAvailable on req', function () {
expect(this.req).not.to.have.property('adminCapabilitiesAvailable')
it('does not define adminCapabilitiesAvailable on req', function (ctx) {
expect(ctx.req).not.to.have.property('adminCapabilitiesAvailable')
})
it('defines adminCapabilities as an empty array on req', function () {
expect(this.req).to.have.property('adminCapabilities')
expect(this.req.adminCapabilities).to.be.an('array')
expect(this.req.adminCapabilities).to.be.empty
it('defines adminCapabilities as an empty array on req', function (ctx) {
expect(ctx.req).to.have.property('adminCapabilities')
expect(ctx.req.adminCapabilities).to.be.an('array')
expect(ctx.req.adminCapabilities).to.be.empty
})
})
describe('user is an admin', function () {
beforeEach(async function () {
this.req = new MockRequest()
this.res = new MockResponse()
this.next = sinon.stub()
beforeEach(async function (ctx) {
ctx.req = new MockRequest()
ctx.res = new MockResponse()
ctx.next = sinon.stub()
this.user = {
ctx.user = {
isAdmin: true,
}
this.req.session = {
user: this.user,
ctx.req.session = {
user: ctx.user,
}
await this.AdminAuthorizationHelper.useAdminCapabilities(
this.req,
this.res,
this.next
await ctx.AdminAuthorizationHelper.useAdminCapabilities(
ctx.req,
ctx.res,
ctx.next
)
})
it('defines adminCapabilitiesAvailable as true on req', function () {
expect(this.req).to.have.property('adminCapabilitiesAvailable', true)
it('defines adminCapabilitiesAvailable as true on req', function (ctx) {
expect(ctx.req).to.have.property('adminCapabilitiesAvailable', true)
})
it('defines adminCapabilities with the capabilities returned from modules', function () {
expect(this.req).to.have.property('adminCapabilities')
expect(this.req.adminCapabilities).to.be.an('array')
expect(this.req.adminCapabilities).to.include('capability1')
expect(this.req.adminCapabilities).to.include('capability2')
it('defines adminCapabilities with the capabilities returned from modules', function (ctx) {
expect(ctx.req).to.have.property('adminCapabilities')
expect(ctx.req.adminCapabilities).to.be.an('array')
expect(ctx.req.adminCapabilities).to.include('capability1')
expect(ctx.req.adminCapabilities).to.include('capability2')
})
})
})
describe('when getting capabilities from modules throws an error', function () {
beforeEach(async function () {
this.fireHook.rejects(new Error('Module error'))
beforeEach(async function (ctx) {
ctx.fireHook.rejects(new Error('Module error'))
this.req = new MockRequest()
this.res = new MockResponse()
this.next = sinon.stub()
ctx.req = new MockRequest()
ctx.res = new MockResponse()
ctx.next = sinon.stub()
this.user = {
ctx.user = {
isAdmin: true,
}
this.req.logger = {
ctx.req.logger = {
warn: sinon.stub(),
}
this.req.session = {
user: this.user,
ctx.req.session = {
user: ctx.user,
}
await this.AdminAuthorizationHelper.useAdminCapabilities(
this.req,
this.res,
this.next
await ctx.AdminAuthorizationHelper.useAdminCapabilities(
ctx.req,
ctx.res,
ctx.next
)
})
it('logs the error', function () {
expect(this.logger.warn).to.have.been.calledWith(
sinon.match.has('err', sinon.match.instanceOf(Error))
)
it('logs the error', function (ctx) {
expect(ctx.logger.warn).toHaveBeenCalled()
expect(ctx.logger.warn.mock.calls[0][0].err).toBeInstanceOf(Error)
})
it('defines adminCapabilitiesAvailable as true on req', function () {
expect(this.req).to.have.property('adminCapabilitiesAvailable', true)
it('defines adminCapabilitiesAvailable as true on req', function (ctx) {
expect(ctx.req).to.have.property('adminCapabilitiesAvailable', true)
})
it('defines adminCapabilities as an empty array', function () {
expect(this.req).to.have.property('adminCapabilities')
expect(this.req.adminCapabilities).to.be.an('array')
expect(this.req.adminCapabilities).to.be.empty
it('defines adminCapabilities as an empty array', function (ctx) {
expect(ctx.req).to.have.property('adminCapabilities')
expect(ctx.req.adminCapabilities).to.be.an('array')
expect(ctx.req.adminCapabilities).to.be.empty
})
})
})
describe('useHasAdminCapability', function () {
it('adds hasAdminCapability to res.locals', function () {
it('adds hasAdminCapability to res.locals', function (ctx) {
const req = new MockRequest()
const res = new MockResponse()
const next = sinon.stub()
this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
expect(res.locals).to.have.property('hasAdminCapability')
expect(res.locals.hasAdminCapability).to.be.a('function')
@@ -269,7 +271,7 @@ describe('AdminAuthorizationHelper', function () {
describe('when the user is not an admin', function () {
describe('when req.adminCapabilitiesAvailable is true', function () {
it('returns false for any capability', function () {
it('returns false for any capability', function (ctx) {
const req = new MockRequest()
const res = new MockResponse()
const next = sinon.stub()
@@ -279,14 +281,14 @@ describe('AdminAuthorizationHelper', function () {
req.session.user = { isAdmin: false }
this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
expect(res.locals.hasAdminCapability('capability1')).to.be.false
})
})
describe('when req.adminCapabilitiesAvailable is false', function () {
it('returns false for any capability', function () {
it('returns false for any capability', function (ctx) {
const req = new MockRequest()
const res = new MockResponse()
const next = sinon.stub()
@@ -296,21 +298,21 @@ describe('AdminAuthorizationHelper', function () {
req.session.user = { isAdmin: false }
this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
expect(res.locals.hasAdminCapability('capability1')).to.be.false
})
})
describe('when req.adminCapabilitiesAvailable is undefined', function () {
it('returns false for any capability', function () {
it('returns false for any capability', function (ctx) {
const req = new MockRequest()
const res = new MockResponse()
const next = sinon.stub()
req.session.user = { isAdmin: false }
this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
expect(res.locals.hasAdminCapability('capability1')).to.be.false
})
@@ -319,7 +321,7 @@ describe('AdminAuthorizationHelper', function () {
describe('user is an admin', function () {
describe('when req.adminCapabilitiesAvailable is false', function () {
it('returns true for any capability', function () {
it('returns true for any capability', function (ctx) {
const req = new MockRequest()
const res = new MockResponse()
const next = sinon.stub()
@@ -327,21 +329,21 @@ describe('AdminAuthorizationHelper', function () {
req.session.user = { isAdmin: true }
req.adminCapabilitiesAvailable = false
this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
expect(res.locals.hasAdminCapability('capability1')).to.be.true
})
})
describe('when req.adminCapabilitiesAvailable is undefined', function () {
it('returns true for any capability', function () {
it('returns true for any capability', function (ctx) {
const req = new MockRequest()
const res = new MockResponse()
const next = sinon.stub()
req.session.user = { isAdmin: true }
this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
expect(res.locals.hasAdminCapability('capability1')).to.be.true
})
@@ -349,7 +351,7 @@ describe('AdminAuthorizationHelper', function () {
describe('when req.adminCapabilitiesAvailable is true', function () {
let req, res, next
beforeEach(function () {
beforeEach(function (ctx) {
req = new MockRequest()
res = new MockResponse()
next = sinon.stub()
@@ -358,7 +360,7 @@ describe('AdminAuthorizationHelper', function () {
req.adminCapabilitiesAvailable = true
req.adminCapabilities = ['capability1', 'capability2']
this.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
ctx.AdminAuthorizationHelper.useHasAdminCapability(req, res, next)
})
it('returns true for a capability the user has', function () {
@@ -373,20 +375,20 @@ describe('AdminAuthorizationHelper', function () {
})
describe('hasAdminCapability', function () {
describe('when user is not an admin', function () {
it('returns false', function () {
it('returns false', function (ctx) {
const req = {
session: {
user: { isAdmin: false },
},
}
expect(
this.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
).to.be.false
})
})
describe('when user is an admin', function () {
describe('when adminCapabilitiesAvailable is falsey', function () {
it('returns true', function () {
it('returns true', function (ctx) {
const req = {
session: {
user: { isAdmin: true },
@@ -394,22 +396,22 @@ describe('AdminAuthorizationHelper', function () {
adminCapabilitiesAvailable: false,
}
expect(
this.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
).to.be.true
})
it('ignores the "requireAdminRoles" argument', function () {
it('ignores the "requireAdminRoles" argument', function (ctx) {
const req = {
session: { user: { isAdmin: true } },
adminCapabilitiesAvailable: false,
}
expect(
this.AdminAuthorizationHelper.hasAdminCapability(
ctx.AdminAuthorizationHelper.hasAdminCapability(
'capability',
true
)(req)
).to.be.true
expect(
this.AdminAuthorizationHelper.hasAdminCapability(
ctx.AdminAuthorizationHelper.hasAdminCapability(
'capability',
false
)(req)
@@ -418,30 +420,26 @@ describe('AdminAuthorizationHelper', function () {
})
describe('when adminCapabilitiesAvailable is true', function () {
describe('when user has the requested capability', function () {
it('returns true', function () {
it('returns true', function (ctx) {
const req = {
session: { user: { isAdmin: true } },
adminCapabilitiesAvailable: true,
adminCapabilities: ['capability'],
}
expect(
this.AdminAuthorizationHelper.hasAdminCapability('capability')(
req
)
ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
).to.be.true
})
})
describe('when user does not have the requested capability', function () {
it('returns false', function () {
it('returns false', function (ctx) {
const req = {
session: { user: { isAdmin: true } },
adminCapabilitiesAvailable: true,
adminCapabilities: ['other-capability'],
}
expect(
this.AdminAuthorizationHelper.hasAdminCapability('capability')(
req
)
ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
).to.be.false
})
})
@@ -449,26 +447,26 @@ describe('AdminAuthorizationHelper', function () {
})
describe('when admin roles are not enabled', function () {
beforeEach(function () {
this.settings.adminRolesEnabled = false
beforeEach(function (ctx) {
ctx.settings.adminRolesEnabled = false
})
it('returns false even for admins', function () {
it('returns false even for admins', function (ctx) {
const req = { session: { user: { isAdmin: true } } }
expect(
this.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
ctx.AdminAuthorizationHelper.hasAdminCapability('capability')(req)
).to.be.false
expect(
this.AdminAuthorizationHelper.hasAdminCapability(
ctx.AdminAuthorizationHelper.hasAdminCapability(
'capability',
true
)(req)
).to.be.false
})
it('returns true when requireAdminRoles=false', function () {
it('returns true when requireAdminRoles=false', function (ctx) {
const req = { session: { user: { isAdmin: true } } }
expect(
this.AdminAuthorizationHelper.hasAdminCapability(
ctx.AdminAuthorizationHelper.hasAdminCapability(
'capability',
false
)(req)

View File

@@ -82,9 +82,12 @@ describe('HistoryController', function () {
})
)
vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({
default: ctx.HistoryManager,
}))
vi.doMock(
'../../../../app/src/Features/History/HistoryManager.mjs',
() => ({
default: ctx.HistoryManager,
})
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectDetailsHandler.mjs',

View File

@@ -1,12 +1,14 @@
const { expect } = require('chai')
const sinon = require('sinon')
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb-legacy')
const {
import { expect } from 'chai'
import sinon from 'sinon'
import SandboxedModule from 'sandboxed-module'
import mongodb from 'mongodb-legacy'
import {
cleanupTestDatabase,
db,
waitForDb,
} = require('../../../../app/src/infrastructure/mongodb')
} from '../../../../app/src/infrastructure/mongodb.js'
const { ObjectId } = mongodb
const MODULE_PATH = '../../../../app/src/Features/History/HistoryManager'

View File

@@ -23,61 +23,64 @@ describe('RestoreManager', function () {
default: Errors,
}))
vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({
default: (ctx.HistoryManager = {
promises: {
getContentAtVersion: sinon.stub().resolves({
// Raw snapshot data that will be passed to Snapshot.fromRaw
files: {
'main.tex': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
editorId: 'test-editor',
vi.doMock(
'../../../../app/src/Features/History/HistoryManager.mjs',
() => ({
default: (ctx.HistoryManager = {
promises: {
getContentAtVersion: sinon.stub().resolves({
// Raw snapshot data that will be passed to Snapshot.fromRaw
files: {
'main.tex': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
editorId: 'test-editor',
},
},
'foo.tex': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
editorId: 'test-editor',
},
},
'folder/file.tex': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
editorId: 'test-editor',
},
},
'foo.png': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
provider: 'bar',
},
},
'linkedFile.bib': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
provider: 'mendeley',
},
},
'withMainTrue.tex': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
main: true,
},
},
},
'foo.tex': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
editorId: 'test-editor',
},
},
'folder/file.tex': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
editorId: 'test-editor',
},
},
'foo.png': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
provider: 'bar',
},
},
'linkedFile.bib': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
provider: 'mendeley',
},
},
'withMainTrue.tex': {
hash: 'abcdef1234567890abcdef1234567890abcdef12',
stringLength: 100,
metadata: {
main: true,
},
},
},
timestamp: new Date().toISOString(),
}),
requestBlob: sinon.stub().resolves({ stream: ctx.blobStream }),
},
}),
}))
timestamp: new Date().toISOString(),
}),
requestBlob: sinon.stub().resolves({ stream: ctx.blobStream }),
},
}),
})
)
vi.doMock('../../../../app/src/infrastructure/Metrics.js', () => ({
default: {

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,41 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const { ObjectId } = require('mongodb-legacy')
const Errors = require('../../../../app/src/Features/Errors/Errors')
const ProjectHelper = require('../../../../app/src/Features/Project/ProjectHelper')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
import ProjectHelper from '../../../../app/src/Features/Project/ProjectHelper.js'
const { ObjectId } = mongodb
const MODULE_PATH = '../../../../app/src/Features/Project/ProjectDetailsHandler'
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
describe('ProjectDetailsHandler', function () {
beforeEach(function () {
this.user = {
beforeEach(async function (ctx) {
ctx.user = {
_id: new ObjectId(),
email: 'user@example.com',
features: 'mock-features',
}
this.collaborator = {
ctx.collaborator = {
_id: new ObjectId(),
email: 'collaborator@example.com',
}
this.project = {
ctx.project = {
_id: new ObjectId(),
name: 'project',
description: 'this is a great project',
something: 'should not exist',
compiler: 'latexxxxxx',
owner_ref: this.user._id,
collaberator_refs: [this.collaborator._id],
owner_ref: ctx.user._id,
collaberator_refs: [ctx.collaborator._id],
}
this.ProjectGetter = {
ctx.ProjectGetter = {
promises: {
getProjectWithoutDocLines: sinon.stub().resolves(this.project),
getProject: sinon.stub().resolves(this.project),
getProjectWithoutDocLines: sinon.stub().resolves(ctx.project),
getProject: sinon.stub().resolves(ctx.project),
findAllUsersProjects: sinon.stub().resolves({
owned: [],
readAndWrite: [],
@@ -40,200 +45,221 @@ describe('ProjectDetailsHandler', function () {
}),
},
}
this.ProjectModelUpdateQuery = {
ctx.ProjectModelUpdateQuery = {
exec: sinon.stub().resolves(),
}
this.ProjectModel = {
updateOne: sinon.stub().returns(this.ProjectModelUpdateQuery),
ctx.ProjectModel = {
updateOne: sinon.stub().returns(ctx.ProjectModelUpdateQuery),
}
this.UserGetter = {
ctx.UserGetter = {
promises: {
getUser: sinon.stub().resolves(this.user),
getUser: sinon.stub().resolves(ctx.user),
},
}
this.TpdsUpdateSender = {
ctx.TpdsUpdateSender = {
promises: {
moveEntity: sinon.stub().resolves(),
},
}
this.TokenGenerator = {
ctx.TokenGenerator = {
readAndWriteToken: sinon.stub(),
promises: {
generateUniqueReadOnlyToken: sinon.stub(),
},
}
this.settings = {
ctx.settings = {
defaultFeatures: 'default-features',
}
this.handler = SandboxedModule.require(MODULE_PATH, {
requires: {
'./ProjectHelper': ProjectHelper,
'./ProjectGetter': this.ProjectGetter,
'../../models/Project': {
Project: this.ProjectModel,
},
'../User/UserGetter': this.UserGetter,
'../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
'../TokenGenerator/TokenGenerator': this.TokenGenerator,
'@overleaf/settings': this.settings,
},
})
vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({
default: ProjectHelper,
}))
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: ctx.ProjectGetter,
}))
vi.doMock('../../../../app/src/models/Project', () => ({
Project: ctx.ProjectModel,
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
vi.doMock(
'../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender',
() => ({
default: ctx.TpdsUpdateSender,
})
)
vi.doMock(
'../../../../app/src/Features/TokenGenerator/TokenGenerator',
() => ({
default: ctx.TokenGenerator,
})
)
vi.doMock('@overleaf/settings', () => ({
default: ctx.settings,
}))
ctx.handler = (await import(MODULE_PATH)).default
})
describe('getDetails', function () {
it('should find the project and owner', async function () {
const details = await this.handler.promises.getDetails(this.project._id)
details.name.should.equal(this.project.name)
details.description.should.equal(this.project.description)
details.compiler.should.equal(this.project.compiler)
details.features.should.equal(this.user.features)
it('should find the project and owner', async function (ctx) {
const details = await ctx.handler.promises.getDetails(ctx.project._id)
details.name.should.equal(ctx.project.name)
details.description.should.equal(ctx.project.description)
details.compiler.should.equal(ctx.project.compiler)
details.features.should.equal(ctx.user.features)
expect(details.something).to.be.undefined
})
it('should find overleaf metadata if it exists', async function () {
this.project.overleaf = { id: 'id' }
const details = await this.handler.promises.getDetails(this.project._id)
details.overleaf.should.equal(this.project.overleaf)
it('should find overleaf metadata if it exists', async function (ctx) {
ctx.project.overleaf = { id: 'id' }
const details = await ctx.handler.promises.getDetails(ctx.project._id)
details.overleaf.should.equal(ctx.project.overleaf)
expect(details.something).to.be.undefined
})
it('should return an error for a non-existent project', async function () {
this.ProjectGetter.promises.getProject.resolves(null)
it('should return an error for a non-existent project', async function (ctx) {
ctx.ProjectGetter.promises.getProject.resolves(null)
await expect(
this.handler.promises.getDetails('0123456789012345678901234')
ctx.handler.promises.getDetails('0123456789012345678901234')
).to.be.rejectedWith(Errors.NotFoundError)
})
it('should return the default features if no owner found', async function () {
this.UserGetter.promises.getUser.resolves(null)
const details = await this.handler.promises.getDetails(this.project._id)
details.features.should.equal(this.settings.defaultFeatures)
it('should return the default features if no owner found', async function (ctx) {
ctx.UserGetter.promises.getUser.resolves(null)
const details = await ctx.handler.promises.getDetails(ctx.project._id)
details.features.should.equal(ctx.settings.defaultFeatures)
})
it('should rethrow any error', async function () {
this.ProjectGetter.promises.getProject.rejects(new Error('boom'))
await expect(this.handler.promises.getDetails(this.project._id)).to.be
it('should rethrow any error', async function (ctx) {
ctx.ProjectGetter.promises.getProject.rejects(new Error('boom'))
await expect(ctx.handler.promises.getDetails(ctx.project._id)).to.be
.rejected
})
})
describe('getProjectDescription', function () {
it('should make a call to mongo just for the description', async function () {
this.ProjectGetter.promises.getProject.resolves()
await this.handler.promises.getProjectDescription(this.project._id)
expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
this.project._id,
it('should make a call to mongo just for the description', async function (ctx) {
ctx.ProjectGetter.promises.getProject.resolves()
await ctx.handler.promises.getProjectDescription(ctx.project._id)
expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
ctx.project._id,
{ description: true }
)
})
it('should return what the mongo call returns', async function () {
it('should return what the mongo call returns', async function (ctx) {
const expectedDescription = 'cool project'
this.ProjectGetter.promises.getProject.resolves({
ctx.ProjectGetter.promises.getProject.resolves({
description: expectedDescription,
})
const description = await this.handler.promises.getProjectDescription(
this.project._id
const description = await ctx.handler.promises.getProjectDescription(
ctx.project._id
)
expect(description).to.equal(expectedDescription)
})
})
describe('setProjectDescription', function () {
beforeEach(function () {
this.description = 'updated teh description'
beforeEach(function (ctx) {
ctx.description = 'updated teh description'
})
it('should update the project detials', async function () {
await this.handler.promises.setProjectDescription(
this.project._id,
this.description
it('should update the project detials', async function (ctx) {
await ctx.handler.promises.setProjectDescription(
ctx.project._id,
ctx.description
)
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: this.project._id },
{ description: this.description }
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: ctx.project._id },
{ description: ctx.description }
)
})
})
describe('renameProject', function () {
beforeEach(function () {
this.newName = 'new name here'
beforeEach(function (ctx) {
ctx.newName = 'new name here'
})
it('should update the project with the new name', async function () {
await this.handler.promises.renameProject(this.project._id, this.newName)
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: this.project._id },
{ name: this.newName }
it('should update the project with the new name', async function (ctx) {
await ctx.handler.promises.renameProject(ctx.project._id, ctx.newName)
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: ctx.project._id },
{ name: ctx.newName }
)
})
it('should tell the TpdsUpdateSender', async function () {
await this.handler.promises.renameProject(this.project._id, this.newName)
expect(this.TpdsUpdateSender.promises.moveEntity).to.have.been.calledWith(
{
projectId: this.project._id,
projectName: this.project.name,
newProjectName: this.newName,
}
)
it('should tell the TpdsUpdateSender', async function (ctx) {
await ctx.handler.promises.renameProject(ctx.project._id, ctx.newName)
expect(ctx.TpdsUpdateSender.promises.moveEntity).to.have.been.calledWith({
projectId: ctx.project._id,
projectName: ctx.project.name,
newProjectName: ctx.newName,
})
})
it('should not do anything with an invalid name', async function () {
await expect(this.handler.promises.renameProject(this.project._id)).to.be
it('should not do anything with an invalid name', async function (ctx) {
await expect(ctx.handler.promises.renameProject(ctx.project._id)).to.be
.rejected
expect(this.TpdsUpdateSender.promises.moveEntity).not.to.have.been.called
expect(this.ProjectModel.updateOne).not.to.have.been.called
expect(ctx.TpdsUpdateSender.promises.moveEntity).not.to.have.been.called
expect(ctx.ProjectModel.updateOne).not.to.have.been.called
})
it('should trim whitespace around name', async function () {
await this.handler.promises.renameProject(
this.project._id,
` ${this.newName} `
it('should trim whitespace around name', async function (ctx) {
await ctx.handler.promises.renameProject(
ctx.project._id,
` ${ctx.newName} `
)
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: this.project._id },
{ name: this.newName }
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: ctx.project._id },
{ name: ctx.newName }
)
})
})
describe('validateProjectName', function () {
it('should reject undefined names', async function () {
await expect(this.handler.promises.validateProjectName(undefined)).to.be
it('should reject undefined names', async function (ctx) {
await expect(ctx.handler.promises.validateProjectName(undefined)).to.be
.rejected
})
it('should reject empty names', async function () {
await expect(this.handler.promises.validateProjectName('')).to.be.rejected
it('should reject empty names', async function (ctx) {
await expect(ctx.handler.promises.validateProjectName('')).to.be.rejected
})
it('should reject names with /s', async function () {
await expect(this.handler.promises.validateProjectName('foo/bar')).to.be
it('should reject names with /s', async function (ctx) {
await expect(ctx.handler.promises.validateProjectName('foo/bar')).to.be
.rejected
})
it('should reject names with \\s', async function () {
await expect(this.handler.promises.validateProjectName('foo\\bar')).to.be
it('should reject names with \\s', async function (ctx) {
await expect(ctx.handler.promises.validateProjectName('foo\\bar')).to.be
.rejected
})
it('should reject long names', async function () {
await expect(this.handler.promises.validateProjectName('a'.repeat(1000)))
it('should reject long names', async function (ctx) {
await expect(ctx.handler.promises.validateProjectName('a'.repeat(1000)))
.to.be.rejected
})
it('should accept normal names', async function () {
await expect(this.handler.promises.validateProjectName('foobar')).to.be
it('should accept normal names', async function (ctx) {
await expect(ctx.handler.promises.validateProjectName('foobar')).to.be
.fulfilled
})
})
describe('generateUniqueName', function () {
// actually testing `ProjectHelper.promises.ensureNameIsUnique()`
beforeEach(function () {
this.longName = 'x'.repeat(this.handler.MAX_PROJECT_NAME_LENGTH - 5)
beforeEach(function (ctx) {
ctx.longName = 'x'.repeat(ctx.handler.MAX_PROJECT_NAME_LENGTH - 5)
const usersProjects = {
owned: [
{ _id: 1, name: 'name' },
@@ -290,116 +316,116 @@ describe('ProjectDetailsHandler', function () {
tokenReadOnly: [
{ _id: 10, name: 'name5' },
{ _id: 11, name: 'name55' },
{ _id: 12, name: this.longName },
{ _id: 12, name: ctx.longName },
],
}
this.ProjectGetter.promises.findAllUsersProjects.resolves(usersProjects)
ctx.ProjectGetter.promises.findAllUsersProjects.resolves(usersProjects)
})
it('should leave a unique name unchanged', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should leave a unique name unchanged', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'unique-name',
['-test-suffix']
)
expect(name).to.equal('unique-name')
})
it('should append a suffix to an existing name', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should append a suffix to an existing name', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'name1',
['-test-suffix']
)
expect(name).to.equal('name1-test-suffix')
})
it('should fallback to a second suffix when needed', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should fallback to a second suffix when needed', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'name1',
['1', '-test-suffix']
)
expect(name).to.equal('name1-test-suffix')
})
it('should truncate the name when append a suffix if the result is too long', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
this.longName,
it('should truncate the name when append a suffix if the result is too long', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
ctx.longName,
['-test-suffix']
)
expect(name).to.equal(
this.longName.substr(0, this.handler.MAX_PROJECT_NAME_LENGTH - 12) +
ctx.longName.substr(0, ctx.handler.MAX_PROJECT_NAME_LENGTH - 12) +
'-test-suffix'
)
})
it('should use a numeric index if no suffix is supplied', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should use a numeric index if no suffix is supplied', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'name1',
[]
)
expect(name).to.equal('name1 (1)')
})
it('should use a numeric index if all suffixes are exhausted', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should use a numeric index if all suffixes are exhausted', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'name',
['1', '11']
)
expect(name).to.equal('name (1)')
})
it('should find the next lowest available numeric index for the base name', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should find the next lowest available numeric index for the base name', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'numeric',
[]
)
expect(name).to.equal('numeric (21)')
})
it('should not find a numeric index lower than the one already present', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should not find a numeric index lower than the one already present', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'numeric (31)',
[]
)
expect(name).to.equal('numeric (41)')
})
it('should handle years in name', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should handle years in name', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'unique-name (2021)',
[]
)
expect(name).to.equal('unique-name (2021)')
})
it('should handle duplicating with year in name', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should handle duplicating with year in name', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'Yearbook (2021)',
[]
)
expect(name).to.equal('Yearbook (2021) (2)')
})
describe('title with that causes invalid regex', function () {
it('should create the project with a suffix when project name exists', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should create the project with a suffix when project name exists', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'Resume (2020',
[]
)
expect(name).to.equal('Resume (2020 (1)')
})
it('should create the project with the provided name', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should create the project with the provided name', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'Yearbook (2021',
[]
)
@@ -409,18 +435,18 @@ describe('ProjectDetailsHandler', function () {
describe('numeric index is already present', function () {
describe('when there is 1 project "x (2)"', function () {
beforeEach(function () {
beforeEach(function (ctx) {
const usersProjects = {
owned: [{ _id: 1, name: 'x (2)' }],
}
this.ProjectGetter.promises.findAllUsersProjects.resolves(
ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
usersProjects
)
})
it('should produce "x (3)" uploading a zip with name "x (2)"', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should produce "x (3)" uploading a zip with name "x (2)"', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'x (2)',
[]
)
@@ -429,21 +455,21 @@ describe('ProjectDetailsHandler', function () {
})
describe('when there are 2 projects "x (2)" and "x (3)"', function () {
beforeEach(function () {
beforeEach(function (ctx) {
const usersProjects = {
owned: [
{ _id: 1, name: 'x (2)' },
{ _id: 2, name: 'x (3)' },
],
}
this.ProjectGetter.promises.findAllUsersProjects.resolves(
ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
usersProjects
)
})
it('should produce "x (4)" when uploading a zip with name "x (2)"', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should produce "x (4)" when uploading a zip with name "x (2)"', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'x (2)',
[]
)
@@ -452,30 +478,30 @@ describe('ProjectDetailsHandler', function () {
})
describe('when there are 2 projects "x (2)" and "x (4)"', function () {
beforeEach(function () {
beforeEach(function (ctx) {
const usersProjects = {
owned: [
{ _id: 1, name: 'x (2)' },
{ _id: 2, name: 'x (4)' },
],
}
this.ProjectGetter.promises.findAllUsersProjects.resolves(
ctx.ProjectGetter.promises.findAllUsersProjects.resolves(
usersProjects
)
})
it('should produce "x (3)" when uploading a zip with name "x (2)"', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should produce "x (3)" when uploading a zip with name "x (2)"', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'x (2)',
[]
)
expect(name).to.equal('x (3)')
})
it('should produce "x (5)" when uploading a zip with name "x (4)"', async function () {
const name = await this.handler.promises.generateUniqueName(
this.user._id,
it('should produce "x (5)" when uploading a zip with name "x (4)"', async function (ctx) {
const name = await ctx.handler.promises.generateUniqueName(
ctx.user._id,
'x (4)',
[]
)
@@ -486,70 +512,70 @@ describe('ProjectDetailsHandler', function () {
})
describe('fixProjectName', function () {
it('should change empty names to Untitled', function () {
expect(this.handler.fixProjectName('')).to.equal('Untitled')
it('should change empty names to Untitled', function (ctx) {
expect(ctx.handler.fixProjectName('')).to.equal('Untitled')
})
it('should replace / with -', function () {
expect(this.handler.fixProjectName('foo/bar')).to.equal('foo-bar')
it('should replace / with -', function (ctx) {
expect(ctx.handler.fixProjectName('foo/bar')).to.equal('foo-bar')
})
it("should replace \\ with ''", function () {
expect(this.handler.fixProjectName('foo \\ bar')).to.equal('foo bar')
it("should replace \\ with ''", function (ctx) {
expect(ctx.handler.fixProjectName('foo \\ bar')).to.equal('foo bar')
})
it('should truncate long names', function () {
expect(this.handler.fixProjectName('a'.repeat(1000))).to.equal(
it('should truncate long names', function (ctx) {
expect(ctx.handler.fixProjectName('a'.repeat(1000))).to.equal(
'a'.repeat(150)
)
})
it('should accept normal names', function () {
expect(this.handler.fixProjectName('foobar')).to.equal('foobar')
it('should accept normal names', function (ctx) {
expect(ctx.handler.fixProjectName('foobar')).to.equal('foobar')
})
it('should trim name after truncation', function () {
expect(this.handler.fixProjectName('a'.repeat(149) + ' a')).to.equal(
it('should trim name after truncation', function (ctx) {
expect(ctx.handler.fixProjectName('a'.repeat(149) + ' a')).to.equal(
'a'.repeat(149)
)
})
})
describe('setPublicAccessLevel', function () {
beforeEach(function () {
this.accessLevel = 'tokenBased'
beforeEach(function (ctx) {
ctx.accessLevel = 'tokenBased'
})
it('should update the project with the new level', async function () {
await this.handler.promises.setPublicAccessLevel(
this.project._id,
this.accessLevel
it('should update the project with the new level', async function (ctx) {
await ctx.handler.promises.setPublicAccessLevel(
ctx.project._id,
ctx.accessLevel
)
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: this.project._id },
{ publicAccesLevel: this.accessLevel }
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: ctx.project._id },
{ publicAccesLevel: ctx.accessLevel }
)
})
it('should not produce an error', async function () {
it('should not produce an error', async function (ctx) {
await expect(
this.handler.promises.setPublicAccessLevel(
this.project._id,
this.accessLevel
ctx.handler.promises.setPublicAccessLevel(
ctx.project._id,
ctx.accessLevel
)
).to.be.fulfilled
})
describe('when update produces an error', function () {
beforeEach(function () {
this.ProjectModelUpdateQuery.exec.rejects(new Error('woops'))
beforeEach(function (ctx) {
ctx.ProjectModelUpdateQuery.exec.rejects(new Error('woops'))
})
it('should produce an error', async function () {
it('should produce an error', async function (ctx) {
await expect(
this.handler.promises.setPublicAccessLevel(
this.project._id,
this.accessLevel
ctx.handler.promises.setPublicAccessLevel(
ctx.project._id,
ctx.accessLevel
)
).to.be.rejected
})
@@ -558,76 +584,76 @@ describe('ProjectDetailsHandler', function () {
describe('ensureTokensArePresent', function () {
describe('when the project has tokens', function () {
beforeEach(function () {
this.project = {
_id: this.project._id,
beforeEach(function (ctx) {
ctx.project = {
_id: ctx.project._id,
tokens: {
readOnly: 'aaa',
readAndWrite: '42bbb',
readAndWritePrefix: '42',
},
}
this.ProjectGetter.promises.getProject.resolves(this.project)
ctx.ProjectGetter.promises.getProject.resolves(ctx.project)
})
it('should get the project', async function () {
await this.handler.promises.ensureTokensArePresent(this.project._id)
expect(this.ProjectGetter.promises.getProject).to.have.been.calledOnce
expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
this.project._id,
it('should get the project', async function (ctx) {
await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledOnce
expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
ctx.project._id,
{
tokens: 1,
}
)
})
it('should not update the project with new tokens', async function () {
await this.handler.promises.ensureTokensArePresent(this.project._id)
expect(this.ProjectModel.updateOne).not.to.have.been.called
it('should not update the project with new tokens', async function (ctx) {
await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
expect(ctx.ProjectModel.updateOne).not.to.have.been.called
})
})
describe('when tokens are missing', function () {
beforeEach(function () {
this.project = { _id: this.project._id }
this.ProjectGetter.promises.getProject.resolves(this.project)
this.readOnlyToken = 'abc'
this.readAndWriteToken = '42def'
this.readAndWriteTokenPrefix = '42'
this.TokenGenerator.promises.generateUniqueReadOnlyToken.resolves(
this.readOnlyToken
beforeEach(function (ctx) {
ctx.project = { _id: ctx.project._id }
ctx.ProjectGetter.promises.getProject.resolves(ctx.project)
ctx.readOnlyToken = 'abc'
ctx.readAndWriteToken = '42def'
ctx.readAndWriteTokenPrefix = '42'
ctx.TokenGenerator.promises.generateUniqueReadOnlyToken.resolves(
ctx.readOnlyToken
)
this.TokenGenerator.readAndWriteToken.returns({
token: this.readAndWriteToken,
numericPrefix: this.readAndWriteTokenPrefix,
ctx.TokenGenerator.readAndWriteToken.returns({
token: ctx.readAndWriteToken,
numericPrefix: ctx.readAndWriteTokenPrefix,
})
})
it('should get the project', async function () {
await this.handler.promises.ensureTokensArePresent(this.project._id)
expect(this.ProjectGetter.promises.getProject).to.have.been.calledOnce
expect(this.ProjectGetter.promises.getProject).to.have.been.calledWith(
this.project._id,
it('should get the project', async function (ctx) {
await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledOnce
expect(ctx.ProjectGetter.promises.getProject).to.have.been.calledWith(
ctx.project._id,
{
tokens: 1,
}
)
})
it('should update the project with new tokens', async function () {
await this.handler.promises.ensureTokensArePresent(this.project._id)
expect(this.TokenGenerator.promises.generateUniqueReadOnlyToken).to.have
it('should update the project with new tokens', async function (ctx) {
await ctx.handler.promises.ensureTokensArePresent(ctx.project._id)
expect(ctx.TokenGenerator.promises.generateUniqueReadOnlyToken).to.have
.been.calledOnce
expect(this.TokenGenerator.readAndWriteToken).to.have.been.calledOnce
expect(this.ProjectModel.updateOne).to.have.been.calledOnce
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: this.project._id },
expect(ctx.TokenGenerator.readAndWriteToken).to.have.been.calledOnce
expect(ctx.ProjectModel.updateOne).to.have.been.calledOnce
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: ctx.project._id },
{
$set: {
tokens: {
readOnly: this.readOnlyToken,
readAndWrite: this.readAndWriteToken,
readAndWritePrefix: this.readAndWriteTokenPrefix,
readOnly: ctx.readOnlyToken,
readAndWrite: ctx.readAndWriteToken,
readAndWritePrefix: ctx.readAndWriteTokenPrefix,
},
},
}
@@ -637,10 +663,10 @@ describe('ProjectDetailsHandler', function () {
})
describe('clearTokens', function () {
it('clears the tokens from the project', async function () {
await this.handler.promises.clearTokens(this.project._id)
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: this.project._id },
it('clears the tokens from the project', async function (ctx) {
await ctx.handler.promises.clearTokens(ctx.project._id)
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: ctx.project._id },
{ $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } }
)
})

View File

@@ -1,19 +1,22 @@
const { expect } = require('chai')
const sinon = require('sinon')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
const modulePath = '../../../../app/src/Features/Project/ProjectEntityHandler'
const SandboxedModule = require('sandboxed-module')
const Errors = require('../../../../app/src/Features/Errors/Errors')
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
describe('ProjectEntityHandler', function () {
const projectId = '4eecb1c1bffa66588e0000a1'
const docId = '4eecb1c1bffa66588e0000a2'
beforeEach(function () {
this.TpdsUpdateSender = {
beforeEach(async function (ctx) {
ctx.TpdsUpdateSender = {
addDoc: sinon.stub().callsArg(1),
addFile: sinon.stub().callsArg(1),
}
this.ProjectModel = class Project {
ctx.ProjectModel = class Project {
constructor(options) {
this._id = projectId
this.name = 'project_name_here'
@@ -21,59 +24,77 @@ describe('ProjectEntityHandler', function () {
this.rootFolder = [this.rootFolder]
}
}
this.project = new this.ProjectModel()
ctx.project = new ctx.ProjectModel()
this.ProjectLocator = { findElement: sinon.stub() }
this.DocumentUpdaterHandler = {
ctx.ProjectLocator = { findElement: sinon.stub() }
ctx.DocumentUpdaterHandler = {
updateProjectStructure: sinon.stub().yields(),
}
this.callback = sinon.stub()
ctx.callback = sinon.stub()
this.ProjectEntityHandler = SandboxedModule.require(modulePath, {
requires: {
'../Docstore/DocstoreManager': (this.DocstoreManager = {
promises: {},
}),
'../../Features/DocumentUpdater/DocumentUpdaterHandler':
this.DocumentUpdaterHandler,
'../../models/Project': {
Project: this.ProjectModel,
},
'./ProjectLocator': this.ProjectLocator,
'./ProjectGetter': (this.ProjectGetter = { promises: {} }),
'../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
},
})
vi.doMock('../../../../app/src/Features/Docstore/DocstoreManager', () => ({
default: (ctx.DocstoreManager = {
promises: {},
}),
}))
vi.doMock(
'../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
() => ({
default: ctx.DocumentUpdaterHandler,
})
)
vi.doMock('../../../../app/src/models/Project', () => ({
Project: ctx.ProjectModel,
}))
vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({
default: ctx.ProjectLocator,
}))
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: (ctx.ProjectGetter = { promises: {} }),
}))
vi.doMock(
'../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender',
() => ({
default: ctx.TpdsUpdateSender,
})
)
ctx.ProjectEntityHandler = (await import(modulePath)).default
})
describe('getting folders, docs and files', function () {
beforeEach(function () {
this.project.rootFolder = [
beforeEach(function (ctx) {
ctx.project.rootFolder = [
{
docs: [
(this.doc1 = {
(ctx.doc1 = {
name: 'doc1',
_id: 'doc1_id',
}),
],
fileRefs: [
(this.file1 = {
(ctx.file1 = {
rev: 1,
_id: 'file1_id',
name: 'file1',
}),
],
folders: [
(this.folder1 = {
(ctx.folder1 = {
name: 'folder1',
docs: [
(this.doc2 = {
(ctx.doc2 = {
name: 'doc2',
_id: 'doc2_id',
}),
],
fileRefs: [
(this.file2 = {
(ctx.file2 = {
rev: 2,
name: 'file2',
_id: 'file2_id',
@@ -84,54 +105,54 @@ describe('ProjectEntityHandler', function () {
],
},
]
this.ProjectGetter.promises.getProjectWithoutDocLines = sinon
ctx.ProjectGetter.promises.getProjectWithoutDocLines = sinon
.stub()
.resolves(this.project)
.resolves(ctx.project)
})
describe('getAllDocs', function () {
let fetchedDocs
beforeEach(async function () {
this.docs = [
beforeEach(async function (ctx) {
ctx.docs = [
{
_id: this.doc1._id,
lines: (this.lines1 = ['one']),
rev: (this.rev1 = 1),
_id: ctx.doc1._id,
lines: (ctx.lines1 = ['one']),
rev: (ctx.rev1 = 1),
},
{
_id: this.doc2._id,
lines: (this.lines2 = ['two']),
rev: (this.rev2 = 2),
_id: ctx.doc2._id,
lines: (ctx.lines2 = ['two']),
rev: (ctx.rev2 = 2),
},
]
this.DocstoreManager.promises.getAllDocs = sinon
ctx.DocstoreManager.promises.getAllDocs = sinon
.stub()
.resolves(this.docs)
.resolves(ctx.docs)
fetchedDocs =
await this.ProjectEntityHandler.promises.getAllDocs(projectId)
await ctx.ProjectEntityHandler.promises.getAllDocs(projectId)
})
it('should get the doc lines and rev from the docstore', function () {
this.DocstoreManager.promises.getAllDocs
it('should get the doc lines and rev from the docstore', function (ctx) {
ctx.DocstoreManager.promises.getAllDocs
.calledWith(projectId)
.should.equal(true)
})
it('should call the callback with the docs with the lines and rev included', function () {
it('should call the callback with the docs with the lines and rev included', function (ctx) {
expect(fetchedDocs).to.deep.equal({
'/doc1': {
_id: this.doc1._id,
lines: this.lines1,
name: this.doc1.name,
rev: this.rev1,
folder: this.project.rootFolder[0],
_id: ctx.doc1._id,
lines: ctx.lines1,
name: ctx.doc1.name,
rev: ctx.rev1,
folder: ctx.project.rootFolder[0],
},
'/folder1/doc2': {
_id: this.doc2._id,
lines: this.lines2,
name: this.doc2.name,
rev: this.rev2,
folder: this.folder1,
_id: ctx.doc2._id,
lines: ctx.lines2,
name: ctx.doc2.name,
rev: ctx.rev2,
folder: ctx.folder1,
},
})
})
@@ -139,85 +160,85 @@ describe('ProjectEntityHandler', function () {
describe('getAllFiles', function () {
let allFiles
beforeEach(async function () {
this.callback = sinon.stub()
allFiles = await this.ProjectEntityHandler.promises.getAllFiles(
beforeEach(async function (ctx) {
ctx.callback = sinon.stub()
allFiles = await ctx.ProjectEntityHandler.promises.getAllFiles(
projectId,
this.callback
ctx.callback
)
})
it('should call the callback with the files', function () {
it('should call the callback with the files', function (ctx) {
expect(allFiles).to.deep.equal({
'/file1': { ...this.file1, folder: this.project.rootFolder[0] },
'/folder1/file2': { ...this.file2, folder: this.folder1 },
'/file1': { ...ctx.file1, folder: ctx.project.rootFolder[0] },
'/folder1/file2': { ...ctx.file2, folder: ctx.folder1 },
})
})
})
describe('getAllDocPathsFromProject', function () {
beforeEach(function () {
this.docs = [
beforeEach(function (ctx) {
ctx.docs = [
{
_id: this.doc1._id,
lines: (this.lines1 = ['one']),
rev: (this.rev1 = 1),
_id: ctx.doc1._id,
lines: (ctx.lines1 = ['one']),
rev: (ctx.rev1 = 1),
},
{
_id: this.doc2._id,
lines: (this.lines2 = ['two']),
rev: (this.rev2 = 2),
_id: ctx.doc2._id,
lines: (ctx.lines2 = ['two']),
rev: (ctx.rev2 = 2),
},
]
})
it('should call the callback with the path for each docId', function () {
it('should call the callback with the path for each docId', function (ctx) {
const expected = {
[this.doc1._id]: `/${this.doc1.name}`,
[this.doc2._id]: `/folder1/${this.doc2.name}`,
[ctx.doc1._id]: `/${ctx.doc1.name}`,
[ctx.doc2._id]: `/folder1/${ctx.doc2.name}`,
}
expect(
this.ProjectEntityHandler.getAllDocPathsFromProject(
this.project,
this.callback
ctx.ProjectEntityHandler.getAllDocPathsFromProject(
ctx.project,
ctx.callback
)
).to.deep.equal(expected)
})
})
describe('getDocPathByProjectIdAndDocId', function () {
it('should call the callback with the path for an existing doc id at the root level', async function () {
it('should call the callback with the path for an existing doc id at the root level', async function (ctx) {
const path =
await this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
await ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
projectId,
this.doc1._id
ctx.doc1._id
)
expect(path).to.deep.equal(`/${this.doc1.name}`)
expect(path).to.deep.equal(`/${ctx.doc1.name}`)
})
it('should call the callback with the path for an existing doc id nested within a folder', async function () {
it('should call the callback with the path for an existing doc id nested within a folder', async function (ctx) {
const path =
await this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
await ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
projectId,
this.doc2._id
ctx.doc2._id
)
expect(path).to.deep.equal(`/folder1/${this.doc2.name}`)
expect(path).to.deep.equal(`/folder1/${ctx.doc2.name}`)
})
it('should call the callback with a NotFoundError for a non-existing doc', async function () {
it('should call the callback with a NotFoundError for a non-existing doc', async function (ctx) {
await expect(
this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
projectId,
'non-existing-id'
)
).to.be.rejectedWith(Errors.NotFoundError)
})
it('should call the callback with a NotFoundError for an existing file', async function () {
it('should call the callback with a NotFoundError for an existing file', async function (ctx) {
await expect(
this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
projectId,
this.file1._id
ctx.file1._id
)
).to.be.rejectedWith(Errors.NotFoundError)
})
@@ -225,66 +246,66 @@ describe('ProjectEntityHandler', function () {
describe('_getAllFolders', async function () {
let folders
beforeEach(async function () {
this.callback = sinon.stub()
beforeEach(async function (ctx) {
ctx.callback = sinon.stub()
folders =
await this.ProjectEntityHandler.promises._getAllFolders(projectId)
await ctx.ProjectEntityHandler.promises._getAllFolders(projectId)
})
it('should get the project without the docs lines', function () {
this.ProjectGetter.promises.getProjectWithoutDocLines
it('should get the project without the docs lines', function (ctx) {
ctx.ProjectGetter.promises.getProjectWithoutDocLines
.calledWith(projectId)
.should.equal(true)
})
it('should call the callback with the folders', function () {
it('should call the callback with the folders', function (ctx) {
expect(folders).to.deep.equal([
{ path: '/', folder: this.project.rootFolder[0] },
{ path: '/folder1', folder: this.folder1 },
{ path: '/', folder: ctx.project.rootFolder[0] },
{ path: '/folder1', folder: ctx.folder1 },
])
})
})
describe('_getAllFoldersFromProject', function () {
it('should return the folders', function () {
it('should return the folders', function (ctx) {
expect(
this.ProjectEntityHandler._getAllFoldersFromProject(this.project)
ctx.ProjectEntityHandler._getAllFoldersFromProject(ctx.project)
).to.deep.equal([
{ path: '/', folder: this.project.rootFolder[0] },
{ path: '/folder1', folder: this.folder1 },
{ path: '/', folder: ctx.project.rootFolder[0] },
{ path: '/folder1', folder: ctx.folder1 },
])
})
})
})
describe('with an invalid file tree', function () {
beforeEach(function () {
this.project.rootFolder = [
beforeEach(function (ctx) {
ctx.project.rootFolder = [
{
docs: [
(this.doc1 = {
(ctx.doc1 = {
name: null, // invalid doc name
_id: 'doc1_id',
}),
],
fileRefs: [
(this.file1 = {
(ctx.file1 = {
rev: 1,
_id: 'file1_id',
name: null, // invalid file name
}),
],
folders: [
(this.folder1 = {
(ctx.folder1 = {
name: null, // invalid folder name
docs: [
(this.doc2 = {
(ctx.doc2 = {
name: 'doc2',
_id: 'doc2_id',
}),
],
fileRefs: [
(this.file2 = {
(ctx.file2 = {
rev: 2,
name: 'file2',
_id: 'file2_id',
@@ -296,107 +317,107 @@ describe('ProjectEntityHandler', function () {
],
},
]
this.ProjectGetter.promises.getProjectWithoutDocLines = sinon
ctx.ProjectGetter.promises.getProjectWithoutDocLines = sinon
.stub()
.resolves(this.project)
.resolves(ctx.project)
})
describe('getAllDocs', function () {
beforeEach(async function () {
this.docs = [
beforeEach(async function (ctx) {
ctx.docs = [
{
_id: this.doc1._id,
lines: (this.lines1 = ['one']),
rev: (this.rev1 = 1),
_id: ctx.doc1._id,
lines: (ctx.lines1 = ['one']),
rev: (ctx.rev1 = 1),
},
{
_id: this.doc2._id,
lines: (this.lines2 = ['two']),
rev: (this.rev2 = 2),
_id: ctx.doc2._id,
lines: (ctx.lines2 = ['two']),
rev: (ctx.rev2 = 2),
},
]
this.DocstoreManager.promises.getAllDocs = sinon
ctx.DocstoreManager.promises.getAllDocs = sinon
.stub()
.resolves(this.docs)
.resolves(ctx.docs)
})
it('should call the callback with an error', async function () {
await expect(this.ProjectEntityHandler.promises.getAllDocs(projectId))
.to.be.rejected
it('should call the callback with an error', async function (ctx) {
await expect(ctx.ProjectEntityHandler.promises.getAllDocs(projectId)).to
.be.rejected
})
})
describe('getAllFiles', function () {
it('should call the callback with and error', async function () {
await expect(this.ProjectEntityHandler.promises.getAllFiles(projectId))
it('should call the callback with and error', async function (ctx) {
await expect(ctx.ProjectEntityHandler.promises.getAllFiles(projectId))
.to.be.rejected
})
})
describe('getDocPathByProjectIdAndDocId', function () {
it('should call the callback with an error for an existing doc id at the root level', async function () {
it('should call the callback with an error for an existing doc id at the root level', async function (ctx) {
await expect(
this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
projectId,
this.doc1._id
ctx.doc1._id
)
).to.be.rejectedWith(Error)
})
it('should call the callback with an error for an existing doc id nested within a folder', async function () {
it('should call the callback with an error for an existing doc id nested within a folder', async function (ctx) {
await expect(
this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
projectId,
this.doc2._id
ctx.doc2._id
)
).to.be.rejectedWith(Error)
})
it('should call the callback with an error for a non-existing doc', async function () {
it('should call the callback with an error for a non-existing doc', async function (ctx) {
await expect(
this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
projectId,
'non-existing-id'
)
).to.be.rejectedWith(Error)
})
it('should call the callback with an error for an existing file', async function () {
it('should call the callback with an error for an existing file', async function (ctx) {
await expect(
this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
ctx.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
projectId,
this.file1._id
ctx.file1._id
)
).to.be.rejectedWith(Error)
})
})
describe('_getAllFolders', function () {
it('should call the callback with an error', async function () {
it('should call the callback with an error', async function (ctx) {
await expect(
this.ProjectEntityHandler.promises._getAllFolders(projectId)
ctx.ProjectEntityHandler.promises._getAllFolders(projectId)
).to.be.rejected
})
})
describe('getAllEntities', function () {
beforeEach(function () {
this.ProjectGetter.promises.getProject = sinon
beforeEach(function (ctx) {
ctx.ProjectGetter.promises.getProject = sinon
.stub()
.resolves(this.project)
.resolves(ctx.project)
})
it('should call the callback with an error', async function () {
it('should call the callback with an error', async function (ctx) {
await expect(
this.ProjectEntityHandler.promises.getAllEntities(projectId)
ctx.ProjectEntityHandler.promises.getAllEntities(projectId)
).to.be.rejected
})
})
describe('getAllDocPathsFromProjectById', function () {
it('should call the callback with an error', async function () {
it('should call the callback with an error', async function (ctx) {
await expect(
this.ProjectEntityHandler.promises.getAllDocPathsFromProjectById(
ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById(
projectId
)
).to.be.rejected
@@ -404,11 +425,11 @@ describe('ProjectEntityHandler', function () {
})
describe('getDocPathFromProjectByDocId', function () {
it('should call the callback with an error', async function () {
it('should call the callback with an error', async function (ctx) {
await expect(
this.ProjectEntityHandler.promises.getDocPathFromProjectByDocId(
ctx.ProjectEntityHandler.promises.getDocPathFromProjectByDocId(
projectId,
this.doc1._id
ctx.doc1._id
)
).to.be.rejected
})
@@ -416,26 +437,26 @@ describe('ProjectEntityHandler', function () {
})
describe('getDoc', function () {
beforeEach(function () {
this.lines = ['mock', 'doc', 'lines']
this.rev = 5
this.version = 42
this.ranges = { mock: 'ranges' }
this.callback = sinon.stub()
this.DocstoreManager.promises.getDoc = sinon.stub().resolves({
lines: this.lines,
rev: this.rev,
version: this.version,
ranges: this.ranges,
beforeEach(function (ctx) {
ctx.lines = ['mock', 'doc', 'lines']
ctx.rev = 5
ctx.version = 42
ctx.ranges = { mock: 'ranges' }
ctx.callback = sinon.stub()
ctx.DocstoreManager.promises.getDoc = sinon.stub().resolves({
lines: ctx.lines,
rev: ctx.rev,
version: ctx.version,
ranges: ctx.ranges,
})
})
it('should call the callback with the lines, version and rev', async function () {
const doc = await this.ProjectEntityHandler.promises.getDoc(
it('should call the callback with the lines, version and rev', async function (ctx) {
const doc = await ctx.ProjectEntityHandler.promises.getDoc(
projectId,
docId
)
this.DocstoreManager.promises.getDoc
ctx.DocstoreManager.promises.getDoc
.calledWith(projectId, docId)
.should.equal(true)
expect(doc).to.exist
@@ -445,32 +466,32 @@ describe('ProjectEntityHandler', function () {
describe('promises.getDoc', function () {
let result
beforeEach(async function () {
this.lines = ['mock', 'doc', 'lines']
this.rev = 5
this.version = 42
this.ranges = { mock: 'ranges' }
beforeEach(async function (ctx) {
ctx.lines = ['mock', 'doc', 'lines']
ctx.rev = 5
ctx.version = 42
ctx.ranges = { mock: 'ranges' }
this.DocstoreManager.promises.getDoc = sinon.stub().resolves({
lines: this.lines,
rev: this.rev,
version: this.version,
ranges: this.ranges,
ctx.DocstoreManager.promises.getDoc = sinon.stub().resolves({
lines: ctx.lines,
rev: ctx.rev,
version: ctx.version,
ranges: ctx.ranges,
})
result = await this.ProjectEntityHandler.promises.getDoc(projectId, docId)
result = await ctx.ProjectEntityHandler.promises.getDoc(projectId, docId)
})
it('should call the docstore', function () {
this.DocstoreManager.promises.getDoc
it('should call the docstore', function (ctx) {
ctx.DocstoreManager.promises.getDoc
.calledWith(projectId, docId)
.should.equal(true)
})
it('should return the lines, rev, version and ranges', function () {
expect(result.lines).to.equal(this.lines)
expect(result.rev).to.equal(this.rev)
expect(result.version).to.equal(this.version)
expect(result.ranges).to.equal(this.ranges)
it('should return the lines, rev, version and ranges', function (ctx) {
expect(result.lines).to.equal(ctx.lines)
expect(result.rev).to.equal(ctx.rev)
expect(result.version).to.equal(ctx.version)
expect(result.ranges).to.equal(ctx.ranges)
})
})
})

View File

@@ -1,30 +1,31 @@
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath = '../../../../app/src/Features/Project/ProjectGetter.js'
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb-legacy')
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(function () {
this.project = { _id: new ObjectId() }
this.projectIdStr = this.project._id.toString()
this.deletedProject = { deleterData: { wombat: 'potato' } }
this.userId = new ObjectId()
beforeEach(async function (ctx) {
ctx.project = { _id: new ObjectId() }
ctx.projectIdStr = ctx.project._id.toString()
ctx.deletedProject = { deleterData: { wombat: 'potato' } }
ctx.userId = new ObjectId()
this.DeletedProject = {
ctx.DeletedProject = {
find: sinon.stub().returns({
exec: sinon.stub().resolves([this.deletedProject]),
exec: sinon.stub().resolves([ctx.deletedProject]),
}),
}
this.Project = {
ctx.Project = {
find: sinon.stub().returns({
exec: sinon.stub().resolves(),
}),
findOne: sinon.stub().returns({
exec: sinon.stub().resolves(this.project),
exec: sinon.stub().resolves(ctx.project),
}),
}
this.CollaboratorsGetter = {
ctx.CollaboratorsGetter = {
promises: {
getProjectsUserIsMemberOf: sinon.stub().resolves({
readAndWrite: [],
@@ -34,58 +35,76 @@ describe('ProjectGetter', function () {
}),
},
}
this.LockManager = {
ctx.LockManager = {
promises: {
runWithLock: sinon
.stub()
.callsFake((namespace, id, runner) => runner()),
},
}
this.db = {
ctx.db = {
projects: {
findOne: sinon.stub().resolves(this.project),
findOne: sinon.stub().resolves(ctx.project),
},
users: {},
}
this.ProjectEntityMongoUpdateHandler = {
ctx.ProjectEntityMongoUpdateHandler = {
lockKey: sinon.stub().returnsArg(0),
}
this.ProjectGetter = SandboxedModule.require(modulePath, {
requires: {
'../../infrastructure/mongodb': { db: this.db, ObjectId },
'../../models/Project': {
Project: this.Project,
},
'../../models/DeletedProject': {
DeletedProject: this.DeletedProject,
},
'../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
'../../infrastructure/LockManager': this.LockManager,
'./ProjectEntityMongoUpdateHandler':
this.ProjectEntityMongoUpdateHandler,
},
})
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('getProjectWithoutDocLines', function () {
beforeEach(function () {
this.ProjectGetter.promises.getProject = sinon.stub().resolves()
beforeEach(function (ctx) {
ctx.ProjectGetter.promises.getProject = sinon.stub().resolves()
})
describe('passing an id', function () {
beforeEach(async function () {
await this.ProjectGetter.promises.getProjectWithoutDocLines(
this.project._id
beforeEach(async function (ctx) {
await ctx.ProjectGetter.promises.getProjectWithoutDocLines(
ctx.project._id
)
})
it('should call find with the project id', function () {
this.ProjectGetter.promises.getProject
.calledWith(this.project._id)
it('should call find with the project id', function (ctx) {
ctx.ProjectGetter.promises.getProject
.calledWith(ctx.project._id)
.should.equal(true)
})
it('should exclude the doc lines', function () {
it('should exclude the doc lines', function (ctx) {
const excludes = {
'rootFolder.docs.lines': 0,
'rootFolder.folders.docs.lines': 0,
@@ -97,32 +116,32 @@ describe('ProjectGetter', function () {
'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs.lines': 0,
}
this.ProjectGetter.promises.getProject
.calledWith(this.project._id, excludes)
ctx.ProjectGetter.promises.getProject
.calledWith(ctx.project._id, excludes)
.should.equal(true)
})
})
})
describe('getProjectWithOnlyFolders', function () {
beforeEach(function () {
this.ProjectGetter.promises.getProject = sinon.stub().resolves()
beforeEach(function (ctx) {
ctx.ProjectGetter.promises.getProject = sinon.stub().resolves()
})
describe('passing an id', function () {
beforeEach(async function () {
await this.ProjectGetter.promises.getProjectWithOnlyFolders(
this.project._id
beforeEach(async function (ctx) {
await ctx.ProjectGetter.promises.getProjectWithOnlyFolders(
ctx.project._id
)
})
it('should call find with the project id', function () {
this.ProjectGetter.promises.getProject
.calledWith(this.project._id)
it('should call find with the project id', function (ctx) {
ctx.ProjectGetter.promises.getProject
.calledWith(ctx.project._id)
.should.equal(true)
})
it('should exclude the docs and files lines', function () {
it('should exclude the docs and files lines', function (ctx) {
const excludes = {
'rootFolder.docs': 0,
'rootFolder.fileRefs': 0,
@@ -141,8 +160,8 @@ describe('ProjectGetter', function () {
'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs': 0,
'rootFolder.folders.folders.folders.folders.folders.folders.folders.fileRefs': 0,
}
this.ProjectGetter.promises.getProject
.calledWith(this.project._id, excludes)
ctx.ProjectGetter.promises.getProject
.calledWith(ctx.project._id, excludes)
.should.equal(true)
})
})
@@ -151,58 +170,58 @@ describe('ProjectGetter', function () {
describe('getProject', function () {
describe('without projection', function () {
describe('with project id', function () {
beforeEach(async function () {
await this.ProjectGetter.promises.getProject(this.projectIdStr)
beforeEach(async function (ctx) {
await ctx.ProjectGetter.promises.getProject(ctx.projectIdStr)
})
it('should call findOne with the project id', function () {
expect(this.db.projects.findOne.callCount).to.equal(1)
it('should call findOne with the project id', function (ctx) {
expect(ctx.db.projects.findOne.callCount).to.equal(1)
expect(
this.db.projects.findOne.lastCall.args[0]._id.toString()
).to.equal(this.projectIdStr)
ctx.db.projects.findOne.lastCall.args[0]._id.toString()
).to.equal(ctx.projectIdStr)
})
})
describe('without project id', function () {
it('should be rejected', function () {
it('should be rejected', function (ctx) {
expect(
this.ProjectGetter.promises.getProject(null)
ctx.ProjectGetter.promises.getProject(null)
).to.be.rejectedWith('no project id provided')
expect(this.db.projects.findOne.callCount).to.equal(0)
expect(ctx.db.projects.findOne.callCount).to.equal(0)
})
})
})
describe('with projection', function () {
beforeEach(function () {
this.projection = { _id: 1 }
beforeEach(function (ctx) {
ctx.projection = { _id: 1 }
})
describe('with project id', function () {
beforeEach(async function () {
await this.ProjectGetter.promises.getProject(
this.projectIdStr,
this.projection
beforeEach(async function (ctx) {
await ctx.ProjectGetter.promises.getProject(
ctx.projectIdStr,
ctx.projection
)
})
it('should call findOne with the project id', function () {
expect(this.db.projects.findOne.callCount).to.equal(1)
it('should call findOne with the project id', function (ctx) {
expect(ctx.db.projects.findOne.callCount).to.equal(1)
expect(
this.db.projects.findOne.lastCall.args[0]._id.toString()
).to.equal(this.projectIdStr)
expect(this.db.projects.findOne.lastCall.args[1]).to.deep.equal({
projection: this.projection,
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 () {
it('should be rejected', function (ctx) {
expect(
this.ProjectGetter.promises.getProject(null)
ctx.ProjectGetter.promises.getProject(null)
).to.be.rejectedWith('no project id provided')
expect(this.db.projects.findOne.callCount).to.equal(0)
expect(ctx.db.projects.findOne.callCount).to.equal(0)
})
})
})
@@ -211,155 +230,151 @@ describe('ProjectGetter', function () {
describe('getProjectWithoutLock', function () {
describe('without projection', function () {
describe('with project id', function () {
beforeEach(async function () {
await this.ProjectGetter.promises.getProjectWithoutLock(
this.projectIdStr
beforeEach(async function (ctx) {
await ctx.ProjectGetter.promises.getProjectWithoutLock(
ctx.projectIdStr
)
})
it('should call findOne with the project id', function () {
expect(this.db.projects.findOne.callCount).to.equal(1)
it('should call findOne with the project id', function (ctx) {
expect(ctx.db.projects.findOne.callCount).to.equal(1)
expect(
this.db.projects.findOne.lastCall.args[0]._id.toString()
).to.equal(this.projectIdStr)
ctx.db.projects.findOne.lastCall.args[0]._id.toString()
).to.equal(ctx.projectIdStr)
})
})
describe('without project id', function () {
it('should be rejected', function () {
it('should be rejected', function (ctx) {
expect(
this.ProjectGetter.promises.getProjectWithoutLock(null)
ctx.ProjectGetter.promises.getProjectWithoutLock(null)
).to.be.rejectedWith('no project id provided')
expect(this.db.projects.findOne.callCount).to.equal(0)
expect(ctx.db.projects.findOne.callCount).to.equal(0)
})
})
})
describe('with projection', function () {
beforeEach(function () {
this.projection = { _id: 1 }
beforeEach(function (ctx) {
ctx.projection = { _id: 1 }
})
describe('with project id', function () {
beforeEach(async function () {
await this.ProjectGetter.promises.getProjectWithoutLock(
this.project._id,
this.projection
beforeEach(async function (ctx) {
await ctx.ProjectGetter.promises.getProjectWithoutLock(
ctx.project._id,
ctx.projection
)
})
it('should call findOne with the project id', function () {
expect(this.db.projects.findOne.callCount).to.equal(1)
it('should call findOne with the project id', function (ctx) {
expect(ctx.db.projects.findOne.callCount).to.equal(1)
expect(
this.db.projects.findOne.lastCall.args[0]._id.toString()
).to.equal(this.projectIdStr)
expect(this.db.projects.findOne.lastCall.args[1]).to.deep.equal({
projection: this.projection,
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 () {
it('should be rejected', function (ctx) {
expect(
this.ProjectGetter.promises.getProjectWithoutLock(null)
ctx.ProjectGetter.promises.getProjectWithoutLock(null)
).to.be.rejectedWith('no project id provided')
expect(this.db.projects.findOne.callCount).to.equal(0)
expect(ctx.db.projects.findOne.callCount).to.equal(0)
})
})
})
})
describe('findAllUsersProjects', function () {
beforeEach(function () {
this.fields = { mock: 'fields' }
this.projectOwned = { _id: 'mock-owned-projects' }
this.projectRW = { _id: 'mock-rw-projects' }
this.projectReview = { _id: 'mock-review-projects' }
this.projectRO = { _id: 'mock-ro-projects' }
this.projectTokenRW = { _id: 'mock-token-rw-projects' }
this.projectTokenRO = { _id: 'mock-token-ro-projects' }
this.Project.find
.withArgs({ owner_ref: this.userId }, this.fields)
.returns({ exec: sinon.stub().resolves([this.projectOwned]) })
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 () {
this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
readAndWrite: [this.projectRW],
readOnly: [this.projectRO],
tokenReadAndWrite: [this.projectTokenRW],
tokenReadOnly: [this.projectTokenRO],
review: [this.projectReview],
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 this.ProjectGetter.promises.findAllUsersProjects(
this.userId,
this.fields
const projects = await ctx.ProjectGetter.promises.findAllUsersProjects(
ctx.userId,
ctx.fields
)
expect(projects).to.deep.equal({
owned: [this.projectOwned],
readAndWrite: [this.projectRW],
readOnly: [this.projectRO],
tokenReadAndWrite: [this.projectTokenRW],
tokenReadOnly: [this.projectTokenRO],
review: [this.projectReview],
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 () {
this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
readAndWrite: [this.projectRW, this.projectOwned],
readOnly: [this.projectRO, this.projectRW],
tokenReadAndWrite: [this.projectTokenRW, this.projectRO],
tokenReadOnly: [
this.projectTokenRW,
this.projectTokenRO,
this.projectRO,
],
review: [this.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 this.ProjectGetter.promises.findAllUsersProjects(
this.userId,
this.fields
const projects = await ctx.ProjectGetter.promises.findAllUsersProjects(
ctx.userId,
ctx.fields
)
expect(projects).to.deep.equal({
owned: [this.projectOwned],
readAndWrite: [this.projectRW],
readOnly: [this.projectRO],
tokenReadAndWrite: [this.projectTokenRW],
tokenReadOnly: [this.projectTokenRO],
review: [this.projectReview],
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 () {
this.beforeEach(async function () {
this.projectIdFound =
await this.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
beforeEach(async function (ctx) {
ctx.projectIdFound =
await ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
'token'
)
})
it('should find project with token', function () {
this.Project.findOne
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 () {
expect(this.projectIdFound).to.equal(this.project._id)
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 () {
this.Project.findOne.returns({ exec: sinon.stub().resolves(null) })
it('should return undefined', async function (ctx) {
ctx.Project.findOne.returns({ exec: sinon.stub().resolves(null) })
const projectId =
await this.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
await ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
'token'
)
@@ -368,86 +383,79 @@ describe('ProjectGetter', function () {
})
describe('when project find returns error', function () {
this.beforeEach(async function () {
this.Project.findOne.returns({ exec: sinon.stub().rejects() })
beforeEach(async function (ctx) {
ctx.Project.findOne.returns({ exec: sinon.stub().rejects() })
})
it('should be rejected', function () {
it('should be rejected', function (ctx) {
expect(
this.ProjectGetter.promises.getProjectIdByReadAndWriteToken('token')
ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken('token')
).to.be.rejected
})
})
})
describe('findUsersProjectsByName', function () {
it('should perform a case-insensitive search', async function () {
this.project1 = { _id: 1, name: 'find me!' }
this.project2 = { _id: 2, name: 'not me!' }
this.project3 = { _id: 3, name: 'FIND ME!' }
this.project4 = { _id: 4, name: 'Find Me!' }
this.Project.find.withArgs({ owner_ref: this.userId }).returns({
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([
this.project1,
this.project2,
this.project3,
this.project4,
]),
.resolves([ctx.project1, ctx.project2, ctx.project3, ctx.project4]),
})
const projects =
await this.ProjectGetter.promises.findUsersProjectsByName(
this.userId,
this.project1.name
)
const projects = await ctx.ProjectGetter.promises.findUsersProjectsByName(
ctx.userId,
ctx.project1.name
)
const projectNames = projects.map(project => project.name)
expect(projectNames).to.have.members([
this.project1.name,
this.project3.name,
this.project4.name,
ctx.project1.name,
ctx.project3.name,
ctx.project4.name,
])
})
it('should search collaborations as well', async function () {
this.project1 = { _id: 1, name: 'find me!' }
this.project2 = { _id: 2, name: 'FIND ME!' }
this.project3 = { _id: 3, name: 'Find Me!' }
this.project4 = { _id: 4, name: 'find ME!' }
this.project5 = { _id: 5, name: 'FIND me!' }
this.Project.find
.withArgs({ owner_ref: this.userId })
.returns({ exec: sinon.stub().resolves([this.project1]) })
this.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
readAndWrite: [this.project2],
readOnly: [this.project3],
tokenReadAndWrite: [this.project4],
tokenReadOnly: [this.project5],
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 this.ProjectGetter.promises.findUsersProjectsByName(
this.userId,
this.project1.name
)
const projects = await ctx.ProjectGetter.promises.findUsersProjectsByName(
ctx.userId,
ctx.project1.name
)
expect(projects.map(project => project.name)).to.have.members([
this.project1.name,
this.project2.name,
ctx.project1.name,
ctx.project2.name,
])
})
})
describe('getUsersDeletedProjects', function () {
it('should look up the deleted projects by deletedProjectOwnerId', async function () {
await this.ProjectGetter.promises.getUsersDeletedProjects('giraffe')
sinon.assert.calledWith(this.DeletedProject.find, {
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 () {
it('should pass the found projects to the callback', async function (ctx) {
const docs =
await this.ProjectGetter.promises.getUsersDeletedProjects('giraffe')
expect(docs).to.deep.equal([this.deletedProject])
await ctx.ProjectGetter.promises.getUsersDeletedProjects('giraffe')
expect(docs).to.deep.equal([ctx.deletedProject])
})
})
})

View File

@@ -46,11 +46,14 @@ describe('ProjectHistoryHandler', function () {
})
)
vi.doMock('../../../../app/src/Features/History/HistoryManager.mjs', () => ({
default: (ctx.HistoryManager = {
promises: {},
}),
}))
vi.doMock(
'../../../../app/src/Features/History/HistoryManager.mjs',
() => ({
default: (ctx.HistoryManager = {
promises: {},
}),
})
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectEntityUpdateHandler',

View File

@@ -5,10 +5,13 @@ import Errors from '../../../../app/src/Features/Errors/Errors.js'
const ObjectId = mongodb.ObjectId
const MODULE_PATH = new URL(
'../../../../app/src/Features/Project/ProjectListController',
import.meta.url
).pathname
const MODULE_PATH = `${import.meta.dirname}/../../../../app/src/Features/Project/ProjectListController`
// Mock AnalyticsManager as it isn't used in these tests but causes the User model to be imported
// TODO: remove this once all models are ESM and this kind of mocking is no longer necessary
vi.mock('../../../../app/src/Features/Analytics/AnalyticsManager.js', () => {
return {}
})
describe('ProjectListController', function () {
beforeEach(async function (ctx) {

View File

@@ -1,8 +1,11 @@
const { expect } = require('chai')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
const modulePath = '../../../../app/src/Features/Project/ProjectLocator'
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const Errors = require('../../../../app/src/Features/Errors/Errors')
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
const project = { _id: '1234566', rootFolder: [] }
const rootDoc = { name: 'rootDoc', _id: 'das239djd' }
@@ -38,47 +41,50 @@ project.rootFolder[0] = rootFolder
project.rootDoc_id = rootDoc._id
describe('ProjectLocator', function () {
beforeEach(function () {
this.ProjectGetter = {
beforeEach(async function (ctx) {
ctx.ProjectGetter = {
getProject: sinon.stub().callsArgWith(2, null, project),
}
this.ProjectHelper = {
ctx.ProjectHelper = {
isArchived: sinon.stub(),
isTrashed: sinon.stub(),
isArchivedOrTrashed: sinon.stub(),
}
this.locator = SandboxedModule.require(modulePath, {
requires: {
'../../models/User': { User: this.User },
'./ProjectGetter': this.ProjectGetter,
'./ProjectHelper': this.ProjectHelper,
},
})
vi.doMock('../../../../app/src/models/User', () => ({
default: { User: ctx.User },
}))
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: ctx.ProjectGetter,
}))
vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({
default: ctx.ProjectHelper,
}))
ctx.locator = (await import(modulePath)).default
})
describe('finding a doc', function () {
it('finds one at the root level', async function () {
const { element, path, folder } = await this.locator.promises.findElement(
{
project_id: project._id,
element_id: doc2._id,
type: 'docs',
}
)
it('finds one at the root level', async function (ctx) {
const { element, path, folder } = await ctx.locator.promises.findElement({
project_id: project._id,
element_id: doc2._id,
type: 'docs',
})
element._id.should.equal(doc2._id)
path.fileSystem.should.equal(`/${doc2.name}`)
folder._id.should.equal(project.rootFolder[0]._id)
path.mongo.should.equal('rootFolder.0.docs.1')
})
it('when it is nested', async function () {
const { element, path, folder } = await this.locator.promises.findElement(
{
project_id: project._id,
element_id: subSubDoc._id,
type: 'doc',
}
)
it('when it is nested', async function (ctx) {
const { element, path, folder } = await ctx.locator.promises.findElement({
project_id: project._id,
element_id: subSubDoc._id,
type: 'doc',
})
expect(element._id).to.equal(subSubDoc._id)
path.fileSystem.should.equal(
`/${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}`
@@ -87,9 +93,9 @@ describe('ProjectLocator', function () {
path.mongo.should.equal('rootFolder.0.folders.1.folders.0.docs.0')
})
it('should give error if element could not be found', async function () {
it('should give error if element could not be found', async function (ctx) {
await expect(
this.locator.promises.findElement({
ctx.locator.promises.findElement({
project_id: project._id,
element_id: 'ddsd432nj42',
type: 'docs',
@@ -101,20 +107,18 @@ describe('ProjectLocator', function () {
})
describe('finding a folder', function () {
it('should return root folder when looking for root folder', async function () {
const { element: foundElement } = await this.locator.promises.findElement(
{
project_id: project._id,
element_id: rootFolder._id,
type: 'folder',
}
)
it('should return root folder when looking for root folder', async function (ctx) {
const { element: foundElement } = await ctx.locator.promises.findElement({
project_id: project._id,
element_id: rootFolder._id,
type: 'folder',
})
foundElement._id.should.equal(rootFolder._id)
})
it('should not return root folder when searching for docs', async function () {
it('should not return root folder when searching for docs', async function (ctx) {
await expect(
this.locator.promises.findElement({
ctx.locator.promises.findElement({
project_id: project._id,
element_id: rootFolder._id,
type: 'docs',
@@ -124,9 +128,9 @@ describe('ProjectLocator', function () {
.and.eventually.have.property('message', 'entity not found')
})
it('should not return root folder when searching for files', async function () {
it('should not return root folder when searching for files', async function (ctx) {
await expect(
this.locator.promises.findElement({
ctx.locator.promises.findElement({
project_id: project._id,
element_id: rootFolder._id,
type: 'files',
@@ -136,12 +140,12 @@ describe('ProjectLocator', function () {
.and.eventually.have.property('message', 'entity not found')
})
it('when at root', async function () {
it('when at root', async function (ctx) {
const {
element: foundElement,
path,
folder: parentFolder,
} = await this.locator.promises.findElement({
} = await ctx.locator.promises.findElement({
project_id: project._id,
element_id: subFolder._id,
type: 'folder',
@@ -152,12 +156,12 @@ describe('ProjectLocator', function () {
path.mongo.should.equal('rootFolder.0.folders.1')
})
it('when deeply nested', async function () {
it('when deeply nested', async function (ctx) {
const {
element: foundElement,
path,
folder: parentFolder,
} = await this.locator.promises.findElement({
} = await ctx.locator.promises.findElement({
project_id: project._id,
element_id: secondSubFolder._id,
type: 'folder',
@@ -171,12 +175,12 @@ describe('ProjectLocator', function () {
})
describe('finding a file', function () {
it('when at root', async function () {
it('when at root', async function (ctx) {
const {
element: foundElement,
path,
folder: parentFolder,
} = await this.locator.promises.findElement({
} = await ctx.locator.promises.findElement({
project_id: project._id,
element_id: file1._id,
type: 'fileRefs',
@@ -187,12 +191,12 @@ describe('ProjectLocator', function () {
path.mongo.should.equal('rootFolder.0.fileRefs.0')
})
it('when deeply nested', async function () {
it('when deeply nested', async function (ctx) {
const {
element: foundElement,
path,
folder: parentFolder,
} = await this.locator.promises.findElement({
} = await ctx.locator.promises.findElement({
project_id: project._id,
element_id: subSubFile._id,
type: 'fileRefs',
@@ -207,25 +211,21 @@ describe('ProjectLocator', function () {
})
describe('finding an element with wrong element type', function () {
it('should add an s onto the element type', async function () {
const { element: foundElement } = await this.locator.promises.findElement(
{
project_id: project._id,
element_id: subSubDoc._id,
type: 'doc',
}
)
it('should add an s onto the element type', async function (ctx) {
const { element: foundElement } = await ctx.locator.promises.findElement({
project_id: project._id,
element_id: subSubDoc._id,
type: 'doc',
})
foundElement._id.should.equal(subSubDoc._id)
})
it('should convert file to fileRefs', async function () {
const { element: foundElement } = await this.locator.promises.findElement(
{
project_id: project._id,
element_id: file1._id,
type: 'fileRefs',
}
)
it('should convert file to fileRefs', async function (ctx) {
const { element: foundElement } = await ctx.locator.promises.findElement({
project_id: project._id,
element_id: file1._id,
type: 'fileRefs',
})
foundElement._id.should.equal(file1._id)
})
})
@@ -243,12 +243,12 @@ describe('ProjectLocator', function () {
_id: '1234566',
rootFolder: [rootFolder2],
}
it('should find doc in project', async function () {
it('should find doc in project', async function (ctx) {
const {
element: foundElement,
path,
folder: parentFolder,
} = await this.locator.promises.findElement({
} = await ctx.locator.promises.findElement({
project: project2,
element_id: doc3._id,
type: 'docs',
@@ -261,38 +261,38 @@ describe('ProjectLocator', function () {
})
describe('finding root doc', function () {
it('should return root doc when passed project', async function () {
const { element: doc } = await this.locator.promises.findRootDoc(project)
it('should return root doc when passed project', async function (ctx) {
const { element: doc } = await ctx.locator.promises.findRootDoc(project)
doc._id.should.equal(rootDoc._id)
})
it('should return root doc when passed project_id', async function () {
const { element: doc } = await this.locator.promises.findRootDoc(
it('should return root doc when passed project_id', async function (ctx) {
const { element: doc } = await ctx.locator.promises.findRootDoc(
project._id
)
doc._id.should.equal(rootDoc._id)
})
it('should return null when the project has no rootDoc', async function () {
it('should return null when the project has no rootDoc', async function (ctx) {
project.rootDoc_id = null
const { element: rootDoc } =
await this.locator.promises.findRootDoc(project)
await ctx.locator.promises.findRootDoc(project)
expect(rootDoc).to.equal(null)
})
it('should return null when the rootDoc_id no longer exists', async function () {
it('should return null when the rootDoc_id no longer exists', async function (ctx) {
project.rootDoc_id = 'doesntexist'
const { element: rootDoc } =
await this.locator.promises.findRootDoc(project)
await ctx.locator.promises.findRootDoc(project)
expect(rootDoc).to.equal(null)
})
})
describe('findElementByPath', function () {
it('should take a doc path and return the element for a root level document', async function () {
it('should take a doc path and return the element for a root level document', async function (ctx) {
const path = `${doc1.name}`
const { element, type, folder } =
await this.locator.promises.findElementByPath({
await ctx.locator.promises.findElementByPath({
project,
path,
})
@@ -301,10 +301,10 @@ describe('ProjectLocator', function () {
expect(folder).to.equal(rootFolder)
})
it('should take a doc path and return the element for a root level document with a starting slash', async function () {
it('should take a doc path and return the element for a root level document with a starting slash', async function (ctx) {
const path = `/${doc1.name}`
const { element, type, folder } =
await this.locator.promises.findElementByPath({
await ctx.locator.promises.findElementByPath({
project,
path,
})
@@ -313,10 +313,10 @@ describe('ProjectLocator', function () {
expect(folder).to.equal(rootFolder)
})
it('should take a doc path and return the element for a nested document', async function () {
it('should take a doc path and return the element for a nested document', async function (ctx) {
const path = `${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}`
const { element, type, folder } =
await this.locator.promises.findElementByPath({
await ctx.locator.promises.findElementByPath({
project,
path,
})
@@ -325,10 +325,10 @@ describe('ProjectLocator', function () {
expect(folder).to.equal(secondSubFolder)
})
it('should take a file path and return the element for a root level document', async function () {
it('should take a file path and return the element for a root level document', async function (ctx) {
const path = `${file1.name}`
const { element, type, folder } =
await this.locator.promises.findElementByPath({
await ctx.locator.promises.findElementByPath({
project,
path,
})
@@ -337,10 +337,10 @@ describe('ProjectLocator', function () {
expect(folder).to.equal(rootFolder)
})
it('should take a file path and return the element for a nested document', async function () {
it('should take a file path and return the element for a nested document', async function (ctx) {
const path = `${subFolder.name}/${secondSubFolder.name}/${subSubFile.name}`
const { element, type, folder } =
await this.locator.promises.findElementByPath({
await ctx.locator.promises.findElementByPath({
project,
path,
})
@@ -349,10 +349,10 @@ describe('ProjectLocator', function () {
expect(folder).to.equal(secondSubFolder)
})
it('should take a file path and return the element for a nested document case insenstive', async function () {
it('should take a file path and return the element for a nested document case insenstive', async function (ctx) {
const path = `${subFolder.name.toUpperCase()}/${secondSubFolder.name.toUpperCase()}/${subSubFile.name.toUpperCase()}`
const { element, type, folder } =
await this.locator.promises.findElementByPath({
await ctx.locator.promises.findElementByPath({
project,
path,
})
@@ -361,10 +361,10 @@ describe('ProjectLocator', function () {
expect(folder).to.equal(secondSubFolder)
})
it('should not return elements with a case-insensitive match when exactCaseMatch is true', async function () {
it('should not return elements with a case-insensitive match when exactCaseMatch is true', async function (ctx) {
const path = `${subFolder.name.toUpperCase()}/${secondSubFolder.name.toUpperCase()}/${subSubFile.name.toUpperCase()}`
await expect(
this.locator.promises.findElementByPath({
ctx.locator.promises.findElementByPath({
project,
path,
exactCaseMatch: true,
@@ -372,10 +372,10 @@ describe('ProjectLocator', function () {
).to.eventually.be.rejected
})
it('should take a file path and return the element for a nested folder', async function () {
it('should take a file path and return the element for a nested folder', async function (ctx) {
const path = `${subFolder.name}/${secondSubFolder.name}`
const { element, type, folder } =
await this.locator.promises.findElementByPath({
await ctx.locator.promises.findElementByPath({
project,
path,
})
@@ -384,10 +384,10 @@ describe('ProjectLocator', function () {
expect(folder).to.equal(subFolder)
})
it('should take a file path and return the root folder', async function () {
it('should take a file path and return the root folder', async function (ctx) {
const path = '/'
const { element, type, folder } =
await this.locator.promises.findElementByPath({
await ctx.locator.promises.findElementByPath({
project,
path,
})
@@ -396,16 +396,16 @@ describe('ProjectLocator', function () {
expect(folder).to.equal(null)
})
it('should return an error if the file can not be found inside know folder', async function () {
it('should return an error if the file can not be found inside know folder', async function (ctx) {
const path = `${subFolder.name}/${secondSubFolder.name}/exist.txt`
await expect(this.locator.promises.findElementByPath({ project, path }))
.to.eventually.be.rejected
await expect(ctx.locator.promises.findElementByPath({ project, path })).to
.eventually.be.rejected
})
it('should return an error if the file can not be found inside unknown folder', async function () {
it('should return an error if the file can not be found inside unknown folder', async function (ctx) {
const path = 'this/does/not/exist.txt'
await expect(
this.locator.promises.findElementByPath({
ctx.locator.promises.findElementByPath({
project,
path,
})
@@ -413,8 +413,8 @@ describe('ProjectLocator', function () {
})
describe('where duplicate folder exists', function () {
beforeEach(function () {
this.duplicateFolder = {
beforeEach(function (ctx) {
ctx.duplicateFolder = {
name: 'duplicate1',
_id: '1234',
folders: [
@@ -425,13 +425,13 @@ describe('ProjectLocator', function () {
fileRefs: [],
},
],
docs: [(this.doc = { name: 'main.tex', _id: '456' })],
docs: [(ctx.doc = { name: 'main.tex', _id: '456' })],
fileRefs: [],
}
this.project = {
ctx.project = {
rootFolder: [
{
folders: [this.duplicateFolder, this.duplicateFolder],
folders: [ctx.duplicateFolder, ctx.duplicateFolder],
fileRefs: [],
docs: [],
},
@@ -439,26 +439,26 @@ describe('ProjectLocator', function () {
}
})
it('should not call the callback more than once', async function () {
const path = `${this.duplicateFolder.name}/${this.doc.name}`
await this.locator.promises.findElementByPath({
project: this.project,
it('should not call the callback more than once', async function (ctx) {
const path = `${ctx.duplicateFolder.name}/${ctx.doc.name}`
await ctx.locator.promises.findElementByPath({
project: ctx.project,
path,
})
}) // mocha will throw exception if done called multiple times
it('should not call the callback more than once when the path is longer than 1 level below the duplicate level', async function () {
const path = `${this.duplicateFolder.name}/1/main.tex`
await this.locator.promises.findElementByPath({
project: this.project,
it('should not call the callback more than once when the path is longer than 1 level below the duplicate level', async function (ctx) {
const path = `${ctx.duplicateFolder.name}/1/main.tex`
await ctx.locator.promises.findElementByPath({
project: ctx.project,
path,
})
})
}) // mocha will throw exception if done called multiple times
describe('with a null doc', function () {
beforeEach(function () {
this.project = {
beforeEach(function (ctx) {
ctx.project = {
rootFolder: [
{
folders: [],
@@ -469,10 +469,10 @@ describe('ProjectLocator', function () {
}
})
it('should not crash with a null', async function () {
it('should not crash with a null', async function (ctx) {
const path = '/other.tex'
const { element } = await this.locator.promises.findElementByPath({
project: this.project,
const { element } = await ctx.locator.promises.findElementByPath({
project: ctx.project,
path,
})
element.name.should.equal('other.tex')
@@ -480,14 +480,14 @@ describe('ProjectLocator', function () {
})
describe('with a null project', function () {
beforeEach(function () {
this.ProjectGetter = { getProject: sinon.stub().callsArg(2) }
beforeEach(function (ctx) {
ctx.ProjectGetter = { getProject: sinon.stub().callsArg(2) }
})
it('should not crash with a null', async function () {
it('should not crash with a null', async function (ctx) {
const path = '/other.tex'
await expect(
this.locator.promises.findElementByPath({
ctx.locator.promises.findElementByPath({
project_id: project._id,
path,
})
@@ -496,15 +496,13 @@ describe('ProjectLocator', function () {
})
describe('with a project_id', function () {
it('should take a doc path and return the element for a root level document', async function () {
it('should take a doc path and return the element for a root level document', async function (ctx) {
const path = `${doc1.name}`
const { element, type } = await this.locator.promises.findElementByPath(
{
project_id: project._id,
path,
}
)
this.ProjectGetter.getProject
const { element, type } = await ctx.locator.promises.findElementByPath({
project_id: project._id,
path,
})
ctx.ProjectGetter.getProject
.calledWith(project._id, { rootFolder: true, rootDoc_id: true })
.should.equal(true)
element.should.deep.equal(doc1)
@@ -514,17 +512,17 @@ describe('ProjectLocator', function () {
})
describe('findElementByMongoPath', function () {
it('traverses the file tree like Mongo would do', function () {
const element = this.locator.findElementByMongoPath(
it('traverses the file tree like Mongo would do', function (ctx) {
const element = ctx.locator.findElementByMongoPath(
project,
'rootFolder.0.folders.1.folders.0.fileRefs.0'
)
expect(element).to.equal(subSubFile)
})
it('throws an error if no element is found', function () {
it('throws an error if no element is found', function (ctx) {
expect(() =>
this.locator.findElementByMongoPath(
ctx.locator.findElementByMongoPath(
project,
'rootolder.0.folders.0.folders.0.fileRefs.0'
)

View File

@@ -1,60 +1,81 @@
const { expect } = require('chai')
const { ObjectId } = require('mongodb-legacy')
const sinon = require('sinon')
import { vi, expect } from 'vitest'
import mongodb from 'mongodb-legacy'
import sinon from 'sinon'
const modulePath =
'../../../../app/src/Features/Project/ProjectRootDocManager.js'
const SandboxedModule = require('sandboxed-module')
'../../../../app/src/Features/Project/ProjectRootDocManager.mjs'
const { ObjectId } = mongodb
describe('ProjectRootDocManager', function () {
beforeEach(function () {
this.project_id = 'project-123'
this.docPaths = {}
this.docId1 = new ObjectId()
this.docId2 = new ObjectId()
this.docId3 = new ObjectId()
this.docId4 = new ObjectId()
this.docPaths[this.docId1] = '/chapter1.tex'
this.docPaths[this.docId2] = '/main.tex'
this.docPaths[this.docId3] = '/nested/chapter1a.tex'
this.docPaths[this.docId4] = '/nested/chapter1b.tex'
this.sl_req_id = 'sl-req-id-123'
this.callback = sinon.stub()
this.globbyFiles = ['a.tex', 'b.tex', 'main.tex']
this.globby = sinon.stub().resolves(this.globbyFiles)
beforeEach(async function (ctx) {
ctx.project_id = 'project-123'
ctx.docPaths = {}
ctx.docId1 = new ObjectId()
ctx.docId2 = new ObjectId()
ctx.docId3 = new ObjectId()
ctx.docId4 = new ObjectId()
ctx.docPaths[ctx.docId1] = '/chapter1.tex'
ctx.docPaths[ctx.docId2] = '/main.tex'
ctx.docPaths[ctx.docId3] = '/nested/chapter1a.tex'
ctx.docPaths[ctx.docId4] = '/nested/chapter1b.tex'
ctx.sl_req_id = 'sl-req-id-123'
ctx.callback = sinon.stub()
ctx.globbyFiles = ['a.tex', 'b.tex', 'main.tex']
ctx.globby = sinon.stub().resolves(ctx.globbyFiles)
this.fs = {
ctx.fs = {
readFile: sinon.stub().callsArgWith(2, new Error('file not found')),
stat: sinon.stub().callsArgWith(1, null, { size: 100 }),
}
this.ProjectRootDocManager = SandboxedModule.require(modulePath, {
requires: {
'./ProjectEntityHandler': (this.ProjectEntityHandler = {}),
'./ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}),
'./ProjectGetter': (this.ProjectGetter = {}),
'../../infrastructure/GracefulShutdown': {
BackgroundTaskTracker: class {
add() {}
done() {}
},
},
globby: this.globby,
fs: this.fs,
vi.doMock(
'../../../../app/src/Features/Project/ProjectEntityHandler',
() => ({
default: (ctx.ProjectEntityHandler = {}),
})
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectEntityUpdateHandler',
() => ({
default: (ctx.ProjectEntityUpdateHandler = {}),
})
)
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: (ctx.ProjectGetter = {}),
}))
vi.doMock('../../../../app/src/infrastructure/GracefulShutdown', () => ({
BackgroundTaskTracker: class {
add() {}
done() {}
},
})
}))
vi.doMock('globby', () => ({
default: ctx.globby,
}))
vi.doMock('fs', () => ({
default: ctx.fs,
}))
ctx.ProjectRootDocManager = (await import(modulePath)).default
})
describe('setRootDocAutomatically', function () {
beforeEach(function () {
this.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
this.ProjectEntityUpdateHandler.isPathValidForRootDoc = sinon
beforeEach(function (ctx) {
ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
ctx.ProjectEntityUpdateHandler.isPathValidForRootDoc = sinon
.stub()
.returns(true)
})
describe('when there is a suitable root doc', function () {
beforeEach(async function () {
this.docs = {
beforeEach(async function (ctx) {
ctx.docs = {
'/chapter1.tex': {
_id: this.docId1,
_id: ctx.docId1,
lines: [
'something else',
'\\begin{document}',
@@ -63,7 +84,7 @@ describe('ProjectRootDocManager', function () {
],
},
'/main.tex': {
_id: this.docId2,
_id: ctx.docId2,
lines: [
'different line',
'\\documentclass{article}',
@@ -71,114 +92,114 @@ describe('ProjectRootDocManager', function () {
],
},
'/nested/chapter1a.tex': {
_id: this.docId3,
_id: ctx.docId3,
lines: ['Hello world'],
},
'/nested/chapter1b.tex': {
_id: this.docId4,
_id: ctx.docId4,
lines: ['Hello world'],
},
}
this.ProjectEntityHandler.getAllDocs = sinon
ctx.ProjectEntityHandler.getAllDocs = sinon
.stub()
.callsArgWith(1, null, this.docs)
await this.ProjectRootDocManager.promises.setRootDocAutomatically(
this.project_id
.callsArgWith(1, null, ctx.docs)
await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
ctx.project_id
)
})
it('should check the docs of the project', function () {
this.ProjectEntityHandler.getAllDocs
.calledWith(this.project_id)
it('should check the docs of the project', function (ctx) {
ctx.ProjectEntityHandler.getAllDocs
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should set the root doc to the doc containing a documentclass', function () {
this.ProjectEntityUpdateHandler.setRootDoc
.calledWith(this.project_id, this.docId2)
it('should set the root doc to the doc containing a documentclass', function (ctx) {
ctx.ProjectEntityUpdateHandler.setRootDoc
.calledWith(ctx.project_id, ctx.docId2)
.should.equal(true)
})
})
describe('when the root doc is an Rtex file', function () {
beforeEach(async function () {
this.docs = {
beforeEach(async function (ctx) {
ctx.docs = {
'/chapter1.tex': {
_id: this.docId1,
_id: ctx.docId1,
lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
},
'/main.Rtex': {
_id: this.docId2,
_id: ctx.docId2,
lines: ['\\documentclass{article}', '\\input{chapter1}'],
},
}
this.ProjectEntityHandler.getAllDocs = sinon
ctx.ProjectEntityHandler.getAllDocs = sinon
.stub()
.callsArgWith(1, null, this.docs)
await this.ProjectRootDocManager.promises.setRootDocAutomatically(
this.project_id
.callsArgWith(1, null, ctx.docs)
await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
ctx.project_id
)
})
it('should set the root doc to the doc containing a documentclass', function () {
this.ProjectEntityUpdateHandler.setRootDoc
.calledWith(this.project_id, this.docId2)
it('should set the root doc to the doc containing a documentclass', function (ctx) {
ctx.ProjectEntityUpdateHandler.setRootDoc
.calledWith(ctx.project_id, ctx.docId2)
.should.equal(true)
})
})
describe('when there is no suitable root doc', function () {
beforeEach(async function () {
this.docs = {
beforeEach(async function (ctx) {
ctx.docs = {
'/chapter1.tex': {
_id: this.docId1,
_id: ctx.docId1,
lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
},
'/style.bst': {
_id: this.docId2,
_id: ctx.docId2,
lines: ['%Example: \\documentclass{article}'],
},
}
this.ProjectEntityHandler.getAllDocs = sinon
ctx.ProjectEntityHandler.getAllDocs = sinon
.stub()
.callsArgWith(1, null, this.docs)
await this.ProjectRootDocManager.promises.setRootDocAutomatically(
this.project_id
.callsArgWith(1, null, ctx.docs)
await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
ctx.project_id
)
})
it('should not set the root doc to the doc containing a documentclass', function () {
this.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false)
it('should not set the root doc to the doc containing a documentclass', function (ctx) {
ctx.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false)
})
})
})
describe('findRootDocFileFromDirectory', function () {
beforeEach(function () {
this.fs.readFile
beforeEach(function (ctx) {
ctx.fs.readFile
.withArgs('/foo/a.tex')
.callsArgWith(2, null, 'Hello World!')
this.fs.readFile
ctx.fs.readFile
.withArgs('/foo/b.tex')
.callsArgWith(2, null, "I'm a little teapot, get me out of here.")
this.fs.readFile
ctx.fs.readFile
.withArgs('/foo/main.tex')
.callsArgWith(2, null, "Help, I'm trapped in a unit testing factory")
this.fs.readFile
ctx.fs.readFile
.withArgs('/foo/c.tex')
.callsArgWith(2, null, 'Tomato, tomahto.')
this.fs.readFile
ctx.fs.readFile
.withArgs('/foo/a/a.tex')
.callsArgWith(2, null, 'Potato? Potahto. Potootee!')
this.documentclassContent = '% test\n\\documentclass\n% test'
ctx.documentclassContent = '% test\n\\documentclass\n% test'
})
describe('when there is a file in a subfolder', function () {
beforeEach(function () {
beforeEach(function (ctx) {
// have to splice globbyFiles weirdly because of the way the stubbed globby method handles references
this.globbyFiles.splice(
ctx.globbyFiles.splice(
0,
this.globbyFiles.length,
ctx.globbyFiles.length,
'c.tex',
'a.tex',
'a/a.tex',
@@ -186,90 +207,90 @@ describe('ProjectRootDocManager', function () {
)
})
it('processes the root folder files first, and then the subfolder, in alphabetical order', async function () {
it('processes the root folder files first, and then the subfolder, in alphabetical order', async function (ctx) {
const { path } =
await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
'/foo'
)
expect(path).to.equal('a.tex')
sinon.assert.callOrder(
this.fs.readFile.withArgs('/foo/a.tex'),
this.fs.readFile.withArgs('/foo/b.tex'),
this.fs.readFile.withArgs('/foo/c.tex'),
this.fs.readFile.withArgs('/foo/a/a.tex')
ctx.fs.readFile.withArgs('/foo/a.tex'),
ctx.fs.readFile.withArgs('/foo/b.tex'),
ctx.fs.readFile.withArgs('/foo/c.tex'),
ctx.fs.readFile.withArgs('/foo/a/a.tex')
)
})
it('processes smaller files first', async function () {
this.fs.stat.withArgs('/foo/c.tex').callsArgWith(1, null, { size: 1 })
it('processes smaller files first', async function (ctx) {
ctx.fs.stat.withArgs('/foo/c.tex').callsArgWith(1, null, { size: 1 })
const { path } =
await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
'/foo'
)
expect(path).to.equal('c.tex')
sinon.assert.callOrder(
this.fs.readFile.withArgs('/foo/c.tex'),
this.fs.readFile.withArgs('/foo/a.tex'),
this.fs.readFile.withArgs('/foo/b.tex'),
this.fs.readFile.withArgs('/foo/a/a.tex')
ctx.fs.readFile.withArgs('/foo/c.tex'),
ctx.fs.readFile.withArgs('/foo/a.tex'),
ctx.fs.readFile.withArgs('/foo/b.tex'),
ctx.fs.readFile.withArgs('/foo/a/a.tex')
)
})
})
describe('when main.tex contains a documentclass', function () {
beforeEach(function () {
this.fs.readFile
beforeEach(function (ctx) {
ctx.fs.readFile
.withArgs('/foo/main.tex')
.callsArgWith(2, null, this.documentclassContent)
.callsArgWith(2, null, ctx.documentclassContent)
})
it('returns main.tex', async function () {
it('returns main.tex', async function (ctx) {
const { path, content } =
await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
'/foo'
)
expect(path).to.equal('main.tex')
expect(content).to.equal(this.documentclassContent)
expect(content).to.equal(ctx.documentclassContent)
})
it('processes main.text first and stops processing when it finds the content', async function () {
await this.ProjectRootDocManager.findRootDocFileFromDirectory('/foo')
expect(this.fs.readFile).to.be.calledWith('/foo/main.tex')
expect(this.fs.readFile).not.to.be.calledWith('/foo/a.tex')
it('processes main.text first and stops processing when it finds the content', async function (ctx) {
await ctx.ProjectRootDocManager.findRootDocFileFromDirectory('/foo')
expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex')
expect(ctx.fs.readFile).not.to.be.calledWith('/foo/a.tex')
})
})
describe('when main.tex does not contain a line starting with \\documentclass', function () {
beforeEach(function () {
this.fs.readFile.withArgs('/foo/a.tex').callsArgWith(2, null, 'foo')
this.fs.readFile.withArgs('/foo/main.tex').callsArgWith(2, null, 'foo')
this.fs.readFile.withArgs('/foo/z.tex').callsArgWith(2, null, 'foo')
this.fs.readFile
beforeEach(function (ctx) {
ctx.fs.readFile.withArgs('/foo/a.tex').callsArgWith(2, null, 'foo')
ctx.fs.readFile.withArgs('/foo/main.tex').callsArgWith(2, null, 'foo')
ctx.fs.readFile.withArgs('/foo/z.tex').callsArgWith(2, null, 'foo')
ctx.fs.readFile
.withArgs('/foo/nested/chapter1a.tex')
.callsArgWith(2, null, 'foo')
})
it('returns the first .tex file from the root folder', async function () {
this.globbyFiles.splice(
it('returns the first .tex file from the root folder', async function (ctx) {
ctx.globbyFiles.splice(
0,
this.globbyFiles.length,
ctx.globbyFiles.length,
'a.tex',
'z.tex',
'nested/chapter1a.tex'
)
const { path, content } =
await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
'/foo'
)
expect(path).to.equal('a.tex')
expect(content).to.equal('foo')
})
it('returns main.tex file from the root folder', async function () {
this.globbyFiles.splice(
it('returns main.tex file from the root folder', async function (ctx) {
ctx.globbyFiles.splice(
0,
this.globbyFiles.length,
ctx.globbyFiles.length,
'a.tex',
'z.tex',
'main.tex',
@@ -277,7 +298,7 @@ describe('ProjectRootDocManager', function () {
)
const { path, content } =
await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
'/foo'
)
expect(path).to.equal('main.tex')
@@ -286,60 +307,60 @@ describe('ProjectRootDocManager', function () {
})
describe('when a.tex contains a documentclass', function () {
beforeEach(function () {
this.fs.readFile
beforeEach(function (ctx) {
ctx.fs.readFile
.withArgs('/foo/a.tex')
.callsArgWith(2, null, this.documentclassContent)
.callsArgWith(2, null, ctx.documentclassContent)
})
it('returns a.tex', async function () {
it('returns a.tex', async function (ctx) {
const { path, content } =
await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
'/foo'
)
expect(path).to.equal('a.tex')
expect(content).to.equal(this.documentclassContent)
expect(content).to.equal(ctx.documentclassContent)
})
it('processes main.text first and stops processing when it finds the content', async function () {
await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
it('processes main.text first and stops processing when it finds the content', async function (ctx) {
await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
'/foo'
)
expect(this.fs.readFile).to.be.calledWith('/foo/main.tex')
expect(this.fs.readFile).to.be.calledWith('/foo/a.tex')
expect(this.fs.readFile).not.to.be.calledWith('/foo/b.tex')
expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex')
expect(ctx.fs.readFile).to.be.calledWith('/foo/a.tex')
expect(ctx.fs.readFile).not.to.be.calledWith('/foo/b.tex')
})
})
describe('when there is no documentclass', function () {
it('returns with no error', async function () {
await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
it('returns with no error', async function (ctx) {
await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
'/foo'
)
})
it('processes all the files', async function () {
await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
it('processes all the files', async function (ctx) {
await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
'/foo'
)
expect(this.fs.readFile).to.be.calledWith('/foo/main.tex')
expect(this.fs.readFile).to.be.calledWith('/foo/a.tex')
expect(this.fs.readFile).to.be.calledWith('/foo/b.tex')
expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex')
expect(ctx.fs.readFile).to.be.calledWith('/foo/a.tex')
expect(ctx.fs.readFile).to.be.calledWith('/foo/b.tex')
})
})
describe('when there is an error reading a file', function () {
beforeEach(function () {
this.fs.readFile
beforeEach(function (ctx) {
ctx.fs.readFile
.withArgs('/foo/a.tex')
.callsArgWith(2, new Error('something went wrong'))
})
it('returns an error', async function () {
it('returns an error', async function (ctx) {
let error
try {
await this.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
'/foo'
)
} catch (err) {
@@ -353,206 +374,196 @@ describe('ProjectRootDocManager', function () {
describe('setRootDocFromName', function () {
describe('when there is a suitable root doc', function () {
beforeEach(async function () {
this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
beforeEach(async function (ctx) {
ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
.stub()
.callsArgWith(1, null, this.docPaths)
this.ProjectEntityUpdateHandler.setRootDoc = sinon
.stub()
.callsArgWith(2)
await this.ProjectRootDocManager.promises.setRootDocFromName(
this.project_id,
.callsArgWith(1, null, ctx.docPaths)
ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
await ctx.ProjectRootDocManager.promises.setRootDocFromName(
ctx.project_id,
'/main.tex'
)
})
it('should check the docs of the project', function () {
this.ProjectEntityHandler.getAllDocPathsFromProjectById
.calledWith(this.project_id)
it('should check the docs of the project', function (ctx) {
ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should set the root doc to main.tex', function () {
this.ProjectEntityUpdateHandler.setRootDoc
.calledWith(this.project_id, this.docId2.toString())
it('should set the root doc to main.tex', function (ctx) {
ctx.ProjectEntityUpdateHandler.setRootDoc
.calledWith(ctx.project_id, ctx.docId2.toString())
.should.equal(true)
})
})
describe('when there is a suitable root doc but the leading slash is missing', function () {
beforeEach(async function () {
this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
beforeEach(async function (ctx) {
ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
.stub()
.callsArgWith(1, null, this.docPaths)
this.ProjectEntityUpdateHandler.setRootDoc = sinon
.stub()
.callsArgWith(2)
await this.ProjectRootDocManager.promises.setRootDocFromName(
this.project_id,
.callsArgWith(1, null, ctx.docPaths)
ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
await ctx.ProjectRootDocManager.promises.setRootDocFromName(
ctx.project_id,
'main.tex'
)
})
it('should check the docs of the project', function () {
this.ProjectEntityHandler.getAllDocPathsFromProjectById
.calledWith(this.project_id)
it('should check the docs of the project', function (ctx) {
ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should set the root doc to main.tex', function () {
this.ProjectEntityUpdateHandler.setRootDoc
.calledWith(this.project_id, this.docId2.toString())
it('should set the root doc to main.tex', function (ctx) {
ctx.ProjectEntityUpdateHandler.setRootDoc
.calledWith(ctx.project_id, ctx.docId2.toString())
.should.equal(true)
})
})
describe('when there is a suitable root doc with a basename match', function () {
beforeEach(async function () {
this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
beforeEach(async function (ctx) {
ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
.stub()
.callsArgWith(1, null, this.docPaths)
this.ProjectEntityUpdateHandler.setRootDoc = sinon
.stub()
.callsArgWith(2)
await this.ProjectRootDocManager.promises.setRootDocFromName(
this.project_id,
.callsArgWith(1, null, ctx.docPaths)
ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
await ctx.ProjectRootDocManager.promises.setRootDocFromName(
ctx.project_id,
'chapter1a.tex'
)
})
it('should check the docs of the project', function () {
this.ProjectEntityHandler.getAllDocPathsFromProjectById
.calledWith(this.project_id)
it('should check the docs of the project', function (ctx) {
ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should set the root doc using the basename', function () {
this.ProjectEntityUpdateHandler.setRootDoc
.calledWith(this.project_id, this.docId3.toString())
it('should set the root doc using the basename', function (ctx) {
ctx.ProjectEntityUpdateHandler.setRootDoc
.calledWith(ctx.project_id, ctx.docId3.toString())
.should.equal(true)
})
})
describe('when there is a suitable root doc but the filename is in quotes', function () {
beforeEach(async function () {
this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
beforeEach(async function (ctx) {
ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
.stub()
.callsArgWith(1, null, this.docPaths)
this.ProjectEntityUpdateHandler.setRootDoc = sinon
.stub()
.callsArgWith(2)
await this.ProjectRootDocManager.promises.setRootDocFromName(
this.project_id,
.callsArgWith(1, null, ctx.docPaths)
ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
await ctx.ProjectRootDocManager.promises.setRootDocFromName(
ctx.project_id,
"'main.tex'"
)
})
it('should check the docs of the project', function () {
this.ProjectEntityHandler.getAllDocPathsFromProjectById
.calledWith(this.project_id)
it('should check the docs of the project', function (ctx) {
ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should set the root doc to main.tex', function () {
this.ProjectEntityUpdateHandler.setRootDoc
.calledWith(this.project_id, this.docId2.toString())
it('should set the root doc to main.tex', function (ctx) {
ctx.ProjectEntityUpdateHandler.setRootDoc
.calledWith(ctx.project_id, ctx.docId2.toString())
.should.equal(true)
})
})
describe('when there is no suitable root doc', function () {
beforeEach(async function () {
this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
beforeEach(async function (ctx) {
ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
.stub()
.callsArgWith(1, null, this.docPaths)
this.ProjectEntityUpdateHandler.setRootDoc = sinon
.stub()
.callsArgWith(2)
await this.ProjectRootDocManager.promises.setRootDocFromName(
this.project_id,
.callsArgWith(1, null, ctx.docPaths)
ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
await ctx.ProjectRootDocManager.promises.setRootDocFromName(
ctx.project_id,
'other.tex'
)
})
it('should not set the root doc', function () {
this.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false)
it('should not set the root doc', function (ctx) {
ctx.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false)
})
})
})
describe('ensureRootDocumentIsSet', function () {
beforeEach(function () {
this.project = {}
this.ProjectGetter.getProject = sinon
beforeEach(function (ctx) {
ctx.project = {}
ctx.ProjectGetter.getProject = sinon
.stub()
.callsArgWith(2, null, this.project)
this.ProjectRootDocManager.setRootDocAutomatically = sinon
.callsArgWith(2, null, ctx.project)
ctx.ProjectRootDocManager.setRootDocAutomatically = sinon
.stub()
.callsArgWith(1, null)
})
describe('when the root doc is set', function () {
beforeEach(function () {
this.project.rootDoc_id = this.docId2
this.ProjectRootDocManager.ensureRootDocumentIsSet(
this.project_id,
this.callback
beforeEach(function (ctx) {
ctx.project.rootDoc_id = ctx.docId2
ctx.ProjectRootDocManager.ensureRootDocumentIsSet(
ctx.project_id,
ctx.callback
)
})
it('should find the project fetching only the rootDoc_id field', function () {
this.ProjectGetter.getProject
.calledWith(this.project_id, { rootDoc_id: 1 })
it('should find the project fetching only the rootDoc_id field', function (ctx) {
ctx.ProjectGetter.getProject
.calledWith(ctx.project_id, { rootDoc_id: 1 })
.should.equal(true)
})
it('should not try to update the project rootDoc_id', function () {
this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
it('should not try to update the project rootDoc_id', function (ctx) {
ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
false
)
})
it('should call the callback', function () {
this.callback.called.should.equal(true)
it('should call the callback', function (ctx) {
ctx.callback.called.should.equal(true)
})
})
describe('when the root doc is not set', function () {
beforeEach(function () {
this.ProjectRootDocManager.ensureRootDocumentIsSet(
this.project_id,
this.callback
beforeEach(function (ctx) {
ctx.ProjectRootDocManager.ensureRootDocumentIsSet(
ctx.project_id,
ctx.callback
)
})
it('should find the project with only the rootDoc_id field', function () {
this.ProjectGetter.getProject
.calledWith(this.project_id, { rootDoc_id: 1 })
it('should find the project with only the rootDoc_id field', function (ctx) {
ctx.ProjectGetter.getProject
.calledWith(ctx.project_id, { rootDoc_id: 1 })
.should.equal(true)
})
it('should update the project rootDoc_id', function () {
this.ProjectRootDocManager.setRootDocAutomatically
.calledWith(this.project_id)
it('should update the project rootDoc_id', function (ctx) {
ctx.ProjectRootDocManager.setRootDocAutomatically
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should call the callback', function () {
this.callback.called.should.equal(true)
it('should call the callback', function (ctx) {
ctx.callback.called.should.equal(true)
})
})
describe('when the project does not exist', function () {
beforeEach(function () {
this.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null)
this.ProjectRootDocManager.ensureRootDocumentIsSet(
this.project_id,
this.callback
beforeEach(function (ctx) {
ctx.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null)
ctx.ProjectRootDocManager.ensureRootDocumentIsSet(
ctx.project_id,
ctx.callback
)
})
it('should call the callback with an error', function () {
this.callback
it('should call the callback with an error', function (ctx) {
ctx.callback
.calledWith(
sinon.match
.instanceOf(Error)
@@ -564,125 +575,125 @@ describe('ProjectRootDocManager', function () {
})
describe('ensureRootDocumentIsValid', function () {
beforeEach(function () {
this.project = {}
this.ProjectGetter.getProject = sinon
beforeEach(function (ctx) {
ctx.project = {}
ctx.ProjectGetter.getProject = sinon
.stub()
.callsArgWith(2, null, this.project)
this.ProjectGetter.getProjectWithoutDocLines = sinon
.callsArgWith(2, null, ctx.project)
ctx.ProjectGetter.getProjectWithoutDocLines = sinon
.stub()
.callsArgWith(1, null, this.project)
this.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields()
this.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields()
this.ProjectRootDocManager.setRootDocAutomatically = sinon
.callsArgWith(1, null, ctx.project)
ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields()
ctx.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields()
ctx.ProjectRootDocManager.setRootDocAutomatically = sinon
.stub()
.callsArgWith(1, null)
})
describe('when the root doc is set', function () {
describe('when the root doc is valid', function () {
beforeEach(function () {
this.project.rootDoc_id = this.docId2
this.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon
beforeEach(function (ctx) {
ctx.project.rootDoc_id = ctx.docId2
ctx.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon
.stub()
.callsArgWith(2, null, this.docPaths[this.docId2])
this.ProjectRootDocManager.ensureRootDocumentIsValid(
this.project_id,
this.callback
.callsArgWith(2, null, ctx.docPaths[ctx.docId2])
ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
ctx.project_id,
ctx.callback
)
})
it('should find the project without doc lines', function () {
this.ProjectGetter.getProjectWithoutDocLines
.calledWith(this.project_id)
it('should find the project without doc lines', function (ctx) {
ctx.ProjectGetter.getProjectWithoutDocLines
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should not try to update the project rootDoc_id', function () {
this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
it('should not try to update the project rootDoc_id', function (ctx) {
ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
false
)
})
it('should call the callback', function () {
this.callback.called.should.equal(true)
it('should call the callback', function (ctx) {
ctx.callback.called.should.equal(true)
})
})
describe('when the root doc is not valid', function () {
beforeEach(function () {
this.project.rootDoc_id = new ObjectId()
this.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon
beforeEach(function (ctx) {
ctx.project.rootDoc_id = new ObjectId()
ctx.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon
.stub()
.callsArgWith(2, null, null)
this.ProjectRootDocManager.ensureRootDocumentIsValid(
this.project_id,
this.callback
ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
ctx.project_id,
ctx.callback
)
})
it('should find the project without doc lines', function () {
this.ProjectGetter.getProjectWithoutDocLines
.calledWith(this.project_id)
it('should find the project without doc lines', function (ctx) {
ctx.ProjectGetter.getProjectWithoutDocLines
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should unset the root doc', function () {
this.ProjectEntityUpdateHandler.unsetRootDoc
.calledWith(this.project_id)
it('should unset the root doc', function (ctx) {
ctx.ProjectEntityUpdateHandler.unsetRootDoc
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should try to find a new rootDoc', function () {
this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
it('should try to find a new rootDoc', function (ctx) {
ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
true
)
})
it('should call the callback', function () {
this.callback.called.should.equal(true)
it('should call the callback', function (ctx) {
ctx.callback.called.should.equal(true)
})
})
})
describe('when the root doc is not set', function () {
beforeEach(function () {
this.ProjectRootDocManager.ensureRootDocumentIsValid(
this.project_id,
this.callback
beforeEach(function (ctx) {
ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
ctx.project_id,
ctx.callback
)
})
it('should find the project without doc lines', function () {
this.ProjectGetter.getProjectWithoutDocLines
.calledWith(this.project_id)
it('should find the project without doc lines', function (ctx) {
ctx.ProjectGetter.getProjectWithoutDocLines
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should update the project rootDoc_id', function () {
this.ProjectRootDocManager.setRootDocAutomatically
.calledWith(this.project_id)
it('should update the project rootDoc_id', function (ctx) {
ctx.ProjectRootDocManager.setRootDocAutomatically
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should call the callback', function () {
this.callback.called.should.equal(true)
it('should call the callback', function (ctx) {
ctx.callback.called.should.equal(true)
})
})
describe('when the project does not exist', function () {
beforeEach(function () {
this.ProjectGetter.getProjectWithoutDocLines = sinon
beforeEach(function (ctx) {
ctx.ProjectGetter.getProjectWithoutDocLines = sinon
.stub()
.callsArgWith(1, null, null)
this.ProjectRootDocManager.ensureRootDocumentIsValid(
this.project_id,
this.callback
ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
ctx.project_id,
ctx.callback
)
})
it('should call the callback with an error', function () {
this.callback
it('should call the callback with an error', function (ctx) {
ctx.callback
.calledWith(
sinon.match
.instanceOf(Error)

View File

@@ -1,37 +1,34 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath = require('path').join(
__dirname,
import { vi, expect } from 'vitest'
import sinon from 'sinon'
const modulePath =
'../../../../app/src/Features/Subscription/LimitationsManager'
)
describe('LimitationsManager', function () {
beforeEach(function () {
this.user = {
_id: (this.userId = 'user-id'),
beforeEach(async function (ctx) {
ctx.user = {
_id: (ctx.userId = 'user-id'),
features: { collaborators: 1 },
}
this.project = {
_id: (this.projectId = 'project-id'),
owner_ref: this.userId,
ctx.project = {
_id: (ctx.projectId = 'project-id'),
owner_ref: ctx.userId,
}
this.ProjectGetter = {
ctx.ProjectGetter = {
promises: {
getProject: sinon.stub().callsFake(async (projectId, fields) => {
if (projectId === this.projectId) {
return this.project
if (projectId === ctx.projectId) {
return ctx.project
} else {
return null
}
}),
},
}
this.UserGetter = {
ctx.UserGetter = {
promises: {
getUser: sinon.stub().callsFake(async (userId, filter) => {
if (userId === this.userId) {
return this.user
if (userId === ctx.userId) {
return ctx.user
} else {
return null
}
@@ -39,7 +36,7 @@ describe('LimitationsManager', function () {
},
}
this.SubscriptionLocator = {
ctx.SubscriptionLocator = {
promises: {
getUsersSubscription: sinon.stub().resolves(),
getSubscription: sinon.stub().resolves(),
@@ -47,161 +44,194 @@ describe('LimitationsManager', function () {
},
}
this.CollaboratorsGetter = {
ctx.CollaboratorsGetter = {
promises: {
getInvitedEditCollaboratorCount: sinon.stub().resolves(0),
getMemberIdPrivilegeLevel: sinon.stub(),
},
}
this.CollaboratorsInviteGetter = {
ctx.CollaboratorsInviteGetter = {
promises: {
getEditInviteCount: sinon.stub().resolves(0),
},
}
this.LimitationsManager = SandboxedModule.require(modulePath, {
requires: {
'../Project/ProjectGetter': this.ProjectGetter,
'../User/UserGetter': this.UserGetter,
'./SubscriptionLocator': this.SubscriptionLocator,
'@overleaf/settings': (this.Settings = {}),
'../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
'../Collaborators/CollaboratorsInviteGetter':
this.CollaboratorsInviteGetter,
'./V1SubscriptionManager': this.V1SubscriptionManager,
},
})
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: ctx.ProjectGetter,
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
vi.doMock(
'../../../../app/src/Features/Subscription/SubscriptionLocator',
() => ({
default: ctx.SubscriptionLocator,
})
)
vi.doMock('@overleaf/settings', () => ({
default: (ctx.Settings = {}),
}))
vi.doMock(
'../../../../app/src/Features/Collaborators/CollaboratorsGetter',
() => ({
default: ctx.CollaboratorsGetter,
})
)
vi.doMock(
'../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter',
() => ({
default: ctx.CollaboratorsInviteGetter,
})
)
vi.doMock(
'../../../../app/src/Features/Subscription/V1SubscriptionManager',
() => ({
default: ctx.V1SubscriptionManager,
})
)
ctx.LimitationsManager = (await import(modulePath)).default
})
describe('allowedNumberOfCollaboratorsInProject', function () {
describe('when the project is owned by a user without a subscription', function () {
beforeEach(function () {
this.Settings.defaultFeatures = { collaborators: 23 }
this.project.owner_ref = this.userId
delete this.user.features
beforeEach(function (ctx) {
ctx.Settings.defaultFeatures = { collaborators: 23 }
ctx.project.owner_ref = ctx.userId
delete ctx.user.features
})
it('should return the default number', async function () {
it('should return the default number', async function (ctx) {
const result =
await this.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
this.projectId
await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
ctx.projectId
)
expect(result).to.equal(this.Settings.defaultFeatures.collaborators)
expect(result).to.equal(ctx.Settings.defaultFeatures.collaborators)
})
})
describe('when the project is owned by a user with a subscription', function () {
beforeEach(function () {
this.project.owner_ref = this.userId
this.user.features = { collaborators: 21 }
beforeEach(function (ctx) {
ctx.project.owner_ref = ctx.userId
ctx.user.features = { collaborators: 21 }
})
it('should return the number of collaborators the user is allowed', async function () {
it('should return the number of collaborators the user is allowed', async function (ctx) {
const result =
await this.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
this.projectId
await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
ctx.projectId
)
expect(result).to.equal(this.user.features.collaborators)
expect(result).to.equal(ctx.user.features.collaborators)
})
})
})
describe('allowedNumberOfCollaboratorsForUser', function () {
describe('when the user has no features', function () {
beforeEach(function () {
this.Settings.defaultFeatures = { collaborators: 23 }
delete this.user.features
beforeEach(function (ctx) {
ctx.Settings.defaultFeatures = { collaborators: 23 }
delete ctx.user.features
})
it('should return the default number', async function () {
it('should return the default number', async function (ctx) {
const result =
await this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
this.userId
await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
ctx.userId
)
expect(result).to.equal(this.Settings.defaultFeatures.collaborators)
expect(result).to.equal(ctx.Settings.defaultFeatures.collaborators)
})
})
describe('when the user has features', function () {
beforeEach(async function () {
this.user.features = { collaborators: 21 }
this.result =
await this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
this.userId
beforeEach(async function (ctx) {
ctx.user.features = { collaborators: 21 }
ctx.result =
await ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
ctx.userId
)
})
it('should return the number of collaborators the user is allowed', function () {
expect(this.result).to.equal(this.user.features.collaborators)
it('should return the number of collaborators the user is allowed', function (ctx) {
expect(ctx.result).to.equal(ctx.user.features.collaborators)
})
})
})
describe('canAcceptEditCollaboratorInvite', function () {
describe('when the project has fewer collaborators than allowed', function () {
beforeEach(function () {
this.current_number = 1
this.user.features.collaborators = 2
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
beforeEach(function (ctx) {
ctx.current_number = 1
ctx.user.features.collaborators = 2
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
.stub()
.resolves(ctx.current_number)
})
it('should return true', async function () {
it('should return true', async function (ctx) {
const result =
await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
this.projectId
await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
ctx.projectId
)
expect(result).to.be.true
})
})
describe('when accepting the invite would exceed the collaborator limit', function () {
beforeEach(function () {
this.current_number = 2
this.user.features.collaborators = 2
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
beforeEach(function (ctx) {
ctx.current_number = 2
ctx.user.features.collaborators = 2
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
.stub()
.resolves(ctx.current_number)
})
it('should return false', async function () {
it('should return false', async function (ctx) {
const result =
await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
this.projectId
await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
ctx.projectId
)
expect(result).to.be.false
})
})
describe('when the project has more collaborators than allowed', function () {
beforeEach(function () {
this.current_number = 3
this.user.features.collaborators = 2
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
beforeEach(function (ctx) {
ctx.current_number = 3
ctx.user.features.collaborators = 2
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
.stub()
.resolves(ctx.current_number)
})
it('should return false', async function () {
it('should return false', async function (ctx) {
const result =
await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
this.projectId
await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
ctx.projectId
)
expect(result).to.be.false
})
})
describe('when the project has infinite collaborators', function () {
beforeEach(function () {
this.current_number = 100
this.user.features.collaborators = -1
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
beforeEach(function (ctx) {
ctx.current_number = 100
ctx.user.features.collaborators = -1
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
.stub()
.resolves(ctx.current_number)
})
it('should return true', async function () {
it('should return true', async function (ctx) {
const result =
await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
this.projectId
await ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
ctx.projectId
)
expect(result).to.be.true
})
@@ -210,21 +240,22 @@ describe('LimitationsManager', function () {
describe('canAddXEditCollaborators', function () {
describe('when the project has fewer collaborators than allowed', function () {
beforeEach(function () {
this.current_number = 1
this.user.features.collaborators = 2
this.invite_count = 0
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
beforeEach(function (ctx) {
ctx.current_number = 1
ctx.user.features.collaborators = 2
ctx.invite_count = 0
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
.stub()
.resolves(this.invite_count)
.resolves(ctx.current_number)
ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(ctx.invite_count)
})
it('should return true', async function () {
it('should return true', async function (ctx) {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
await ctx.LimitationsManager.promises.canAddXEditCollaborators(
ctx.projectId,
1
)
expect(result).to.be.true
@@ -232,21 +263,22 @@ describe('LimitationsManager', function () {
})
describe('when the project has fewer collaborators and invites than allowed', function () {
beforeEach(function () {
this.current_number = 1
this.user.features.collaborators = 4
this.invite_count = 1
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
beforeEach(function (ctx) {
ctx.current_number = 1
ctx.user.features.collaborators = 4
ctx.invite_count = 1
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
.stub()
.resolves(this.invite_count)
.resolves(ctx.current_number)
ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(ctx.invite_count)
})
it('should return true', async function () {
it('should return true', async function (ctx) {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
await ctx.LimitationsManager.promises.canAddXEditCollaborators(
ctx.projectId,
1
)
expect(result).to.be.true
@@ -254,21 +286,22 @@ describe('LimitationsManager', function () {
})
describe('when the project has fewer collaborators than allowed but I want to add more than allowed', function () {
beforeEach(function () {
this.current_number = 1
this.user.features.collaborators = 2
this.invite_count = 0
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
beforeEach(function (ctx) {
ctx.current_number = 1
ctx.user.features.collaborators = 2
ctx.invite_count = 0
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
.stub()
.resolves(this.invite_count)
.resolves(ctx.current_number)
ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(ctx.invite_count)
})
it('should return false', async function () {
it('should return false', async function (ctx) {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
await ctx.LimitationsManager.promises.canAddXEditCollaborators(
ctx.projectId,
2
)
expect(result).to.be.false
@@ -276,21 +309,22 @@ describe('LimitationsManager', function () {
})
describe('when the project has more collaborators than allowed', function () {
beforeEach(function () {
this.current_number = 3
this.user.features.collaborators = 2
this.invite_count = 0
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
beforeEach(function (ctx) {
ctx.current_number = 3
ctx.user.features.collaborators = 2
ctx.invite_count = 0
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
.stub()
.resolves(this.invite_count)
.resolves(ctx.current_number)
ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(ctx.invite_count)
})
it('should return false', async function () {
it('should return false', async function (ctx) {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
await ctx.LimitationsManager.promises.canAddXEditCollaborators(
ctx.projectId,
1
)
expect(result).to.be.false
@@ -298,21 +332,22 @@ describe('LimitationsManager', function () {
})
describe('when the project has infinite collaborators', function () {
beforeEach(function () {
this.current_number = 100
this.user.features.collaborators = -1
this.invite_count = 0
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
beforeEach(function (ctx) {
ctx.current_number = 100
ctx.user.features.collaborators = -1
ctx.invite_count = 0
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
.stub()
.resolves(this.invite_count)
.resolves(ctx.current_number)
ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(ctx.invite_count)
})
it('should return true', async function () {
it('should return true', async function (ctx) {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
await ctx.LimitationsManager.promises.canAddXEditCollaborators(
ctx.projectId,
1
)
expect(result).to.be.true
@@ -320,21 +355,22 @@ describe('LimitationsManager', function () {
})
describe('when the project has more invites than allowed', function () {
beforeEach(function () {
this.current_number = 0
this.user.features.collaborators = 2
this.invite_count = 2
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
beforeEach(function (ctx) {
ctx.current_number = 0
ctx.user.features.collaborators = 2
ctx.invite_count = 2
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
.stub()
.resolves(this.invite_count)
.resolves(ctx.current_number)
ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(ctx.invite_count)
})
it('should return false', async function () {
it('should return false', async function (ctx) {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
await ctx.LimitationsManager.promises.canAddXEditCollaborators(
ctx.projectId,
1
)
expect(result).to.be.false
@@ -342,21 +378,22 @@ describe('LimitationsManager', function () {
})
describe('when the project has more invites and collaborators than allowed', function () {
beforeEach(function () {
this.current_number = 1
this.user.features.collaborators = 2
this.invite_count = 1
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
beforeEach(function (ctx) {
ctx.current_number = 1
ctx.user.features.collaborators = 2
ctx.invite_count = 1
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount = sinon
.stub()
.resolves(this.invite_count)
.resolves(ctx.current_number)
ctx.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(ctx.invite_count)
})
it('should return false', async function () {
it('should return false', async function (ctx) {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
await ctx.LimitationsManager.promises.canAddXEditCollaborators(
ctx.projectId,
1
)
expect(result).to.be.false
@@ -365,19 +402,19 @@ describe('LimitationsManager', function () {
})
describe('canChangeCollaboratorPrivilegeLevel', function () {
beforeEach(function () {
this.collaboratorId = 'collaborator-id'
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
beforeEach(function (ctx) {
ctx.collaboratorId = 'collaborator-id'
ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
'readOnly'
)
})
describe("when the limit hasn't been reached", function () {
it('accepts changing a viewer to an editor', async function () {
it('accepts changing a viewer to an editor', async function (ctx) {
const result =
await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
this.projectId,
this.collaboratorId,
await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
ctx.projectId,
ctx.collaboratorId,
'readAndWrite'
)
expect(result).to.be.true
@@ -385,30 +422,30 @@ describe('LimitationsManager', function () {
})
describe('when the limit has been reached', function () {
beforeEach(function () {
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount.resolves(
beforeEach(function (ctx) {
ctx.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount.resolves(
1
)
})
it('accepts changing a reviewer to an editor', async function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
it('accepts changing a reviewer to an editor', async function (ctx) {
ctx.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
'review'
)
const result =
await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
this.projectId,
this.collaboratorId,
await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
ctx.projectId,
ctx.collaboratorId,
'readAndWrite'
)
expect(result).to.be.true
})
it('rejects changing a viewer to a reviewer', async function () {
it('rejects changing a viewer to a reviewer', async function (ctx) {
const result =
await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
this.projectId,
this.collaboratorId,
await ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
ctx.projectId,
ctx.collaboratorId,
'review'
)
expect(result).to.be.false
@@ -417,25 +454,25 @@ describe('LimitationsManager', function () {
})
describe('userHasSubscription', function () {
beforeEach(function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
beforeEach(function (ctx) {
ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves()
})
it('should return true if the recurly token is set', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
it('should return true if the recurly token is set', async function (ctx) {
ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({
recurlySubscription_id: '1234',
})
const { hasSubscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
expect(hasSubscription).to.be.true
})
it('should return true if the paymentProvider field is set', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
it('should return true if the paymentProvider field is set', async function (ctx) {
ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({
paymentProvider: {
@@ -443,80 +480,80 @@ describe('LimitationsManager', function () {
},
})
const { hasSubscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
expect(hasSubscription).to.be.true
})
it('should return false if the recurly token is not set', async function () {
this.SubscriptionLocator.promises.getUsersSubscription.resolves({})
it('should return false if the recurly token is not set', async function (ctx) {
ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({})
const { hasSubscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
expect(hasSubscription).to.be.false
})
it('should return false if the subscription is undefined', async function () {
this.SubscriptionLocator.promises.getUsersSubscription.resolves()
it('should return false if the subscription is undefined', async function (ctx) {
ctx.SubscriptionLocator.promises.getUsersSubscription.resolves()
const { hasSubscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
expect(hasSubscription).to.be.false
})
it('should return the subscription', async function () {
it('should return the subscription', async function (ctx) {
const stubbedSubscription = { freeTrial: {}, token: '' }
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
stubbedSubscription
)
const { subscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
expect(subscription).to.deep.equal(stubbedSubscription)
})
describe('when user has a custom account', function () {
beforeEach(function () {
this.fakeSubscription = { customAccount: true }
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
this.fakeSubscription
beforeEach(function (ctx) {
ctx.fakeSubscription = { customAccount: true }
ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
ctx.fakeSubscription
)
})
it('should return true', async function () {
it('should return true', async function (ctx) {
const { hasSubscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
expect(hasSubscription).to.be.true
})
it('should return the subscription', async function () {
it('should return the subscription', async function (ctx) {
const { subscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
expect(subscription).to.deep.equal(this.fakeSubscription)
await ctx.LimitationsManager.promises.userHasSubscription(ctx.user)
expect(subscription).to.deep.equal(ctx.fakeSubscription)
})
})
})
describe('userIsMemberOfGroupSubscription', function () {
beforeEach(function () {
this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
beforeEach(function (ctx) {
ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon
.stub()
.resolves()
})
it('should return false if there are no groups subcriptions', async function () {
this.SubscriptionLocator.promises.getMemberSubscriptions.resolves([])
it('should return false if there are no groups subcriptions', async function (ctx) {
ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves([])
const { isMember } =
await this.LimitationsManager.promises.userIsMemberOfGroupSubscription(
this.user
await ctx.LimitationsManager.promises.userIsMemberOfGroupSubscription(
ctx.user
)
expect(isMember).to.be.false
})
it('should return true if there are no groups subcriptions', async function () {
it('should return true if there are no groups subcriptions', async function (ctx) {
const subscriptions = ['mock-subscription']
this.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
subscriptions
)
const { isMember, subscriptions: retSubscriptions } =
await this.LimitationsManager.promises.userIsMemberOfGroupSubscription(
this.user
await ctx.LimitationsManager.promises.userIsMemberOfGroupSubscription(
ctx.user
)
expect(isMember).to.be.true
expect(retSubscriptions).to.deep.equal(subscriptions)
@@ -524,52 +561,52 @@ describe('LimitationsManager', function () {
})
describe('hasPaidSubscription', function () {
beforeEach(function () {
this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
beforeEach(function (ctx) {
ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon
.stub()
.resolves([])
this.SubscriptionLocator.promises.getUsersSubscription = sinon
ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves(null)
})
it('should return true if userIsMemberOfGroupSubscription', async function () {
this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
it('should return true if userIsMemberOfGroupSubscription', async function (ctx) {
ctx.SubscriptionLocator.promises.getMemberSubscriptions = sinon
.stub()
.resolves([{ _id: '123' }])
const { hasPaidSubscription } =
await this.LimitationsManager.promises.hasPaidSubscription(this.user)
await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user)
expect(hasPaidSubscription).to.be.true
})
it('should return true if userHasSubscription', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
it('should return true if userHasSubscription', async function (ctx) {
ctx.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({ recurlySubscription_id: '123' })
const { hasPaidSubscription } =
await this.LimitationsManager.promises.hasPaidSubscription(this.user)
await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user)
expect(hasPaidSubscription).to.be.true
})
it('should return false if none are true', async function () {
it('should return false if none are true', async function (ctx) {
const { hasPaidSubscription } =
await this.LimitationsManager.promises.hasPaidSubscription(this.user)
await ctx.LimitationsManager.promises.hasPaidSubscription(ctx.user)
expect(hasPaidSubscription).to.be.false
})
it('should have userHasSubscriptionOrIsGroupMember alias', async function () {
it('should have userHasSubscriptionOrIsGroupMember alias', async function (ctx) {
const { hasPaidSubscription } =
await this.LimitationsManager.promises.userHasSubscriptionOrIsGroupMember(
this.user
await ctx.LimitationsManager.promises.userHasSubscriptionOrIsGroupMember(
ctx.user
)
expect(hasPaidSubscription).to.be.false
})
})
describe('hasGroupMembersLimitReached', function () {
beforeEach(function () {
this.subscriptionId = '12312'
this.subscription = {
beforeEach(function (ctx) {
ctx.subscriptionId = '12312'
ctx.subscription = {
membersLimit: 3,
member_ids: ['', ''],
teamInvites: [
@@ -578,37 +615,37 @@ describe('LimitationsManager', function () {
}
})
it('should return true if the limit is hit (including members and invites)', async function () {
this.SubscriptionLocator.promises.getSubscription.resolves(
this.subscription
it('should return true if the limit is hit (including members and invites)', async function (ctx) {
ctx.SubscriptionLocator.promises.getSubscription.resolves(
ctx.subscription
)
const { limitReached } =
await this.LimitationsManager.promises.hasGroupMembersLimitReached(
this.subscriptionId
await ctx.LimitationsManager.promises.hasGroupMembersLimitReached(
ctx.subscriptionId
)
expect(limitReached).to.be.true
})
it('should return false if the limit is not hit (including members and invites)', async function () {
this.subscription.membersLimit = 4
this.SubscriptionLocator.promises.getSubscription.resolves(
this.subscription
it('should return false if the limit is not hit (including members and invites)', async function (ctx) {
ctx.subscription.membersLimit = 4
ctx.SubscriptionLocator.promises.getSubscription.resolves(
ctx.subscription
)
const { limitReached } =
await this.LimitationsManager.promises.hasGroupMembersLimitReached(
this.subscriptionId
await ctx.LimitationsManager.promises.hasGroupMembersLimitReached(
ctx.subscriptionId
)
expect(limitReached).to.be.false
})
it('should return true if the limit has been exceded (including members and invites)', async function () {
this.subscription.membersLimit = 2
this.SubscriptionLocator.promises.getSubscription.resolves(
this.subscription
it('should return true if the limit has been exceded (including members and invites)', async function (ctx) {
ctx.subscription.membersLimit = 2
ctx.SubscriptionLocator.promises.getSubscription.resolves(
ctx.subscription
)
const { limitReached } =
await this.LimitationsManager.promises.hasGroupMembersLimitReached(
this.subscriptionId
await ctx.LimitationsManager.promises.hasGroupMembersLimitReached(
ctx.subscriptionId
)
expect(limitReached).to.be.true
})

View File

@@ -1,15 +1,20 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
const modulePath =
'../../../../app/src/Features/Subscription/TeamInvitesHandler'
const { ObjectId } = require('mongodb-legacy')
const Errors = require('../../../../app/src/Features/Errors/Errors')
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
const { ObjectId } = mongodb
describe('TeamInvitesHandler', function () {
beforeEach(function () {
this.manager = {
beforeEach(async function (ctx) {
ctx.manager = {
_id: '666666',
first_name: 'Daenerys',
last_name: 'Targaryen',
@@ -17,34 +22,34 @@ describe('TeamInvitesHandler', function () {
emails: [{ email: 'daenerys@example.com' }],
}
this.token = 'aaaaaaaaaaaaaaaaaaaaaa'
ctx.token = 'aaaaaaaaaaaaaaaaaaaaaa'
this.teamInvite = {
ctx.teamInvite = {
email: 'jorah@example.com',
token: this.token,
token: ctx.token,
}
// ensure teamInvite can be converted from Document to Object
this.teamInvite.toObject = () => this.teamInvite
ctx.teamInvite.toObject = () => ctx.teamInvite
this.subscription = {
ctx.subscription = {
id: '55153a8014829a865bbf700d',
_id: new ObjectId('55153a8014829a865bbf700d'),
recurlySubscription_id: '1a2b3c4d5e6f7g',
admin_id: this.manager._id,
admin_id: ctx.manager._id,
groupPlan: true,
member_ids: [],
teamInvites: [this.teamInvite],
teamInvites: [ctx.teamInvite],
save: sinon.stub().resolves(),
}
this.SubscriptionLocator = {
ctx.SubscriptionLocator = {
promises: {
getUsersSubscription: sinon.stub(),
getSubscription: sinon.stub().resolves(this.subscription),
getSubscription: sinon.stub().resolves(ctx.subscription),
},
}
this.UserGetter = {
ctx.UserGetter = {
promises: {
getUser: sinon.stub().resolves(),
getUserByAnyEmail: sinon.stub().resolves(),
@@ -52,55 +57,55 @@ describe('TeamInvitesHandler', function () {
},
}
this.SubscriptionUpdater = {
ctx.SubscriptionUpdater = {
promises: {
addUserToGroup: sinon.stub().resolves(),
deleteSubscription: sinon.stub().resolves(),
},
}
this.LimitationsManager = {
ctx.LimitationsManager = {
teamHasReachedMemberLimit: sinon.stub().returns(false),
}
this.Subscription = {
ctx.Subscription = {
findOne: sinon.stub().resolves(),
updateOne: sinon.stub().resolves(),
}
this.SSOConfig = {
ctx.SSOConfig = {
findById: sinon.stub().resolves(),
}
this.EmailHandler = {
ctx.EmailHandler = {
promises: {
sendEmail: sinon.stub().resolves(null),
},
}
this.newToken = 'bbbbbbbbb'
ctx.newToken = 'bbbbbbbbb'
this.crypto = {
ctx.crypto = {
randomBytes: () => {
return { toString: sinon.stub().returns(this.newToken) }
return { toString: sinon.stub().returns(ctx.newToken) }
},
}
this.UserGetter.promises.getUser
.withArgs(this.manager._id)
.resolves(this.manager)
this.UserGetter.promises.getUserByAnyEmail
.withArgs(this.manager.email)
.resolves(this.manager)
this.UserGetter.promises.getUserByMainEmail
.withArgs(this.manager.email)
.resolves(this.manager)
ctx.UserGetter.promises.getUser
.withArgs(ctx.manager._id)
.resolves(ctx.manager)
ctx.UserGetter.promises.getUserByAnyEmail
.withArgs(ctx.manager.email)
.resolves(ctx.manager)
ctx.UserGetter.promises.getUserByMainEmail
.withArgs(ctx.manager.email)
.resolves(ctx.manager)
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
this.subscription
ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
ctx.subscription
)
this.NotificationsBuilder = {
ctx.NotificationsBuilder = {
promises: {
groupInvitation: sinon.stub().returns({
create: sinon.stub().resolves(),
@@ -109,51 +114,105 @@ describe('TeamInvitesHandler', function () {
},
}
this.Subscription.findOne.resolves(this.subscription)
ctx.Subscription.findOne.resolves(ctx.subscription)
this.RecurlyClient = {
ctx.RecurlyClient = {
promises: {
terminateSubscriptionByUuid: sinon.stub().resolves(),
},
}
this.TeamInvitesHandler = SandboxedModule.require(modulePath, {
requires: {
'mongodb-legacy': { ObjectId },
crypto: this.crypto,
'@overleaf/settings': { siteUrl: 'http://example.com' },
'../../models/TeamInvite': { TeamInvite: (this.TeamInvite = {}) },
'../../models/Subscription': { Subscription: this.Subscription },
'../../models/SSOConfig': { SSOConfig: this.SSOConfig },
'../User/UserGetter': this.UserGetter,
'./SubscriptionLocator': this.SubscriptionLocator,
'./SubscriptionUpdater': this.SubscriptionUpdater,
'./LimitationsManager': this.LimitationsManager,
'../Email/EmailHandler': this.EmailHandler,
'../Notifications/NotificationsBuilder': this.NotificationsBuilder,
'../../infrastructure/Modules': (this.Modules = {
promises: { hooks: { fire: sinon.stub().resolves() } },
}),
'./RecurlyClient': this.RecurlyClient,
},
})
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
vi.doMock('crypto', () => ({
default: ctx.crypto,
}))
vi.doMock('@overleaf/settings', () => ({
default: { siteUrl: 'http://example.com' },
}))
vi.doMock('../../../../app/src/models/TeamInvite', () => ({
TeamInvite: (ctx.TeamInvite = {}),
}))
vi.doMock('../../../../app/src/models/Subscription', () => ({
Subscription: ctx.Subscription,
}))
vi.doMock('../../../../app/src/models/SSOConfig', () => ({
SSOConfig: ctx.SSOConfig,
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
vi.doMock(
'../../../../app/src/Features/Subscription/SubscriptionLocator',
() => ({
default: ctx.SubscriptionLocator,
})
)
vi.doMock(
'../../../../app/src/Features/Subscription/SubscriptionUpdater',
() => ({
default: ctx.SubscriptionUpdater,
})
)
vi.doMock(
'../../../../app/src/Features/Subscription/LimitationsManager',
() => ({
default: ctx.LimitationsManager,
})
)
vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({
default: ctx.EmailHandler,
}))
vi.doMock(
'../../../../app/src/Features/Notifications/NotificationsBuilder',
() => ({
default: ctx.NotificationsBuilder,
})
)
vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
default: (ctx.Modules = {
promises: { hooks: { fire: sinon.stub().resolves() } },
}),
}))
vi.doMock(
'../../../../app/src/Features/Subscription/RecurlyClient',
() => ({
default: ctx.RecurlyClient,
})
)
ctx.TeamInvitesHandler = (await import(modulePath)).default
})
describe('getInvite', function () {
it("returns the invite if there's one", async function () {
it("returns the invite if there's one", async function (ctx) {
const { invite, subscription } =
await this.TeamInvitesHandler.promises.getInvite(this.token)
await ctx.TeamInvitesHandler.promises.getInvite(ctx.token)
expect(invite).to.deep.eq(this.teamInvite)
expect(subscription).to.deep.eq(this.subscription)
expect(invite).to.deep.eq(ctx.teamInvite)
expect(subscription).to.deep.eq(ctx.subscription)
})
it("returns teamNotFound if there's none", async function () {
this.Subscription.findOne = sinon.stub().resolves(null)
it("returns teamNotFound if there's none", async function (ctx) {
ctx.Subscription.findOne = sinon.stub().resolves(null)
let error
try {
await this.TeamInvitesHandler.promises.getInvite(this.token)
await ctx.TeamInvitesHandler.promises.getInvite(ctx.token)
} catch (err) {
error = err
}
@@ -163,66 +222,66 @@ describe('TeamInvitesHandler', function () {
})
describe('createInvite', function () {
it('adds the team invite to the subscription', async function () {
const invite = await this.TeamInvitesHandler.promises.createInvite(
this.manager._id,
this.subscription,
it('adds the team invite to the subscription', async function (ctx) {
const invite = await ctx.TeamInvitesHandler.promises.createInvite(
ctx.manager._id,
ctx.subscription,
'John.Snow@example.com'
)
expect(invite.token).to.eq(this.newToken)
expect(invite.token).to.eq(ctx.newToken)
expect(invite.email).to.eq('john.snow@example.com')
expect(invite.inviterName).to.eq(
'Daenerys Targaryen (daenerys@example.com)'
)
expect(invite.invite).to.be.true
expect(this.subscription.teamInvites).to.deep.include(invite)
expect(ctx.subscription.teamInvites).to.deep.include(invite)
})
it('sends an email', async function () {
await this.TeamInvitesHandler.promises.createInvite(
this.manager._id,
this.subscription,
it('sends an email', async function (ctx) {
await ctx.TeamInvitesHandler.promises.createInvite(
ctx.manager._id,
ctx.subscription,
'John.Snow@example.com'
)
this.EmailHandler.promises.sendEmail
ctx.EmailHandler.promises.sendEmail
.calledWith(
'verifyEmailToJoinTeam',
sinon.match({
to: 'john.snow@example.com',
inviter: this.manager,
acceptInviteUrl: `http://example.com/subscription/invites/${this.newToken}/`,
inviter: ctx.manager,
acceptInviteUrl: `http://example.com/subscription/invites/${ctx.newToken}/`,
})
)
.should.equal(true)
})
it('refreshes the existing invite if the email has already been invited', async function () {
const originalInvite = Object.assign({}, this.teamInvite)
it('refreshes the existing invite if the email has already been invited', async function (ctx) {
const originalInvite = Object.assign({}, ctx.teamInvite)
const invite = await this.TeamInvitesHandler.promises.createInvite(
this.manager._id,
this.subscription,
const invite = await ctx.TeamInvitesHandler.promises.createInvite(
ctx.manager._id,
ctx.subscription,
originalInvite.email
)
expect(invite).to.exist
expect(this.subscription.teamInvites.length).to.eq(1)
expect(this.subscription.teamInvites).to.deep.include(invite)
expect(ctx.subscription.teamInvites.length).to.eq(1)
expect(ctx.subscription.teamInvites).to.deep.include(invite)
expect(invite.email).to.eq(originalInvite.email)
this.subscription.save.calledOnce.should.eq(true)
ctx.subscription.save.calledOnce.should.eq(true)
})
it('removes any legacy invite from the subscription', async function () {
await this.TeamInvitesHandler.promises.createInvite(
this.manager._id,
this.subscription,
it('removes any legacy invite from the subscription', async function (ctx) {
await ctx.TeamInvitesHandler.promises.createInvite(
ctx.manager._id,
ctx.subscription,
'John.Snow@example.com'
)
this.Subscription.updateOne
ctx.Subscription.updateOne
.calledWith(
{ _id: new ObjectId('55153a8014829a865bbf700d') },
{ $pull: { invited_emails: 'john.snow@example.com' } }
@@ -230,77 +289,77 @@ describe('TeamInvitesHandler', function () {
.should.eq(true)
})
it('add user to subscription if inviting self', async function () {
const invite = await this.TeamInvitesHandler.promises.createInvite(
this.manager._id,
this.subscription,
this.manager.email
it('add user to subscription if inviting self', async function (ctx) {
const invite = await ctx.TeamInvitesHandler.promises.createInvite(
ctx.manager._id,
ctx.subscription,
ctx.manager.email
)
sinon.assert.calledWith(
this.SubscriptionUpdater.promises.addUserToGroup,
this.subscription._id,
this.manager._id
ctx.SubscriptionUpdater.promises.addUserToGroup,
ctx.subscription._id,
ctx.manager._id
)
sinon.assert.notCalled(this.subscription.save)
sinon.assert.notCalled(ctx.subscription.save)
expect(invite.token).to.not.exist
expect(invite.email).to.eq(this.manager.email)
expect(invite.first_name).to.eq(this.manager.first_name)
expect(invite.last_name).to.eq(this.manager.last_name)
expect(invite.email).to.eq(ctx.manager.email)
expect(invite.first_name).to.eq(ctx.manager.first_name)
expect(invite.last_name).to.eq(ctx.manager.last_name)
expect(invite.invite).to.be.false
})
it('sends an SSO invite if SSO is enabled and inviting self', async function () {
this.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
this.SSOConfig.findById
.withArgs(this.subscription.ssoConfig)
it('sends an SSO invite if SSO is enabled and inviting self', async function (ctx) {
ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
ctx.SSOConfig.findById
.withArgs(ctx.subscription.ssoConfig)
.resolves({ enabled: true })
await this.TeamInvitesHandler.promises.createInvite(
this.manager._id,
this.subscription,
this.manager.email
await ctx.TeamInvitesHandler.promises.createInvite(
ctx.manager._id,
ctx.subscription,
ctx.manager.email
)
sinon.assert.calledWith(
this.Modules.promises.hooks.fire,
ctx.Modules.promises.hooks.fire,
'sendGroupSSOReminder',
this.manager._id,
this.subscription._id
ctx.manager._id,
ctx.subscription._id
)
})
it('does not send an SSO invite if SSO is disabled and inviting self', async function () {
this.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
this.SSOConfig.findById
.withArgs(this.subscription.ssoConfig)
it('does not send an SSO invite if SSO is disabled and inviting self', async function (ctx) {
ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
ctx.SSOConfig.findById
.withArgs(ctx.subscription.ssoConfig)
.resolves({ enabled: false })
await this.TeamInvitesHandler.promises.createInvite(
this.manager._id,
this.subscription,
this.manager.email
await ctx.TeamInvitesHandler.promises.createInvite(
ctx.manager._id,
ctx.subscription,
ctx.manager.email
)
sinon.assert.notCalled(this.Modules.promises.hooks.fire)
sinon.assert.notCalled(ctx.Modules.promises.hooks.fire)
})
it('sends a notification if inviting registered user', async function () {
it('sends a notification if inviting registered user', async function (ctx) {
const id = new ObjectId('6a6b3a8014829a865bbf700d')
const managedUsersEnabled = false
this.UserGetter.promises.getUserByMainEmail
ctx.UserGetter.promises.getUserByMainEmail
.withArgs('john.snow@example.com')
.resolves({
_id: id,
})
const invite = await this.TeamInvitesHandler.promises.createInvite(
this.manager._id,
this.subscription,
const invite = await ctx.TeamInvitesHandler.promises.createInvite(
ctx.manager._id,
ctx.subscription,
'John.Snow@example.com'
)
this.NotificationsBuilder.promises
ctx.NotificationsBuilder.promises
.groupInvitation(
id.toString(),
this.subscription._id,
ctx.subscription._id,
managedUsersEnabled
)
.create.calledWith(invite)
@@ -309,42 +368,42 @@ describe('TeamInvitesHandler', function () {
})
describe('importInvite', function () {
beforeEach(function () {
this.sentAt = new Date()
beforeEach(function (ctx) {
ctx.sentAt = new Date()
})
it('can imports an invite from v1', function () {
this.TeamInvitesHandler.importInvite(
this.subscription,
it('can imports an invite from v1', function (ctx) {
ctx.TeamInvitesHandler.importInvite(
ctx.subscription,
'A-Team',
'hannibal@a-team.org',
'secret',
this.sentAt,
ctx.sentAt,
error => {
expect(error).not.to.exist
this.subscription.save.calledOnce.should.eq(true)
ctx.subscription.save.calledOnce.should.eq(true)
const invite = this.subscription.teamInvites.find(
const invite = ctx.subscription.teamInvites.find(
i => i.email === 'hannibal@a-team.org'
)
expect(invite.token).to.eq('secret')
expect(invite.sentAt).to.eq(this.sentAt)
expect(invite.sentAt).to.eq(ctx.sentAt)
}
)
})
})
describe('acceptInvite', function () {
beforeEach(function () {
this.user = {
beforeEach(function (ctx) {
ctx.user = {
id: '123456789',
first_name: 'Tyrion',
last_name: 'Lannister',
email: 'tyrion@example.com',
}
this.user_subscription = {
ctx.user_subscription = {
id: '66264b9125930b976cc0811e',
_id: new ObjectId('66264b9125930b976cc0811e'),
groupPlan: false,
@@ -355,17 +414,17 @@ describe('TeamInvitesHandler', function () {
save: sinon.stub().resolves(),
}
this.ipAddress = '127.0.0.1'
ctx.ipAddress = '127.0.0.1'
this.UserGetter.promises.getUserByAnyEmail
.withArgs(this.user.email)
.resolves(this.user)
ctx.UserGetter.promises.getUserByAnyEmail
.withArgs(ctx.user.email)
.resolves(ctx.user)
this.SubscriptionLocator.promises.getUsersSubscription
.withArgs(this.user.id)
.resolves(this.user_subscription)
ctx.SubscriptionLocator.promises.getUsersSubscription
.withArgs(ctx.user.id)
.resolves(ctx.user_subscription)
this.subscription.teamInvites.push({
ctx.subscription.teamInvites.push({
email: 'john.snow@example.com',
token: 'dddddddd',
inviterName: 'Daenerys Targaryen (daenerys@example.com)',
@@ -373,24 +432,24 @@ describe('TeamInvitesHandler', function () {
})
describe('with standard group', function () {
it('adds the user to the team', async function () {
await this.TeamInvitesHandler.promises.acceptInvite(
it('adds the user to the team', async function (ctx) {
await ctx.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id,
this.ipAddress
ctx.user.id,
ctx.ipAddress
)
this.SubscriptionUpdater.promises.addUserToGroup
.calledWith(this.subscription._id, this.user.id)
ctx.SubscriptionUpdater.promises.addUserToGroup
.calledWith(ctx.subscription._id, ctx.user.id)
.should.eq(true)
})
it('removes the invite from the subscription', async function () {
await this.TeamInvitesHandler.promises.acceptInvite(
it('removes the invite from the subscription', async function (ctx) {
await ctx.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id,
this.ipAddress
ctx.user.id,
ctx.ipAddress
)
this.Subscription.updateOne
ctx.Subscription.updateOne
.calledWith(
{ _id: new ObjectId('55153a8014829a865bbf700d') },
{ $pull: { teamInvites: { email: 'john.snow@example.com' } } }
@@ -398,114 +457,114 @@ describe('TeamInvitesHandler', function () {
.should.eq(true)
})
it('removes dashboard notification after they accepted group invitation', async function () {
it('removes dashboard notification after they accepted group invitation', async function (ctx) {
const managedUsersEnabled = false
await this.TeamInvitesHandler.promises.acceptInvite(
await ctx.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id,
this.ipAddress
ctx.user.id,
ctx.ipAddress
)
sinon.assert.called(
this.NotificationsBuilder.promises.groupInvitation(
this.user.id,
this.subscription._id,
ctx.NotificationsBuilder.promises.groupInvitation(
ctx.user.id,
ctx.subscription._id,
managedUsersEnabled
).read
)
})
it('should not schedule an SSO invite reminder', async function () {
await this.TeamInvitesHandler.promises.acceptInvite(
it('should not schedule an SSO invite reminder', async function (ctx) {
await ctx.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id,
this.ipAddress
ctx.user.id,
ctx.ipAddress
)
sinon.assert.notCalled(this.Modules.promises.hooks.fire)
sinon.assert.notCalled(ctx.Modules.promises.hooks.fire)
})
})
describe('with managed group', function () {
it('should enroll the group member', async function () {
this.subscription.managedUsersEnabled = true
it('should enroll the group member', async function (ctx) {
ctx.subscription.managedUsersEnabled = true
await this.TeamInvitesHandler.promises.acceptInvite(
await ctx.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id,
this.ipAddress
ctx.user.id,
ctx.ipAddress
)
sinon.assert.calledWith(
this.SubscriptionUpdater.promises.deleteSubscription,
this.user_subscription,
{ id: this.user.id, ip: this.ipAddress }
ctx.SubscriptionUpdater.promises.deleteSubscription,
ctx.user_subscription,
{ id: ctx.user.id, ip: ctx.ipAddress }
)
sinon.assert.calledWith(
this.RecurlyClient.promises.terminateSubscriptionByUuid,
this.user_subscription.recurlySubscription_id
ctx.RecurlyClient.promises.terminateSubscriptionByUuid,
ctx.user_subscription.recurlySubscription_id
)
sinon.assert.calledWith(
this.Modules.promises.hooks.fire,
ctx.Modules.promises.hooks.fire,
'enrollInManagedSubscription',
this.user.id,
this.subscription
ctx.user.id,
ctx.subscription
)
})
it('should not delete the users subscription if that subscription is also the join target', async function () {
this.subscription.managedUsersEnabled = true
this.SubscriptionLocator.promises.getUsersSubscription
.withArgs(this.user.id)
.resolves(this.subscription)
it('should not delete the users subscription if that subscription is also the join target', async function (ctx) {
ctx.subscription.managedUsersEnabled = true
ctx.SubscriptionLocator.promises.getUsersSubscription
.withArgs(ctx.user.id)
.resolves(ctx.subscription)
await this.TeamInvitesHandler.promises.acceptInvite(
await ctx.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id,
this.ipAddress
ctx.user.id,
ctx.ipAddress
)
sinon.assert.notCalled(
this.SubscriptionUpdater.promises.deleteSubscription
ctx.SubscriptionUpdater.promises.deleteSubscription
)
})
})
describe('with group SSO enabled', function () {
it('should schedule an SSO invite reminder', async function () {
this.subscription.ssoConfig = 'ssoconfig1'
this.SSOConfig.findById
it('should schedule an SSO invite reminder', async function (ctx) {
ctx.subscription.ssoConfig = 'ssoconfig1'
ctx.SSOConfig.findById
.withArgs('ssoconfig1')
.resolves({ enabled: true })
await this.TeamInvitesHandler.promises.acceptInvite(
await ctx.TeamInvitesHandler.promises.acceptInvite(
'dddddddd',
this.user.id,
this.ipAddress
ctx.user.id,
ctx.ipAddress
)
sinon.assert.calledWith(
this.Modules.promises.hooks.fire,
ctx.Modules.promises.hooks.fire,
'scheduleGroupSSOReminder',
this.user.id,
this.subscription._id
ctx.user.id,
ctx.subscription._id
)
})
})
})
describe('revokeInvite', function () {
it('removes the team invite from the subscription', async function () {
await this.TeamInvitesHandler.promises.revokeInvite(
this.manager._id,
this.subscription,
it('removes the team invite from the subscription', async function (ctx) {
await ctx.TeamInvitesHandler.promises.revokeInvite(
ctx.manager._id,
ctx.subscription,
'jorah@example.com'
)
this.Subscription.updateOne
ctx.Subscription.updateOne
.calledWith(
{ _id: new ObjectId('55153a8014829a865bbf700d') },
{ $pull: { teamInvites: { email: 'jorah@example.com' } } }
)
.should.eq(true)
this.Subscription.updateOne
ctx.Subscription.updateOne
.calledWith(
{ _id: new ObjectId('55153a8014829a865bbf700d') },
{ $pull: { invited_emails: 'jorah@example.com' } }
@@ -513,7 +572,7 @@ describe('TeamInvitesHandler', function () {
.should.eq(true)
})
it('removes dashboard notification for pending group invitation', async function () {
it('removes dashboard notification for pending group invitation', async function (ctx) {
const managedUsersEnabled = false
const pendingUser = {
@@ -521,20 +580,20 @@ describe('TeamInvitesHandler', function () {
email: 'tyrion@example.com',
}
this.UserGetter.promises.getUserByAnyEmail
ctx.UserGetter.promises.getUserByAnyEmail
.withArgs(pendingUser.email)
.resolves(pendingUser)
await this.TeamInvitesHandler.promises.revokeInvite(
this.manager._id,
this.subscription,
await ctx.TeamInvitesHandler.promises.revokeInvite(
ctx.manager._id,
ctx.subscription,
pendingUser.email
)
sinon.assert.called(
this.NotificationsBuilder.promises.groupInvitation(
ctx.NotificationsBuilder.promises.groupInvitation(
pendingUser.id,
this.subscription._id,
ctx.subscription._id,
managedUsersEnabled
).read
)
@@ -542,47 +601,47 @@ describe('TeamInvitesHandler', function () {
})
describe('createTeamInvitesForLegacyInvitedEmail', function () {
beforeEach(function () {
this.subscription.invited_emails = [
beforeEach(function (ctx) {
ctx.subscription.invited_emails = [
'eddard@example.com',
'robert@example.com',
]
this.TeamInvitesHandler.createInvite = sinon.stub().resolves(null)
this.SubscriptionLocator.promises.getGroupsWithEmailInvite = sinon
ctx.TeamInvitesHandler.createInvite = sinon.stub().resolves(null)
ctx.SubscriptionLocator.promises.getGroupsWithEmailInvite = sinon
.stub()
.resolves([this.subscription])
.resolves([ctx.subscription])
})
it('sends an invitation email to addresses in the legacy invited_emails field', async function () {
it('sends an invitation email to addresses in the legacy invited_emails field', async function (ctx) {
const invites =
await this.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail(
await ctx.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail(
'eddard@example.com'
)
expect(invites.length).to.eq(1)
const [invite] = invites
expect(invite.token).to.eq(this.newToken)
expect(invite.token).to.eq(ctx.newToken)
expect(invite.email).to.eq('eddard@example.com')
expect(invite.inviterName).to.eq(
'Daenerys Targaryen (daenerys@example.com)'
)
expect(invite.invite).to.be.true
expect(this.subscription.teamInvites).to.deep.include(invite)
expect(ctx.subscription.teamInvites).to.deep.include(invite)
})
})
describe('validation', function () {
it("doesn't create an invite if the team limit has been reached", async function () {
this.LimitationsManager.teamHasReachedMemberLimit = sinon
it("doesn't create an invite if the team limit has been reached", async function (ctx) {
ctx.LimitationsManager.teamHasReachedMemberLimit = sinon
.stub()
.returns(true)
let error
try {
await this.TeamInvitesHandler.promises.createInvite(
this.manager._id,
this.subscription,
await ctx.TeamInvitesHandler.promises.createInvite(
ctx.manager._id,
ctx.subscription,
'John.Snow@example.com'
)
} catch (err) {
@@ -596,14 +655,14 @@ describe('TeamInvitesHandler', function () {
})
})
it("doesn't create an invite if the subscription is not in a group plan", async function () {
this.subscription.groupPlan = false
it("doesn't create an invite if the subscription is not in a group plan", async function (ctx) {
ctx.subscription.groupPlan = false
let error
try {
await this.TeamInvitesHandler.promises.createInvite(
this.manager._id,
this.subscription,
await ctx.TeamInvitesHandler.promises.createInvite(
ctx.manager._id,
ctx.subscription,
'John.Snow@example.com'
)
} catch (err) {
@@ -617,24 +676,24 @@ describe('TeamInvitesHandler', function () {
})
})
it("doesn't create an invite if the user is already part of the team", async function () {
it("doesn't create an invite if the user is already part of the team", async function (ctx) {
const member = {
id: '1a2b',
_id: '1a2b',
email: 'tyrion@example.com',
}
this.subscription.member_ids = [member.id]
this.UserGetter.promises.getUserByAnyEmail
ctx.subscription.member_ids = [member.id]
ctx.UserGetter.promises.getUserByAnyEmail
.withArgs(member.email)
.resolves(member)
let error
try {
await this.TeamInvitesHandler.promises.createInvite(
this.manager._id,
this.subscription,
await ctx.TeamInvitesHandler.promises.createInvite(
ctx.manager._id,
ctx.subscription,
'tyrion@example.com'
)
} catch (err) {

View File

@@ -1,236 +1,251 @@
/* eslint-disable
max-len,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { RequestFailedError } = require('@overleaf/fetch-utils')
const { ReadableString } = require('@overleaf/stream-utils')
import { beforeEach, describe, it, vi } from 'vitest'
import sinon from 'sinon'
import { RequestFailedError } from '@overleaf/fetch-utils'
import { ReadableString } from '@overleaf/stream-utils'
const modulePath = '../../../../app/src/Features/Templates/TemplatesManager'
describe('TemplatesManager', function () {
beforeEach(function () {
this.project_id = 'project-id'
this.brandVariationId = 'brand-variation-id'
this.compiler = 'pdflatex'
this.imageName = 'TL2017'
this.mainFile = 'main.tex'
this.templateId = 'template-id'
this.templateName = 'template name'
this.templateVersionId = 'template-version-id'
this.user_id = 'user-id'
this.dumpPath = `${this.dumpFolder}/${this.uuid}`
this.callback = sinon.stub()
this.pipeline = sinon.stub().callsFake(async (stream, res) => {
beforeEach(async function (ctx) {
ctx.project_id = 'project-id'
ctx.brandVariationId = 'brand-variation-id'
ctx.compiler = 'pdflatex'
ctx.imageName = 'TL2017'
ctx.mainFile = 'main.tex'
ctx.templateId = 'template-id'
ctx.templateName = 'template name'
ctx.templateVersionId = 'template-version-id'
ctx.user_id = 'user-id'
ctx.dumpFolder = 'dump/path'
ctx.uuid = '1234'
ctx.dumpPath = `${ctx.dumpFolder}/${ctx.uuid}`
ctx.callback = sinon.stub()
ctx.pipeline = sinon.stub().callsFake(async (stream, res) => {
if (res.callback) res.callback()
})
this.request = sinon.stub().returns({
ctx.request = sinon.stub().returns({
pipe() {},
on() {},
response: {
statusCode: 200,
},
})
this.fs = {
ctx.fs = {
promises: { unlink: sinon.stub() },
unlink: sinon.stub(),
createWriteStream: sinon.stub().returns({ on: sinon.stub().yields() }),
}
this.ProjectUploadManager = {
ctx.ProjectUploadManager = {
promises: {
createProjectFromZipArchiveWithName: sinon
.stub()
.resolves({ _id: this.project_id }),
.resolves({ _id: ctx.project_id }),
},
}
this.dumpFolder = 'dump/path'
this.ProjectOptionsHandler = {
ctx.ProjectOptionsHandler = {
promises: {
setCompiler: sinon.stub().resolves(),
setImageName: sinon.stub().resolves(),
setBrandVariationId: sinon.stub().resolves(),
},
}
this.uuid = '1234'
this.ProjectRootDocManager = {
ctx.ProjectRootDocManager = {
promises: {
setRootDocFromName: sinon.stub().resolves(),
},
}
this.ProjectDetailsHandler = {
ctx.ProjectDetailsHandler = {
getProjectDescription: sinon.stub(),
fixProjectName: sinon.stub().returns(this.templateName),
fixProjectName: sinon.stub().returns(ctx.templateName),
}
this.Project = { updateOne: sinon.stub().resolves() }
this.mockStream = new ReadableString('{}')
this.mockResponse = {
ctx.Project = { updateOne: sinon.stub().resolves() }
ctx.mockStream = new ReadableString('{}')
ctx.mockResponse = {
status: 200,
headers: new Headers({
'Content-Length': '2',
'Content-Type': 'application/json',
}),
}
this.FetchUtils = {
ctx.FetchUtils = {
fetchJson: sinon.stub(),
fetchStreamWithResponse: sinon.stub().resolves({
stream: this.mockStream,
response: this.mockResponse,
stream: ctx.mockStream,
response: ctx.mockResponse,
}),
RequestFailedError,
}
this.TemplatesManager = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/fetch-utils': this.FetchUtils,
'../Uploads/ProjectUploadManager': this.ProjectUploadManager,
'../Project/ProjectOptionsHandler': this.ProjectOptionsHandler,
'../Project/ProjectRootDocManager': this.ProjectRootDocManager,
'../Project/ProjectDetailsHandler': this.ProjectDetailsHandler,
'../Authentication/SessionManager': (this.SessionManager = {
getLoggedInUserId: sinon.stub(),
}),
'@overleaf/settings': {
path: {
dumpFolder: this.dumpFolder,
},
siteUrl: (this.siteUrl = 'http://127.0.0.1:3000'),
apis: {
v1: {
url: (this.v1Url = 'http://overleaf.com'),
user: 'overleaf',
pass: 'password',
timeout: 10,
},
},
overleaf: {
host: this.v1Url,
vi.doMock('@overleaf/fetch-utils', () => ctx.FetchUtils)
vi.doMock(
'../../../../app/src/Features/Uploads/ProjectUploadManager',
() => ({ default: ctx.ProjectUploadManager })
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectOptionsHandler',
() => ({ default: ctx.ProjectOptionsHandler })
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectRootDocManager',
() => ({ default: ctx.ProjectRootDocManager })
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectDetailsHandler',
() => ({ default: ctx.ProjectDetailsHandler })
)
ctx.SessionManager = {
getLoggedInUserId: sinon.stub(),
}
vi.doMock(
'../../../../app/src/Features/Authentication/SessionManager',
() => ({ default: ctx.SessionManager })
)
vi.doMock('@overleaf/settings', () => ({
default: {
path: {
dumpFolder: ctx.dumpFolder,
},
siteUrl: (ctx.siteUrl = 'http://127.0.0.1:3000'),
apis: {
v1: {
url: (ctx.v1Url = 'http://overleaf.com'),
user: 'overleaf',
pass: 'password',
timeout: 10,
},
},
crypto: {
randomUUID: () => this.uuid,
},
request: this.request,
fs: this.fs,
'../../models/Project': { Project: this.Project },
'stream/promises': { pipeline: this.pipeline },
'../Compile/ClsiCacheManager': {
prepareClsiCache: sinon.stub().rejects(new Error('ignore this')),
overleaf: {
host: ctx.v1Url,
},
},
}).promises
return (this.zipUrl =
'%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex')
}))
vi.doMock('node:crypto', () => ({
default: {
randomUUID: () => ctx.uuid,
},
}))
vi.doMock('node:fs', () => ({ default: ctx.fs }))
vi.doMock('request', () => ({ default: ctx.request }))
vi.doMock('../../../../app/src/models/Project', () => ({
Project: ctx.Project,
}))
vi.doMock('node:stream/promises', () => ({ pipeline: ctx.pipeline }))
vi.doMock('../../../../app/src/Features/Compile/ClsiCacheManager', () => ({
default: {
prepareClsiCache: sinon.stub().rejects(new Error('ignore this')),
},
}))
ctx.TemplatesManager = (await import(modulePath)).default.promises
ctx.zipUrl =
'%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex'
})
describe('createProjectFromV1Template', function () {
describe('when all options passed', function () {
beforeEach(function () {
return this.TemplatesManager.createProjectFromV1Template(
this.brandVariationId,
this.compiler,
this.mainFile,
this.templateId,
this.templateName,
this.templateVersionId,
this.user_id,
this.imageName
beforeEach(async function (ctx) {
await ctx.TemplatesManager.createProjectFromV1Template(
ctx.brandVariationId,
ctx.compiler,
ctx.mainFile,
ctx.templateId,
ctx.templateName,
ctx.templateVersionId,
ctx.user_id,
ctx.imageName
)
})
it('should fetch zip from v1 based on template id', function () {
return this.FetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
`${this.v1Url}/api/v1/overleaf/templates/${this.templateVersionId}`
it('should fetch zip from v1 based on template id', function (ctx) {
ctx.FetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
`${ctx.v1Url}/api/v1/overleaf/templates/${ctx.templateVersionId}`
)
})
it('should save temporary file', function () {
return this.fs.createWriteStream.should.have.been.calledWith(
this.dumpPath
)
it('should save temporary file', function (ctx) {
ctx.fs.createWriteStream.should.have.been.calledWith(ctx.dumpPath)
})
it('should create project', function () {
return this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName.should.have.been.calledWithMatch(
this.user_id,
this.templateName,
this.dumpPath,
it('should create project', function (ctx) {
ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName.should.have.been.calledWithMatch(
ctx.user_id,
ctx.templateName,
ctx.dumpPath,
{
fromV1TemplateId: this.templateId,
fromV1TemplateVersionId: this.templateVersionId,
fromV1TemplateId: ctx.templateId,
fromV1TemplateVersionId: ctx.templateVersionId,
}
)
})
it('should unlink file', function () {
return this.fs.promises.unlink.should.have.been.calledWith(
this.dumpPath
it('should unlink file', function (ctx) {
ctx.fs.promises.unlink.should.have.been.calledWith(ctx.dumpPath)
})
it('should set project options when passed', function (ctx) {
ctx.ProjectOptionsHandler.promises.setCompiler.should.have.been.calledWithMatch(
ctx.project_id,
ctx.compiler
)
ctx.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch(
ctx.project_id,
ctx.imageName
)
ctx.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWithMatch(
ctx.project_id,
ctx.mainFile
)
ctx.ProjectOptionsHandler.promises.setBrandVariationId.should.have.been.calledWithMatch(
ctx.project_id,
ctx.brandVariationId
)
})
it('should set project options when passed', function () {
this.ProjectOptionsHandler.promises.setCompiler.should.have.been.calledWithMatch(
this.project_id,
this.compiler
)
this.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch(
this.project_id,
this.imageName
)
this.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWithMatch(
this.project_id,
this.mainFile
)
return this.ProjectOptionsHandler.promises.setBrandVariationId.should.have.been.calledWithMatch(
this.project_id,
this.brandVariationId
)
})
it('should update project', function () {
return this.Project.updateOne.should.have.been.calledWithMatch(
{ _id: this.project_id },
it('should update project', function (ctx) {
ctx.Project.updateOne.should.have.been.calledWithMatch(
{ _id: ctx.project_id },
{
fromV1TemplateId: this.templateId,
fromV1TemplateVersionId: this.templateVersionId,
fromV1TemplateId: ctx.templateId,
fromV1TemplateVersionId: ctx.templateVersionId,
}
)
})
})
describe('when some options not set', function () {
beforeEach(function () {
return this.TemplatesManager.createProjectFromV1Template(
beforeEach(async function (ctx) {
await ctx.TemplatesManager.createProjectFromV1Template(
null,
null,
null,
this.templateId,
this.templateName,
this.templateVersionId,
this.user_id,
ctx.templateId,
ctx.templateName,
ctx.templateVersionId,
ctx.user_id,
null
)
})
it('should not set missing project options', function () {
this.ProjectOptionsHandler.promises.setCompiler.called.should.equal(
it('should not set missing project options', function (ctx) {
ctx.ProjectOptionsHandler.promises.setCompiler.called.should.equal(
false
)
this.ProjectRootDocManager.promises.setRootDocFromName.called.should.equal(
ctx.ProjectRootDocManager.promises.setRootDocFromName.called.should.equal(
false
)
this.ProjectOptionsHandler.promises.setBrandVariationId.called.should.equal(
ctx.ProjectOptionsHandler.promises.setBrandVariationId.called.should.equal(
false
)
return this.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch(
this.project_id,
ctx.ProjectOptionsHandler.promises.setImageName.should.have.been.calledWithMatch(
ctx.project_id,
'wl_texlive:2018.1'
)
})

View File

@@ -1,176 +1,194 @@
const { expect } = require('chai')
const sinon = require('sinon')
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb-legacy')
const { Project } = require('../helpers/models/Project')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'
import indirectlyImportModels from '../helpers/indirectlyImportModels.js'
const { Project } = indirectlyImportModels(['Project'])
const { ObjectId } = mongodb
const MODULE_PATH =
'../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher'
describe('TpdsProjectFlusher', function () {
beforeEach(function () {
this.project = { _id: new ObjectId(), overleaf: { history: { id: 42 } } }
this.folder = { _id: new ObjectId() }
this.docs = {
beforeEach(async function (ctx) {
ctx.project = { _id: new ObjectId(), overleaf: { history: { id: 42 } } }
ctx.folder = { _id: new ObjectId() }
ctx.docs = {
'/doc/one': {
_id: 'mock-doc-1',
lines: ['one'],
rev: 5,
folder: this.folder,
folder: ctx.folder,
},
'/doc/two': {
_id: 'mock-doc-2',
lines: ['two'],
rev: 6,
folder: this.folder,
folder: ctx.folder,
},
}
this.files = {
ctx.files = {
'/file/one': {
_id: 'mock-file-1',
rev: 7,
folder: this.folder,
folder: ctx.folder,
hash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
},
'/file/two': {
_id: 'mock-file-2',
rev: 8,
folder: this.folder,
folder: ctx.folder,
hash: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
},
}
this.DocumentUpdaterHandler = {
ctx.DocumentUpdaterHandler = {
promises: {
flushProjectToMongo: sinon.stub().resolves(),
},
}
this.ProjectGetter = {
ctx.ProjectGetter = {
promises: {
getProject: sinon.stub().resolves(this.project),
getProject: sinon.stub().resolves(ctx.project),
},
}
this.ProjectEntityHandler = {
ctx.ProjectEntityHandler = {
promises: {
getAllDocs: sinon.stub().withArgs(this.project._id).resolves(this.docs),
getAllFiles: sinon
.stub()
.withArgs(this.project._id)
.resolves(this.files),
getAllDocs: sinon.stub().withArgs(ctx.project._id).resolves(ctx.docs),
getAllFiles: sinon.stub().withArgs(ctx.project._id).resolves(ctx.files),
},
}
this.TpdsUpdateSender = {
ctx.TpdsUpdateSender = {
promises: {
addDoc: sinon.stub().resolves(),
addFile: sinon.stub().resolves(),
},
}
this.ProjectMock = sinon.mock(Project)
ctx.ProjectMock = sinon.mock(Project)
this.TpdsProjectFlusher = SandboxedModule.require(MODULE_PATH, {
requires: {
'../DocumentUpdater/DocumentUpdaterHandler':
this.DocumentUpdaterHandler,
'../Project/ProjectGetter': this.ProjectGetter,
'../Project/ProjectEntityHandler': this.ProjectEntityHandler,
'../../models/Project': { Project },
'./TpdsUpdateSender': this.TpdsUpdateSender,
},
})
vi.doMock(
'../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
() => ({
default: ctx.DocumentUpdaterHandler,
})
)
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: ctx.ProjectGetter,
}))
vi.doMock(
'../../../../app/src/Features/Project/ProjectEntityHandler',
() => ({
default: ctx.ProjectEntityHandler,
})
)
vi.doMock('../../../../app/src/models/Project', () => ({
Project,
}))
vi.doMock(
'../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender',
() => ({
default: ctx.TpdsUpdateSender,
})
)
ctx.TpdsProjectFlusher = (await import(MODULE_PATH)).default
})
afterEach(function () {
this.ProjectMock.restore()
afterEach(function (ctx) {
ctx.ProjectMock.restore()
})
describe('flushProjectToTpds', function () {
describe('usually', function () {
beforeEach(async function () {
await this.TpdsProjectFlusher.promises.flushProjectToTpds(
this.project._id
beforeEach(async function (ctx) {
await ctx.TpdsProjectFlusher.promises.flushProjectToTpds(
ctx.project._id
)
})
it('should flush the project from the doc updater', function () {
it('should flush the project from the doc updater', function (ctx) {
expect(
this.DocumentUpdaterHandler.promises.flushProjectToMongo
).to.have.been.calledWith(this.project._id)
ctx.DocumentUpdaterHandler.promises.flushProjectToMongo
).to.have.been.calledWith(ctx.project._id)
})
it('should flush each doc to the TPDS', function () {
for (const [path, doc] of Object.entries(this.docs)) {
expect(this.TpdsUpdateSender.promises.addDoc).to.have.been.calledWith(
{
projectId: this.project._id,
docId: doc._id,
projectName: this.project.name,
rev: doc.rev,
path,
folderId: this.folder._id,
}
)
it('should flush each doc to the TPDS', function (ctx) {
for (const [path, doc] of Object.entries(ctx.docs)) {
expect(ctx.TpdsUpdateSender.promises.addDoc).to.have.been.calledWith({
projectId: ctx.project._id,
docId: doc._id,
projectName: ctx.project.name,
rev: doc.rev,
path,
folderId: ctx.folder._id,
})
}
})
it('should flush each file to the TPDS', function () {
for (const [path, file] of Object.entries(this.files)) {
expect(
this.TpdsUpdateSender.promises.addFile
).to.have.been.calledWith({
projectId: this.project._id,
historyId: this.project.overleaf.history.id,
fileId: file._id,
hash: file.hash,
projectName: this.project.name,
rev: file.rev,
path,
folderId: this.folder._id,
})
it('should flush each file to the TPDS', function (ctx) {
for (const [path, file] of Object.entries(ctx.files)) {
expect(ctx.TpdsUpdateSender.promises.addFile).to.have.been.calledWith(
{
projectId: ctx.project._id,
historyId: ctx.project.overleaf.history.id,
fileId: file._id,
hash: file.hash,
projectName: ctx.project.name,
rev: file.rev,
path,
folderId: ctx.folder._id,
}
)
}
})
})
describe('when a TPDS flush is pending', function () {
beforeEach(async function () {
this.project.deferredTpdsFlushCounter = 2
this.ProjectMock.expects('updateOne')
beforeEach(async function (ctx) {
ctx.project.deferredTpdsFlushCounter = 2
ctx.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.project._id,
_id: ctx.project._id,
deferredTpdsFlushCounter: { $lte: 2 },
},
{ $set: { deferredTpdsFlushCounter: 0 } }
)
.chain('exec')
.resolves()
await this.TpdsProjectFlusher.promises.flushProjectToTpds(
this.project._id
await ctx.TpdsProjectFlusher.promises.flushProjectToTpds(
ctx.project._id
)
})
it('resets the deferred flush counter', function () {
this.ProjectMock.verify()
it('resets the deferred flush counter', function (ctx) {
ctx.ProjectMock.verify()
})
})
})
describe('deferProjectFlushToTpds', function () {
beforeEach(async function () {
this.ProjectMock.expects('updateOne')
beforeEach(async function (ctx) {
ctx.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.project._id,
_id: ctx.project._id,
},
{ $inc: { deferredTpdsFlushCounter: 1 } }
)
.chain('exec')
.resolves()
await this.TpdsProjectFlusher.promises.deferProjectFlushToTpds(
this.project._id
await ctx.TpdsProjectFlusher.promises.deferProjectFlushToTpds(
ctx.project._id
)
})
it('increments the deferred flush counter', function () {
this.ProjectMock.verify()
it('increments the deferred flush counter', function (ctx) {
ctx.ProjectMock.verify()
})
})
@@ -178,24 +196,24 @@ describe('TpdsProjectFlusher', function () {
let cases = [0, undefined]
cases.forEach(counterValue => {
describe(`when the deferred flush counter is ${counterValue}`, function () {
beforeEach(async function () {
this.project.deferredTpdsFlushCounter = counterValue
await this.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(
this.project._id
beforeEach(async function (ctx) {
ctx.project.deferredTpdsFlushCounter = counterValue
await ctx.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(
ctx.project._id
)
})
it("doesn't flush the project from the doc updater", function () {
expect(this.DocumentUpdaterHandler.promises.flushProjectToMongo).not
.to.have.been.called
it("doesn't flush the project from the doc updater", function (ctx) {
expect(ctx.DocumentUpdaterHandler.promises.flushProjectToMongo).not.to
.have.been.called
})
it("doesn't flush any doc", function () {
expect(this.TpdsUpdateSender.promises.addDoc).not.to.have.been.called
it("doesn't flush any doc", function (ctx) {
expect(ctx.TpdsUpdateSender.promises.addDoc).not.to.have.been.called
})
it("doesn't flush any file", function () {
expect(this.TpdsUpdateSender.promises.addFile).not.to.have.been.called
it("doesn't flush any file", function (ctx) {
expect(ctx.TpdsUpdateSender.promises.addFile).not.to.have.been.called
})
})
})
@@ -203,63 +221,63 @@ describe('TpdsProjectFlusher', function () {
cases = [1, 2]
cases.forEach(counterValue => {
describe(`when the deferred flush counter is ${counterValue}`, function () {
beforeEach(async function () {
this.project.deferredTpdsFlushCounter = counterValue
this.ProjectMock.expects('updateOne')
beforeEach(async function (ctx) {
ctx.project.deferredTpdsFlushCounter = counterValue
ctx.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.project._id,
_id: ctx.project._id,
deferredTpdsFlushCounter: { $lte: counterValue },
},
{ $set: { deferredTpdsFlushCounter: 0 } }
)
.chain('exec')
.resolves()
await this.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(
this.project._id
await ctx.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(
ctx.project._id
)
})
it('flushes the project from the doc updater', function () {
it('flushes the project from the doc updater', function (ctx) {
expect(
this.DocumentUpdaterHandler.promises.flushProjectToMongo
).to.have.been.calledWith(this.project._id)
ctx.DocumentUpdaterHandler.promises.flushProjectToMongo
).to.have.been.calledWith(ctx.project._id)
})
it('flushes each doc to the TPDS', function () {
for (const [path, doc] of Object.entries(this.docs)) {
it('flushes each doc to the TPDS', function (ctx) {
for (const [path, doc] of Object.entries(ctx.docs)) {
expect(
this.TpdsUpdateSender.promises.addDoc
ctx.TpdsUpdateSender.promises.addDoc
).to.have.been.calledWith({
projectId: this.project._id,
projectId: ctx.project._id,
docId: doc._id,
projectName: this.project.name,
projectName: ctx.project.name,
rev: doc.rev,
path,
folderId: this.folder._id,
folderId: ctx.folder._id,
})
}
})
it('flushes each file to the TPDS', function () {
for (const [path, file] of Object.entries(this.files)) {
it('flushes each file to the TPDS', function (ctx) {
for (const [path, file] of Object.entries(ctx.files)) {
expect(
this.TpdsUpdateSender.promises.addFile
ctx.TpdsUpdateSender.promises.addFile
).to.have.been.calledWith({
projectId: this.project._id,
historyId: this.project.overleaf.history.id,
projectId: ctx.project._id,
historyId: ctx.project.overleaf.history.id,
fileId: file._id,
hash: file.hash,
projectName: this.project.name,
projectName: ctx.project.name,
rev: file.rev,
path,
folderId: this.folder._id,
folderId: ctx.folder._id,
})
}
})
it('resets the deferred flush counter', function () {
this.ProjectMock.verify()
it('resets the deferred flush counter', function (ctx) {
ctx.ProjectMock.verify()
})
})
})

View File

@@ -1,12 +1,13 @@
const { ObjectId } = require('mongodb-legacy')
const SandboxedModule = require('sandboxed-module')
const path = require('path')
const sinon = require('sinon')
const { expect } = require('chai')
import { beforeEach, describe, expect, it, vi } from 'vitest'
import mongodb from 'mongodb-legacy'
import path from 'path'
import sinon from 'sinon'
const { ObjectId } = mongodb
const modulePath = path.join(
__dirname,
'../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js'
import.meta.dirname,
'../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.mjs'
)
const projectId = 'project_id_here'
@@ -21,29 +22,29 @@ const filestoreUrl = 'filestore.overleaf.com'
const projectHistoryUrl = 'http://project-history:3054'
describe('TpdsUpdateSender', function () {
beforeEach(function () {
this.fakeUser = {
beforeEach(async function (ctx) {
ctx.fakeUser = {
_id: '12390i',
}
this.memberIds = [userId, collaberatorRef, readOnlyRef]
this.enqueueUrl = new URL(
ctx.memberIds = [userId, collaberatorRef, readOnlyRef]
ctx.enqueueUrl = new URL(
'http://tpdsworker/enqueue/web_to_tpds_http_requests'
)
this.CollaboratorsGetter = {
ctx.CollaboratorsGetter = {
promises: {
getInvitedMemberIds: sinon.stub().resolves(this.memberIds),
getInvitedMemberIds: sinon.stub().resolves(ctx.memberIds),
},
}
this.docstoreUrl = 'docstore.overleaf.env'
this.response = {
ctx.docstoreUrl = 'docstore.overleaf.env'
ctx.response = {
ok: true,
json: sinon.stub(),
}
this.FetchUtils = {
ctx.FetchUtils = {
fetchNothing: sinon.stub().resolves(),
}
this.settings = {
ctx.settings = {
siteUrl,
apis: {
thirdPartyDataStore: { url: thirdPartyDataStoreApiUrl },
@@ -51,7 +52,7 @@ describe('TpdsUpdateSender', function () {
url: filestoreUrl,
},
docstore: {
pubUrl: this.docstoreUrl,
pubUrl: ctx.docstoreUrl,
},
project_history: {
url: projectHistoryUrl,
@@ -62,46 +63,63 @@ describe('TpdsUpdateSender', function () {
getUsers
.withArgs({
_id: {
$in: this.memberIds,
$in: ctx.memberIds,
},
'dropbox.access_token.uid': { $ne: null },
})
.resolves(
this.memberIds.map(userId => {
ctx.memberIds.map(userId => {
return { _id: userId }
})
)
this.UserGetter = {
ctx.UserGetter = {
promises: { getUsers },
}
this.TpdsUpdateSender = SandboxedModule.require(modulePath, {
requires: {
'mongodb-legacy': { ObjectId },
'@overleaf/settings': this.settings,
'@overleaf/fetch-utils': this.FetchUtils,
'../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
'../User/UserGetter.js': this.UserGetter,
'@overleaf/metrics': {
inc() {},
},
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
vi.doMock('@overleaf/settings', () => ({
default: ctx.settings,
}))
vi.doMock('@overleaf/fetch-utils', () => ctx.FetchUtils)
vi.doMock(
'../../../../app/src/Features/Collaborators/CollaboratorsGetter',
() => ({
default: ctx.CollaboratorsGetter,
})
)
vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({
default: ctx.UserGetter,
}))
vi.doMock('@overleaf/metrics', () => ({
default: {
inc() {},
},
})
}))
ctx.TpdsUpdateSender = (await import(modulePath)).default
})
describe('enqueue', function () {
it('should not call request if there is no tpdsworker url', async function () {
await this.TpdsUpdateSender.promises.enqueue(null, null, null)
this.FetchUtils.fetchNothing.should.not.have.been.called
it('should not call request if there is no tpdsworker url', async function (ctx) {
await ctx.TpdsUpdateSender.promises.enqueue(null, null, null)
ctx.FetchUtils.fetchNothing.should.not.have.been.called
})
it('should post the message to the tpdsworker', async function () {
this.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
it('should post the message to the tpdsworker', async function (ctx) {
ctx.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
const group0 = 'myproject'
const method0 = 'somemethod0'
const job0 = 'do something'
await this.TpdsUpdateSender.promises.enqueue(group0, method0, job0)
this.FetchUtils.fetchNothing.should.have.been.calledWithMatch(
this.enqueueUrl,
await ctx.TpdsUpdateSender.promises.enqueue(group0, method0, job0)
ctx.FetchUtils.fetchNothing.should.have.been.calledWithMatch(
ctx.enqueueUrl,
{
method: 'POST',
json: { group: group0, job: job0, method: method0 },
@@ -111,17 +129,17 @@ describe('TpdsUpdateSender', function () {
})
describe('sending updates', function () {
beforeEach(function () {
this.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
beforeEach(function (ctx) {
ctx.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
})
it('queues a post the file with user and file id and hash', async function () {
it('queues a post the file with user and file id and hash', async function (ctx) {
const fileId = '4545345'
const hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
const historyId = 91525
const path = '/some/path/here.jpg'
await this.TpdsUpdateSender.promises.addFile({
await ctx.TpdsUpdateSender.promises.addFile({
projectId,
historyId,
fileId,
@@ -130,8 +148,8 @@ describe('TpdsUpdateSender', function () {
projectName,
})
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: userId,
@@ -148,8 +166,8 @@ describe('TpdsUpdateSender', function () {
}
)
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: collaberatorRef,
@@ -157,8 +175,8 @@ describe('TpdsUpdateSender', function () {
}
)
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: readOnlyRef,
@@ -168,12 +186,12 @@ describe('TpdsUpdateSender', function () {
)
})
it('post doc with stream origin of docstore', async function () {
it('post doc with stream origin of docstore', async function (ctx) {
const docId = '4545345'
const path = '/some/path/here.tex'
const lines = ['line1', 'line2', 'line3']
await this.TpdsUpdateSender.promises.addDoc({
await ctx.TpdsUpdateSender.promises.addDoc({
projectId,
docId,
path,
@@ -181,8 +199,8 @@ describe('TpdsUpdateSender', function () {
projectName,
})
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: userId,
@@ -192,15 +210,15 @@ describe('TpdsUpdateSender', function () {
uri: `${thirdPartyDataStoreApiUrl}/user/${userId}/entity/${encodeURIComponent(
projectName
)}${encodeURIComponent(path)}`,
streamOrigin: `${this.docstoreUrl}/project/${projectId}/doc/${docId}/raw`,
streamOrigin: `${ctx.docstoreUrl}/project/${projectId}/doc/${docId}/raw`,
headers: {},
},
},
}
)
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: collaberatorRef,
@@ -211,8 +229,8 @@ describe('TpdsUpdateSender', function () {
}
)
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: readOnlyRef,
@@ -224,19 +242,19 @@ describe('TpdsUpdateSender', function () {
)
})
it('deleting entity', async function () {
it('deleting entity', async function (ctx) {
const path = '/path/here/t.tex'
const subtreeEntityIds = ['id1', 'id2']
await this.TpdsUpdateSender.promises.deleteEntity({
await ctx.TpdsUpdateSender.promises.deleteEntity({
projectId,
path,
projectName,
subtreeEntityIds,
})
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: userId,
@@ -253,8 +271,8 @@ describe('TpdsUpdateSender', function () {
}
)
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: collaberatorRef,
@@ -265,8 +283,8 @@ describe('TpdsUpdateSender', function () {
}
)
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: readOnlyRef,
@@ -278,19 +296,19 @@ describe('TpdsUpdateSender', function () {
)
})
it('moving entity', async function () {
it('moving entity', async function (ctx) {
const startPath = 'staring/here/file.tex'
const endPath = 'ending/here/file.tex'
await this.TpdsUpdateSender.promises.moveEntity({
await ctx.TpdsUpdateSender.promises.moveEntity({
projectId,
startPath,
endPath,
projectName,
})
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: userId,
@@ -308,8 +326,8 @@ describe('TpdsUpdateSender', function () {
}
)
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: collaberatorRef,
@@ -320,8 +338,8 @@ describe('TpdsUpdateSender', function () {
}
)
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: readOnlyRef,
@@ -333,18 +351,18 @@ describe('TpdsUpdateSender', function () {
)
})
it('should be able to rename a project using the move entity func', async function () {
it('should be able to rename a project using the move entity func', async function (ctx) {
const oldProjectName = '/oldProjectName/'
const newProjectName = '/newProjectName/'
await this.TpdsUpdateSender.promises.moveEntity({
await ctx.TpdsUpdateSender.promises.moveEntity({
projectId,
projectName: oldProjectName,
newProjectName,
})
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: userId,
@@ -362,8 +380,8 @@ describe('TpdsUpdateSender', function () {
}
)
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: collaberatorRef,
@@ -374,8 +392,8 @@ describe('TpdsUpdateSender', function () {
}
)
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: readOnlyRef,
@@ -387,11 +405,11 @@ describe('TpdsUpdateSender', function () {
)
})
it('pollDropboxForUser', async function () {
await this.TpdsUpdateSender.promises.pollDropboxForUser(userId)
it('pollDropboxForUser', async function (ctx) {
await ctx.TpdsUpdateSender.promises.pollDropboxForUser(userId)
expect(this.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
this.enqueueUrl,
expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
ctx.enqueueUrl,
{
json: {
group: userId,
@@ -410,24 +428,24 @@ describe('TpdsUpdateSender', function () {
})
describe('user not linked to dropbox', function () {
beforeEach(function () {
this.UserGetter.promises.getUsers
beforeEach(function (ctx) {
ctx.UserGetter.promises.getUsers
.withArgs({
_id: {
$in: this.memberIds,
$in: ctx.memberIds,
},
'dropbox.access_token.uid': { $ne: null },
})
.resolves([])
})
it('does not make request to tpds', async function () {
it('does not make request to tpds', async function (ctx) {
const fileId = '4545345'
const hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
const historyId = 91525
const path = '/some/path/here.jpg'
await this.TpdsUpdateSender.promises.addFile({
await ctx.TpdsUpdateSender.promises.addFile({
projectId,
historyId,
hash,
@@ -435,7 +453,7 @@ describe('TpdsUpdateSender', function () {
path,
projectName,
})
this.FetchUtils.fetchNothing.should.not.have.been.called
ctx.FetchUtils.fetchNothing.should.not.have.been.called
})
})
})

View File

@@ -236,12 +236,13 @@ describe('TokenAccessController', function () {
}),
}))
ctx.AdminAuthorizationHelper = {
canRedirectToAdminDomain: sinon.stub(),
}
vi.doMock(
'../../../../app/src/Features/Helpers/AdminAuthorizationHelper',
() =>
(ctx.AdminAuthorizationHelper = {
canRedirectToAdminDomain: sinon.stub(),
})
() => ({ default: ctx.AdminAuthorizationHelper })
)
vi.doMock(
@@ -764,17 +765,12 @@ describe('TokenAccessController', function () {
.stub()
.resolves([{ email: 'test@not-overleaf.com' }])
await new Promise(resolve => {
ctx.res.callback = () => {
expect(ctx.res.json).to.have.been.calledWith({
redirect: `${ctx.Settings.adminUrl}/#prefix`,
})
resolve()
}
ctx.TokenAccessController.grantTokenAccessReadAndWrite(
ctx.req,
ctx.res
)
await ctx.TokenAccessController.grantTokenAccessReadAndWrite(
ctx.req,
ctx.res
)
expect(ctx.res.json).to.have.been.calledWith({
redirect: `${ctx.Settings.adminUrl}/#prefix`,
})
})

View File

@@ -1,48 +1,53 @@
const sinon = require('sinon')
const { expect } = require('chai')
const mockFs = require('mock-fs')
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb-legacy')
const Settings = require('@overleaf/settings')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import mockFs from 'mock-fs'
import mongodb from 'mongodb-legacy'
import Settings from '@overleaf/settings'
const { ObjectId } = mongodb
const MODULE_PATH =
'../../../../app/src/Features/Uploads/FileSystemImportManager.js'
'../../../../app/src/Features/Uploads/FileSystemImportManager.mjs'
describe('FileSystemImportManager', function () {
beforeEach(function () {
this.projectId = new ObjectId()
this.folderId = new ObjectId()
this.newFolderId = new ObjectId()
this.userId = new ObjectId()
beforeEach(async function (ctx) {
ctx.projectId = new ObjectId()
ctx.folderId = new ObjectId()
ctx.newFolderId = new ObjectId()
ctx.userId = new ObjectId()
this.EditorController = {
ctx.EditorController = {
promises: {
addDoc: sinon.stub().resolves(),
addFile: sinon.stub().resolves(),
upsertDoc: sinon.stub().resolves(),
upsertFile: sinon.stub().resolves(),
addFolder: sinon.stub().resolves({ _id: this.newFolderId }),
addFolder: sinon.stub().resolves({ _id: ctx.newFolderId }),
},
}
this.FileSystemImportManager = SandboxedModule.require(MODULE_PATH, {
requires: {
'@overleaf/settings': {
textExtensions: ['tex', 'txt'],
editableFilenames: [
'latexmkrc',
'.latexmkrc',
'makefile',
'gnumakefile',
],
fileIgnorePattern: Settings.fileIgnorePattern, // use the real pattern from the default settings
},
'../Editor/EditorController': this.EditorController,
vi.doMock('@overleaf/settings', () => ({
default: {
textExtensions: ['tex', 'txt'],
editableFilenames: [
'latexmkrc',
'.latexmkrc',
'makefile',
'gnumakefile',
],
fileIgnorePattern: Settings.fileIgnorePattern, // use the real pattern from the default settings
},
})
}))
vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({
default: ctx.EditorController,
}))
ctx.FileSystemImportManager = (await import(MODULE_PATH)).default
})
describe('importDir', function () {
beforeEach(async function () {
beforeEach(async function (ctx) {
mockFs({
'import-test': {
'main.tex': 'My thesis',
@@ -64,87 +69,87 @@ describe('FileSystemImportManager', function () {
},
symlink: mockFs.symlink({ path: 'import-test' }),
})
this.entries =
await this.FileSystemImportManager.promises.importDir('import-test')
this.projectPaths = this.entries.map(x => x.projectPath)
ctx.entries =
await ctx.FileSystemImportManager.promises.importDir('import-test')
ctx.projectPaths = ctx.entries.map(x => x.projectPath)
})
afterEach(function () {
mockFs.restore()
})
it('should import regular docs', function () {
expect(this.entries).to.deep.include({
it('should import regular docs', function (ctx) {
expect(ctx.entries).to.deep.include({
type: 'doc',
projectPath: '/main.tex',
lines: ['My thesis'],
})
})
it('should skip symlinks inside the import folder', function () {
expect(this.projectPaths).not.to.include('/link-to-main.tex')
it('should skip symlinks inside the import folder', function (ctx) {
expect(ctx.projectPaths).not.to.include('/link-to-main.tex')
})
it('should skip ignored files', function () {
expect(this.projectPaths).not.to.include('/.DS_Store')
it('should skip ignored files', function (ctx) {
expect(ctx.projectPaths).not.to.include('/.DS_Store')
})
it('should import binary files', function () {
expect(this.entries).to.deep.include({
it('should import binary files', function (ctx) {
expect(ctx.entries).to.deep.include({
type: 'file',
projectPath: '/images/cat.jpg',
fsPath: 'import-test/images/cat.jpg',
})
})
it('should deal with Mac/Windows/Unix line endings', function () {
expect(this.entries).to.deep.include({
it('should deal with Mac/Windows/Unix line endings', function (ctx) {
expect(ctx.entries).to.deep.include({
type: 'doc',
projectPath: '/line-endings/unix.txt',
lines: ['one', 'two', 'three'],
})
expect(this.entries).to.deep.include({
expect(ctx.entries).to.deep.include({
type: 'doc',
projectPath: '/line-endings/mac.txt',
lines: ['uno', 'dos', 'tres'],
})
expect(this.entries).to.deep.include({
expect(ctx.entries).to.deep.include({
type: 'doc',
projectPath: '/line-endings/windows.txt',
lines: ['ein', 'zwei', 'drei'],
})
expect(this.entries).to.deep.include({
expect(ctx.entries).to.deep.include({
type: 'doc',
projectPath: '/line-endings/mixed.txt',
lines: ['uno', 'due', 'tre', 'quattro'],
})
})
it('should import documents with latin1 encoding', function () {
expect(this.entries).to.deep.include({
it('should import documents with latin1 encoding', function (ctx) {
expect(ctx.entries).to.deep.include({
type: 'doc',
projectPath: '/encodings/latin1.txt',
lines: ['tétanisant!'],
})
})
it('should import documents with utf16-le encoding', function () {
expect(this.entries).to.deep.include({
it('should import documents with utf16-le encoding', function (ctx) {
expect(ctx.entries).to.deep.include({
type: 'doc',
projectPath: '/encodings/utf16le.txt',
lines: ['\ufeffétonnant!'],
})
})
it('should error when the root folder is a symlink', async function () {
await expect(this.FileSystemImportManager.promises.importDir('symlink'))
.to.be.rejected
it('should error when the root folder is a symlink', async function (ctx) {
await expect(ctx.FileSystemImportManager.promises.importDir('symlink')).to
.be.rejected
})
})
describe('addEntity', function () {
describe('with directory', function () {
beforeEach(async function () {
beforeEach(async function (ctx) {
mockFs({
path: {
to: {
@@ -156,10 +161,10 @@ describe('FileSystemImportManager', function () {
},
})
await this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
await ctx.FileSystemImportManager.promises.addEntity(
ctx.userId,
ctx.projectId,
ctx.folderId,
'folder',
'path/to/folder',
false
@@ -170,32 +175,32 @@ describe('FileSystemImportManager', function () {
mockFs.restore()
})
it('should add a folder to the project', function () {
this.EditorController.promises.addFolder.should.have.been.calledWith(
this.projectId,
this.folderId,
it('should add a folder to the project', function (ctx) {
ctx.EditorController.promises.addFolder.should.have.been.calledWith(
ctx.projectId,
ctx.folderId,
'folder',
'upload'
)
})
it("should add the folder's contents", function () {
this.EditorController.promises.addDoc.should.have.been.calledWith(
this.projectId,
this.newFolderId,
it("should add the folder's contents", function (ctx) {
ctx.EditorController.promises.addDoc.should.have.been.calledWith(
ctx.projectId,
ctx.newFolderId,
'doc.tex',
['one', 'two', 'three'],
'upload',
this.userId
ctx.userId
)
this.EditorController.promises.addFile.should.have.been.calledWith(
this.projectId,
this.newFolderId,
ctx.EditorController.promises.addFile.should.have.been.calledWith(
ctx.projectId,
ctx.newFolderId,
'image.jpg',
'path/to/folder/image.jpg',
null,
'upload',
this.userId
ctx.userId
)
})
})
@@ -210,51 +215,51 @@ describe('FileSystemImportManager', function () {
})
describe('with replace set to false', function () {
beforeEach(async function () {
await this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
beforeEach(async function (ctx) {
await ctx.FileSystemImportManager.promises.addEntity(
ctx.userId,
ctx.projectId,
ctx.folderId,
'image.jpg',
'uploaded-file',
false
)
})
it('should add the file', function () {
this.EditorController.promises.addFile.should.have.been.calledWith(
this.projectId,
this.folderId,
it('should add the file', function (ctx) {
ctx.EditorController.promises.addFile.should.have.been.calledWith(
ctx.projectId,
ctx.folderId,
'image.jpg',
'uploaded-file',
null,
'upload',
this.userId
ctx.userId
)
})
})
describe('with replace set to true', function () {
beforeEach(async function () {
await this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
beforeEach(async function (ctx) {
await ctx.FileSystemImportManager.promises.addEntity(
ctx.userId,
ctx.projectId,
ctx.folderId,
'image.jpg',
'uploaded-file',
true
)
})
it('should add the file', function () {
this.EditorController.promises.upsertFile.should.have.been.calledWith(
this.projectId,
this.folderId,
it('should add the file', function (ctx) {
ctx.EditorController.promises.upsertFile.should.have.been.calledWith(
ctx.projectId,
ctx.folderId,
'image.jpg',
'uploaded-file',
null,
'upload',
this.userId
ctx.userId
)
})
})
@@ -279,49 +284,49 @@ describe('FileSystemImportManager', function () {
})
describe('with replace set to false', function () {
beforeEach(async function () {
await this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
beforeEach(async function (ctx) {
await ctx.FileSystemImportManager.promises.addEntity(
ctx.userId,
ctx.projectId,
ctx.folderId,
'doc.tex',
'path/to/uploaded-file',
false
)
})
it('should insert the doc', function () {
this.EditorController.promises.addDoc.should.have.been.calledWith(
this.projectId,
this.folderId,
it('should insert the doc', function (ctx) {
ctx.EditorController.promises.addDoc.should.have.been.calledWith(
ctx.projectId,
ctx.folderId,
'doc.tex',
['one', 'two', 'three'],
'upload',
this.userId
ctx.userId
)
})
})
describe('with replace set to true', function () {
beforeEach(async function () {
await this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
beforeEach(async function (ctx) {
await ctx.FileSystemImportManager.promises.addEntity(
ctx.userId,
ctx.projectId,
ctx.folderId,
'doc.tex',
'path/to/uploaded-file',
true
)
})
it('should upsert the doc', function () {
this.EditorController.promises.upsertDoc.should.have.been.calledWith(
this.projectId,
this.folderId,
it('should upsert the doc', function (ctx) {
ctx.EditorController.promises.upsertDoc.should.have.been.calledWith(
ctx.projectId,
ctx.folderId,
'doc.tex',
['one', 'two', 'three'],
'upload',
this.userId
ctx.userId
)
})
})
@@ -339,20 +344,20 @@ describe('FileSystemImportManager', function () {
mockFs.restore()
})
it('should stop with an error', async function () {
it('should stop with an error', async function (ctx) {
await expect(
this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
ctx.FileSystemImportManager.promises.addEntity(
ctx.userId,
ctx.projectId,
ctx.folderId,
'main.tex',
'path/to/symlink',
false
)
).to.be.rejectedWith('path is symlink')
this.EditorController.promises.addFolder.should.not.have.been.called
this.EditorController.promises.addDoc.should.not.have.been.called
this.EditorController.promises.addFile.should.not.have.been.called
ctx.EditorController.promises.addFolder.should.not.have.been.called
ctx.EditorController.promises.addDoc.should.not.have.been.called
ctx.EditorController.promises.addFile.should.not.have.been.called
})
})
})

View File

@@ -1,176 +1,238 @@
const sinon = require('sinon')
const { expect } = require('chai')
const timekeeper = require('timekeeper')
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb-legacy')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import timekeeper from 'timekeeper'
import mongodb from 'mongodb-legacy'
const { ObjectId } = mongodb
const MODULE_PATH =
'../../../../app/src/Features/Uploads/ProjectUploadManager.js'
'../../../../app/src/Features/Uploads/ProjectUploadManager.mjs'
describe('ProjectUploadManager', function () {
beforeEach(function () {
this.now = Date.now()
timekeeper.freeze(this.now)
this.rootFolderId = new ObjectId()
this.ownerId = new ObjectId()
this.zipPath = '/path/to/zip/file-name.zip'
this.extractedZipPath = `/path/to/zip/file-name-${this.now}`
this.mainContent = 'Contents of main.tex'
this.projectName = 'My project*'
this.fixedProjectName = 'My project'
this.uniqueProjectName = 'My project (1)'
this.project = {
beforeEach(async function (ctx) {
ctx.now = Date.now()
timekeeper.freeze(ctx.now)
ctx.rootFolderId = new ObjectId()
ctx.ownerId = new ObjectId()
ctx.zipPath = '/path/to/zip/file-name.zip'
ctx.extractedZipPath = `/path/to/zip/file-name-${ctx.now}`
ctx.mainContent = 'Contents of main.tex'
ctx.projectName = 'My project*'
ctx.fixedProjectName = 'My project'
ctx.uniqueProjectName = 'My project (1)'
ctx.project = {
_id: new ObjectId(),
rootFolder: [{ _id: this.rootFolderId }],
rootFolder: [{ _id: ctx.rootFolderId }],
overleaf: { history: { id: 12345 } },
}
this.doc = {
ctx.doc = {
_id: new ObjectId(),
name: 'main.tex',
}
this.docFsPath = '/path/to/doc'
this.docLines = ['My thesis', 'by A. U. Thor']
this.file = {
ctx.docFsPath = '/path/to/doc'
ctx.docLines = ['My thesis', 'by A. U. Thor']
ctx.file = {
_id: new ObjectId(),
name: 'image.png',
}
this.fileFsPath = '/path/to/file'
ctx.fileFsPath = '/path/to/file'
this.topLevelDestination = '/path/to/zip/file-extracted/nested'
this.newProjectVersion = 123
this.importEntries = [
ctx.topLevelDestination = '/path/to/zip/file-extracted/nested'
ctx.newProjectVersion = 123
ctx.importEntries = [
{
type: 'doc',
projectPath: '/main.tex',
lines: this.docLines,
lines: ctx.docLines,
},
{
type: 'file',
projectPath: `/${this.file.name}`,
fsPath: this.fileFsPath,
projectPath: `/${ctx.file.name}`,
fsPath: ctx.fileFsPath,
},
]
this.docEntries = [
ctx.docEntries = [
{
doc: this.doc,
path: `/${this.doc.name}`,
docLines: this.docLines.join('\n'),
doc: ctx.doc,
path: `/${ctx.doc.name}`,
docLines: ctx.docLines.join('\n'),
},
]
this.fileEntries = [
ctx.fileEntries = [
{
file: this.file,
path: `/${this.file.name}`,
file: ctx.file,
path: `/${ctx.file.name}`,
createdBlob: true,
},
]
this.fs = {
ctx.fs = {
promises: {
rm: sinon.stub().resolves(),
},
}
this.ArchiveManager = {
ctx.ArchiveManager = {
promises: {
extractZipArchive: sinon.stub().resolves(),
findTopLevelDirectory: sinon
.stub()
.withArgs(this.extractedZipPath)
.resolves(this.topLevelDestination),
.withArgs(ctx.extractedZipPath)
.resolves(ctx.topLevelDestination),
},
}
this.Doc = sinon.stub().returns(this.doc)
this.DocstoreManager = {
ctx.Doc = sinon.stub().returns(ctx.doc)
ctx.DocstoreManager = {
promises: {
updateDoc: sinon.stub().resolves(),
},
}
this.DocumentHelper = {
ctx.DocumentHelper = {
getTitleFromTexContent: sinon
.stub()
.withArgs(this.mainContent)
.returns(this.projectName),
.withArgs(ctx.mainContent)
.returns(ctx.projectName),
}
this.DocumentUpdaterHandler = {
ctx.DocumentUpdaterHandler = {
promises: {
updateProjectStructure: sinon.stub().resolves(),
},
}
this.FileStoreHandler = {
ctx.FileStoreHandler = {
promises: {
uploadFileFromDiskWithHistoryId: sinon.stub().resolves({
fileRef: this.file,
fileRef: ctx.file,
createdBlob: true,
}),
},
}
this.FileSystemImportManager = {
ctx.FileSystemImportManager = {
promises: {
importDir: sinon
.stub()
.withArgs(this.topLevelDestination)
.resolves(this.importEntries),
.withArgs(ctx.topLevelDestination)
.resolves(ctx.importEntries),
},
}
this.ProjectCreationHandler = {
ctx.ProjectCreationHandler = {
promises: {
createBlankProject: sinon.stub().resolves(this.project),
createBlankProject: sinon.stub().resolves(ctx.project),
},
}
this.ProjectEntityMongoUpdateHandler = {
ctx.ProjectEntityMongoUpdateHandler = {
promises: {
createNewFolderStructure: sinon.stub().resolves(this.newProjectVersion),
createNewFolderStructure: sinon.stub().resolves(ctx.newProjectVersion),
},
}
this.ProjectRootDocManager = {
ctx.ProjectRootDocManager = {
promises: {
setRootDocAutomatically: sinon.stub().resolves(),
findRootDocFileFromDirectory: sinon
.stub()
.resolves({ path: 'main.tex', content: this.mainContent }),
.resolves({ path: 'main.tex', content: ctx.mainContent }),
setRootDocFromName: sinon.stub().resolves(),
},
}
this.ProjectDetailsHandler = {
ctx.ProjectDetailsHandler = {
fixProjectName: sinon
.stub()
.withArgs(this.projectName)
.returns(this.fixedProjectName),
.withArgs(ctx.projectName)
.returns(ctx.fixedProjectName),
promises: {
generateUniqueName: sinon.stub().resolves(this.uniqueProjectName),
generateUniqueName: sinon.stub().resolves(ctx.uniqueProjectName),
},
}
this.ProjectDeleter = {
ctx.ProjectDeleter = {
promises: {
deleteProject: sinon.stub().resolves(),
},
}
this.TpdsProjectFlusher = {
ctx.TpdsProjectFlusher = {
promises: {
flushProjectToTpds: sinon.stub().resolves(),
},
}
this.ProjectUploadManager = SandboxedModule.require(MODULE_PATH, {
requires: {
fs: this.fs,
'./ArchiveManager': this.ArchiveManager,
'../../models/Doc': { Doc: this.Doc },
'../Docstore/DocstoreManager': this.DocstoreManager,
'../Documents/DocumentHelper': this.DocumentHelper,
'../DocumentUpdater/DocumentUpdaterHandler':
this.DocumentUpdaterHandler,
'../FileStore/FileStoreHandler': this.FileStoreHandler,
'./FileSystemImportManager': this.FileSystemImportManager,
'../Project/ProjectCreationHandler': this.ProjectCreationHandler,
'../Project/ProjectEntityMongoUpdateHandler':
this.ProjectEntityMongoUpdateHandler,
'../Project/ProjectRootDocManager': this.ProjectRootDocManager,
'../Project/ProjectDetailsHandler': this.ProjectDetailsHandler,
'../Project/ProjectDeleter': this.ProjectDeleter,
'../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
},
})
vi.doMock('fs', () => ({
default: ctx.fs,
}))
vi.doMock('../../../../app/src/Features/Uploads/ArchiveManager', () => ({
default: ctx.ArchiveManager,
}))
vi.doMock('../../../../app/src/models/Doc', () => ({
Doc: ctx.Doc,
}))
vi.doMock('../../../../app/src/Features/Docstore/DocstoreManager', () => ({
default: ctx.DocstoreManager,
}))
vi.doMock('../../../../app/src/Features/Documents/DocumentHelper', () => ({
default: ctx.DocumentHelper,
}))
vi.doMock(
'../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
() => ({
default: ctx.DocumentUpdaterHandler,
})
)
vi.doMock(
'../../../../app/src/Features/FileStore/FileStoreHandler',
() => ({
default: ctx.FileStoreHandler,
})
)
vi.doMock(
'../../../../app/src/Features/Uploads/FileSystemImportManager',
() => ({
default: ctx.FileSystemImportManager,
})
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectCreationHandler',
() => ({
default: ctx.ProjectCreationHandler,
})
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler',
() => ({
default: ctx.ProjectEntityMongoUpdateHandler,
})
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectRootDocManager',
() => ({
default: ctx.ProjectRootDocManager,
})
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectDetailsHandler',
() => ({
default: ctx.ProjectDetailsHandler,
})
)
vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({
default: ctx.ProjectDeleter,
}))
vi.doMock(
'../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher',
() => ({
default: ctx.TpdsProjectFlusher,
})
)
ctx.ProjectUploadManager = (await import(MODULE_PATH)).default
})
afterEach(function () {
@@ -179,65 +241,65 @@ describe('ProjectUploadManager', function () {
describe('createProjectFromZipArchive', function () {
describe('when the title can be read from the root document', function () {
beforeEach(async function () {
await this.ProjectUploadManager.promises.createProjectFromZipArchive(
this.ownerId,
this.projectName,
this.zipPath
beforeEach(async function (ctx) {
await ctx.ProjectUploadManager.promises.createProjectFromZipArchive(
ctx.ownerId,
ctx.projectName,
ctx.zipPath
)
})
it('should extract the archive', function () {
this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
this.zipPath,
this.extractedZipPath
it('should extract the archive', function (ctx) {
ctx.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
ctx.zipPath,
ctx.extractedZipPath
)
})
it('should create a project', function () {
this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
this.ownerId,
this.uniqueProjectName
it('should create a project', function (ctx) {
ctx.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
ctx.ownerId,
ctx.uniqueProjectName
)
})
it('should initialize the file tree', function () {
this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
this.project._id,
this.docEntries,
this.fileEntries
it('should initialize the file tree', function (ctx) {
ctx.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
ctx.project._id,
ctx.docEntries,
ctx.fileEntries
)
})
it('should notify document updater', function () {
this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
this.project._id,
this.project.overleaf.history.id,
this.ownerId,
it('should notify document updater', function (ctx) {
ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
ctx.project._id,
ctx.project.overleaf.history.id,
ctx.ownerId,
{
newDocs: this.docEntries,
newFiles: this.fileEntries,
newProject: { version: this.newProjectVersion },
newDocs: ctx.docEntries,
newFiles: ctx.fileEntries,
newProject: { version: ctx.newProjectVersion },
},
null
)
})
it('should flush the project to TPDS', function () {
this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
this.project._id
it('should flush the project to TPDS', function (ctx) {
ctx.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
ctx.project._id
)
})
it('should set the root document', function () {
this.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWith(
this.project._id,
it('should set the root document', function (ctx) {
ctx.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWith(
ctx.project._id,
'main.tex'
)
})
it('should remove the destination directory afterwards', function () {
this.fs.promises.rm.should.have.been.calledWith(this.extractedZipPath, {
it('should remove the destination directory afterwards', function (ctx) {
ctx.fs.promises.rm.should.have.been.calledWith(ctx.extractedZipPath, {
recursive: true,
force: true,
})
@@ -245,122 +307,122 @@ describe('ProjectUploadManager', function () {
})
describe("when the root document can't be determined", function () {
beforeEach(async function () {
this.ProjectRootDocManager.promises.findRootDocFileFromDirectory.resolves(
beforeEach(async function (ctx) {
ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory.resolves(
{}
)
await this.ProjectUploadManager.promises.createProjectFromZipArchive(
this.ownerId,
this.projectName,
this.zipPath
await ctx.ProjectUploadManager.promises.createProjectFromZipArchive(
ctx.ownerId,
ctx.projectName,
ctx.zipPath
)
})
it('should not try to set the root doc', function () {
this.ProjectRootDocManager.promises.setRootDocFromName.should.not.have
it('should not try to set the root doc', function (ctx) {
ctx.ProjectRootDocManager.promises.setRootDocFromName.should.not.have
.been.called
})
})
})
describe('createProjectFromZipArchiveWithName', function () {
beforeEach(async function () {
await this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
this.ownerId,
this.projectName,
this.zipPath
beforeEach(async function (ctx) {
await ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
ctx.ownerId,
ctx.projectName,
ctx.zipPath
)
})
it('should extract the archive', function () {
this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
this.zipPath,
this.extractedZipPath
it('should extract the archive', function (ctx) {
ctx.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
ctx.zipPath,
ctx.extractedZipPath
)
})
it('should create a project owned by the owner_id', function () {
this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
this.ownerId,
this.uniqueProjectName
it('should create a project owned by the owner_id', function (ctx) {
ctx.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
ctx.ownerId,
ctx.uniqueProjectName
)
})
it('should automatically set the root doc', function () {
this.ProjectRootDocManager.promises.setRootDocAutomatically.should.have.been.calledWith(
this.project._id
it('should automatically set the root doc', function (ctx) {
ctx.ProjectRootDocManager.promises.setRootDocAutomatically.should.have.been.calledWith(
ctx.project._id
)
})
it('should initialize the file tree', function () {
this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
this.project._id,
this.docEntries,
this.fileEntries
it('should initialize the file tree', function (ctx) {
ctx.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
ctx.project._id,
ctx.docEntries,
ctx.fileEntries
)
})
it('should notify document updater', function () {
this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
this.project._id,
this.project.overleaf.history.id,
this.ownerId,
it('should notify document updater', function (ctx) {
ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
ctx.project._id,
ctx.project.overleaf.history.id,
ctx.ownerId,
{
newDocs: this.docEntries,
newFiles: this.fileEntries,
newProject: { version: this.newProjectVersion },
newDocs: ctx.docEntries,
newFiles: ctx.fileEntries,
newProject: { version: ctx.newProjectVersion },
},
null
)
})
it('should flush the project to TPDS', function () {
this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
this.project._id
it('should flush the project to TPDS', function (ctx) {
ctx.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
ctx.project._id
)
})
it('should remove the destination directory afterwards', function () {
this.fs.promises.rm.should.have.been.calledWith(this.extractedZipPath, {
it('should remove the destination directory afterwards', function (ctx) {
ctx.fs.promises.rm.should.have.been.calledWith(ctx.extractedZipPath, {
recursive: true,
force: true,
})
})
describe('when initializing the folder structure fails', function () {
beforeEach(async function () {
this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.rejects()
beforeEach(async function (ctx) {
ctx.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.rejects()
await expect(
this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
this.ownerId,
this.projectName,
this.zipPath
ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
ctx.ownerId,
ctx.projectName,
ctx.zipPath
)
).to.be.rejected
})
it('should cleanup the blank project created', async function () {
this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
this.project._id
it('should cleanup the blank project created', async function (ctx) {
ctx.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
ctx.project._id
)
})
})
describe('when setting automatically the root doc fails', function () {
beforeEach(async function () {
this.ProjectRootDocManager.promises.setRootDocAutomatically.rejects()
beforeEach(async function (ctx) {
ctx.ProjectRootDocManager.promises.setRootDocAutomatically.rejects()
await expect(
this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
this.ownerId,
this.projectName,
this.zipPath
ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
ctx.ownerId,
ctx.projectName,
ctx.zipPath
)
).to.be.rejected
})
it('should cleanup the blank project created', function () {
this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
this.project._id
it('should cleanup the blank project created', function (ctx) {
ctx.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
ctx.project._id
)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,57 @@
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath = '../../../../app/src/Features/User/UserHandler.js'
const SandboxedModule = require('sandboxed-module')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
const modulePath = '../../../../app/src/Features/User/UserHandler.mjs'
describe('UserHandler', function () {
beforeEach(function () {
this.user = {
beforeEach(async function (ctx) {
ctx.user = {
_id: '12390i',
email: 'bob@bob.com',
remove: sinon.stub().callsArgWith(0),
}
this.TeamInvitesHandler = {
ctx.TeamInvitesHandler = {
promises: {
createTeamInvitesForLegacyInvitedEmail: sinon.stub().resolves(),
},
}
this.db = {
ctx.db = {
users: {
countDocuments: sinon.stub().resolves(2),
},
}
this.UserHandler = SandboxedModule.require(modulePath, {
requires: {
'../Subscription/TeamInvitesHandler': this.TeamInvitesHandler,
'../../infrastructure/mongodb': { db: this.db },
},
})
vi.doMock(
'../../../../app/src/Features/Subscription/TeamInvitesHandler',
() => ({
default: ctx.TeamInvitesHandler,
})
)
vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({
db: ctx.db,
READ_PREFERENCE_SECONDARY: 'read-preference-secondary',
}))
ctx.UserHandler = (await import(modulePath)).default
})
describe('populateTeamInvites', function () {
beforeEach(async function () {
await this.UserHandler.promises.populateTeamInvites(this.user)
beforeEach(async function (ctx) {
await ctx.UserHandler.promises.populateTeamInvites(ctx.user)
})
it('notifies the user about legacy team invites', function () {
this.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail
.calledWith(this.user.email)
it('notifies the user about legacy team invites', function (ctx) {
ctx.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail
.calledWith(ctx.user.email)
.should.eq(true)
})
})
describe('countActiveUsers', function () {
it('return user count from DB lookup', async function () {
expect(await this.UserHandler.promises.countActiveUsers()).to.equal(2)
it('return user count from DB lookup', async function (ctx) {
expect(await ctx.UserHandler.promises.countActiveUsers()).to.equal(2)
})
})
})