[web] deduplicate getting the project when loading the editor (#32762)

* [web] enable async local storage on all the endpoints

Co-authored-by: Brian Gough <brian.gough@overleaf.com>

* [web] deduplicate getting the project when loading the editor

Co-authored-by: Brian Gough <brian.gough@overleaf.com>

* [web] use ProjectAccess state for computing analytics segmentation

Co-authored-by: Brian Gough <brian.gough@overleaf.com>

* [web] restore ownership of active flag and deferredTpdsFlushCounter

* [web] add missing await

* [web] update unit tests

* [web] add metrics for project access caching

* [web] add missing test mock

* [web] invalidate async local storage when changing project access

* [web] deduplicate project lookup when checking for token access

* [web] add helper function for getting cached ProjectAccess

* [web] add acceptance test for caching of ProjectAccess

* [web] account for saas-only project access in tests

* Revert "[web] enable async local storage on all the endpoints"

This reverts commit 1b82f3b935040e8cfd180d1f6bf4183a655580e2.

* [web] add async local storage to project endpoints in top-50

* [web] invalidate async local storage for project access from modules

Co-authored-by: Jessica Lawshe <jessica.lawshe@overleaf.com>

---------

Co-authored-by: Brian Gough <brian.gough@overleaf.com>
Co-authored-by: Jessica Lawshe <jessica.lawshe@overleaf.com>
GitOrigin-RevId: 3eea7956b24e6f937dc1c17948681063d4dca3ea
This commit is contained in:
Jakob Ackermann
2026-04-17 08:07:08 +00:00
committed by Copybot
co-authored by Brian Gough Jessica Lawshe
parent 6117feef1b
commit d56477565c
18 changed files with 219 additions and 69 deletions
@@ -12,6 +12,8 @@ import Errors from '../Errors/Errors.js'
import ProjectEditorHandler from '../Project/ProjectEditorHandler.mjs'
import Sources from '../Authorization/Sources.mjs'
import PrivilegeLevels from '../Authorization/PrivilegeLevels.mjs'
import AsyncLocalStorage from '../../infrastructure/AsyncLocalStorage.mjs'
import Metrics from '@overleaf/metrics'
const { ObjectId } = mongodb
@@ -44,6 +46,12 @@ class ProjectAccess {
/** @type {PublicAccessLevel} */
#publicAccessLevel
/** @type {ObjectId} */
#ownerId
/** @type {Record<string, number>} */
#stats
/**
* @param {{ owner_ref: ObjectId; collaberator_refs: ObjectId[]; readOnly_refs: ObjectId[]; tokenAccessReadAndWrite_refs: ObjectId[]; tokenAccessReadOnly_refs: ObjectId[]; publicAccesLevel: PublicAccessLevel; pendingEditor_refs: ObjectId[]; reviewer_refs: ObjectId[]; pendingReviewer_refs: ObjectId[]; }} project
*/
@@ -59,7 +67,28 @@ class ProjectAccess {
project.reviewer_refs,
project.pendingReviewer_refs
)
this.#stats = {
reviewers: (project.reviewer_refs || []).length,
namedEditors: (project.collaberator_refs || []).length,
pendingEditors: (project.pendingEditor_refs || []).length,
tokenEditors: (project.tokenAccessReadAndWrite_refs || []).length,
}
this.#publicAccessLevel = project.publicAccesLevel
this.#ownerId = project.owner_ref
}
/**
* @return {ObjectId}
*/
getOwnerId() {
return this.#ownerId
}
/**
* @return {Record<string, number>}
*/
getStats() {
return this.#stats
}
/**
@@ -154,6 +183,24 @@ class ProjectAccess {
return false
}
/**
* @param {string | ObjectId} userId
* @return {boolean}
*/
isUserReadWriteTokenMember(userId) {
if (!userId) return false
for (const member of this.#members) {
if (
member.id === userId.toString() &&
member.source === Sources.TOKEN &&
member.privilegeLevel === PrivilegeLevels.READ_AND_WRITE
) {
return true
}
}
return false
}
/**
* @param {string | ObjectId} userId
* @return {boolean}
@@ -212,10 +259,30 @@ class ProjectAccess {
}
}
/**
* @param {string} projectId
* @param {string} path
* @return {ProjectAccess|null}
* @private
*/
function _getCachedProjectAccess(projectId, path) {
const store = AsyncLocalStorage.storage.getStore()
const key = `projectAccess:${projectId}`
if (store && store[key]) {
Metrics.inc('project_access_cache', 1, { status: 'hit', path })
return store[key]
}
Metrics.inc('project_access_cache', 1, { status: 'miss', path })
return null
}
/**
* @param {any} projectId
*/
async function getProjectAccess(projectId) {
let projectAccess = _getCachedProjectAccess(projectId, 'full')
if (projectAccess) return projectAccess
const project = await ProjectGetter.promises.getProject(projectId, {
owner_ref: 1,
collaberator_refs: 1,
@@ -230,7 +297,25 @@ async function getProjectAccess(projectId) {
if (!project) {
throw new Errors.NotFoundError(`no project found with id ${projectId}`)
}
return new ProjectAccess(project)
projectAccess = new ProjectAccess(project)
const store = AsyncLocalStorage.storage.getStore()
const key = `projectAccess:${projectId}`
if (store) store[key] = projectAccess
return projectAccess
}
/**
* @param {string} projectId
* @return {Promise<ObjectId>}
*/
async function getProjectOwnerId(projectId) {
const projectAccess = _getCachedProjectAccess(projectId, 'project-owner')
if (projectAccess) return projectAccess.getOwnerId()
const project = await ProjectGetter.promises.getProject(projectId, {
owner_ref: true,
})
return project.owner_ref
}
/**
@@ -451,6 +536,9 @@ async function getAllInvitedMembers(projectId) {
* @param {any} projectId
*/
async function userIsTokenMember(userId, projectId) {
const projectAccess = _getCachedProjectAccess(projectId, 'token-member')
if (projectAccess) return projectAccess.isUserTokenMember(userId)
userId = new ObjectId(userId.toString())
projectId = new ObjectId(projectId.toString())
const project = await Project.findOne(
@@ -473,6 +561,9 @@ async function userIsTokenMember(userId, projectId) {
* @param {any} projectId
*/
async function userIsReadWriteTokenMember(userId, projectId) {
const projectAccess = _getCachedProjectAccess(projectId, 'rw-token-member')
if (projectAccess) return projectAccess.isUserReadWriteTokenMember(userId)
userId = new ObjectId(userId.toString())
projectId = new ObjectId(projectId.toString())
const project = await Project.findOne(
@@ -645,6 +736,7 @@ export default {
userIsTokenMember,
userIsReadWriteTokenMember,
getAllInvitedMembers,
getProjectOwnerId,
},
ProjectAccess,
}
@@ -12,6 +12,7 @@ import Errors from '../Errors/Errors.js'
import TpdsUpdateSender from '../ThirdPartyDataStore/TpdsUpdateSender.mjs'
import EditorRealTimeController from '../Editor/EditorRealTimeController.mjs'
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.mjs'
import AsyncLocalStorage from '../../infrastructure/AsyncLocalStorage.mjs'
export default {
userIsTokenMember: callbackify(userIsTokenMember),
@@ -31,6 +32,7 @@ export default {
}
async function removeUserFromProject(projectId, userId) {
AsyncLocalStorage.removeItem(`projectAccess:${projectId}`)
try {
await Project.updateOne(
{ _id: projectId },
@@ -97,6 +99,7 @@ async function addUserIdToProject(
privilegeLevel,
{ pendingEditor, pendingReviewer } = {}
) {
AsyncLocalStorage.removeItem(`projectAccess:${projectId}`)
const project = await ProjectGetter.promises.getProject(projectId, {
owner_ref: 1,
name: 1,
@@ -199,6 +202,9 @@ async function transferProjects(fromUserId, toUserId) {
).exec()
const projectIds = projects.map(p => p._id)
logger.debug({ projectIds, fromUserId, toUserId }, 'transferring projects')
for (const projectId of projectIds) {
AsyncLocalStorage.removeItem(`projectAccess:${projectId}`)
}
await Project.updateMany(
{ owner_ref: fromUserId },
@@ -273,6 +279,7 @@ async function setCollaboratorPrivilegeLevel(
{ pendingEditor, pendingReviewer } = {},
auditInfo = {}
) {
AsyncLocalStorage.removeItem(`projectAccess:${projectId}`)
// Make sure we're only updating the project if the user is already a
// collaborator
const query = {
@@ -13,6 +13,7 @@ import OError from '@overleaf/o-error'
import TagsHandler from '../Tags/TagsHandler.mjs'
import { promiseMapWithLimit } from '@overleaf/promise-utils'
import LimitationsManager from '../Subscription/LimitationsManager.mjs'
import AsyncLocalStorage from '../../infrastructure/AsyncLocalStorage.mjs'
export default {
promises: {
@@ -212,6 +213,7 @@ async function _determinePrivilegeLevelForPreviousOwner(projectId) {
}
async function _transferOwnership(projectId, previousOwnerId, newOwnerId) {
AsyncLocalStorage.removeItem(`projectAccess:${projectId}`)
// Remove new owner from collaborators list
await CollaboratorsHandler.promises.removeUserFromProject(
projectId,
@@ -3,6 +3,7 @@ import AuthenticationController from '../Authentication/AuthenticationController
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.mjs'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
import AsyncLocalStorage from '../../infrastructure/AsyncLocalStorage.mjs'
const rateLimiters = {
addDocToProject: new RateLimiter('add-doc-to-project', {
@@ -20,6 +21,7 @@ export default {
apply(webRouter, privateApiRouter) {
webRouter.post(
'/project/:Project_id/doc',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
RateLimiterMiddleware.rateLimit(rateLimiters.addDocToProject, {
params: ['Project_id'],
@@ -37,6 +39,7 @@ export default {
webRouter.post(
'/project/:Project_id/:entity_type/:entity_id/rename',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
EditorHttpController.renameEntity
)
@@ -48,6 +51,7 @@ export default {
webRouter.delete(
'/project/:Project_id/file/:entity_id',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
EditorHttpController.deleteFile
)
@@ -6,6 +6,7 @@ import AuthenticationController from '../Authentication/AuthenticationController
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
import HistoryController from './HistoryController.mjs'
import AsyncLocalStorage from '../../infrastructure/AsyncLocalStorage.mjs'
const rateLimiters = {
downloadProjectRevision: new RateLimiter('download-project-revision', {
@@ -33,12 +34,14 @@ function apply(webRouter, privateApiRouter) {
webRouter.head(
'/project/:project_id/blob/:hash',
RateLimiterMiddleware.rateLimit(rateLimiters.getProjectBlob),
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.headBlob
)
webRouter.get(
'/project/:project_id/blob/:hash',
RateLimiterMiddleware.rateLimit(rateLimiters.getProjectBlob),
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.getBlob
)
@@ -47,24 +50,28 @@ function apply(webRouter, privateApiRouter) {
webRouter.get(
'/project/:Project_id/updates',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.proxyToHistoryApiAndInjectUserDetails
)
webRouter.get(
'/project/:Project_id/doc/:doc_id/diff',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.proxyToHistoryApi
)
webRouter.get(
'/project/:Project_id/diff',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.proxyToHistoryApiAndInjectUserDetails
)
webRouter.get(
'/project/:Project_id/filetree/diff',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.proxyToHistoryApi
@@ -103,6 +110,7 @@ function apply(webRouter, privateApiRouter) {
webRouter.post(
'/project/:Project_id/flush',
RateLimiterMiddleware.rateLimit(rateLimiters.flushHistory),
AsyncLocalStorage.middleware,
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.proxyToHistoryApi
@@ -117,6 +125,7 @@ function apply(webRouter, privateApiRouter) {
webRouter.get(
'/project/:Project_id/labels',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.getLabels
@@ -136,12 +145,14 @@ function apply(webRouter, privateApiRouter) {
webRouter.get(
'/project/:project_id/latest/history',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.getLatestHistory
)
webRouter.get(
'/project/:project_id/changes',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.getChanges
@@ -2,7 +2,6 @@ import OError from '@overleaf/o-error'
import logger from '@overleaf/logger'
import DocstoreManager from '../Docstore/DocstoreManager.mjs'
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.mjs'
import ProjectGetter from '../Project/ProjectGetter.mjs'
import ProjectUpdateHandler from '../Project/ProjectUpdateHandler.mjs'
import { Project } from '../../models/Project.mjs'
import Modules from '../../infrastructure/Modules.mjs'
@@ -33,19 +32,12 @@ function findInactiveProjects(limit, daysOld) {
}
const InactiveProjectManager = {
async reactivateProjectIfRequired(projectId) {
let project
try {
project = await ProjectGetter.promises.getProject(projectId, {
active: true,
})
} catch (err) {
OError.tag(err, 'error getting project', {
project_id: projectId,
})
throw err
}
/**
* @param {{_id: ObjectId, active: boolean }} project
* @return {Promise<void>}
*/
async reactivateProjectIfRequired(project) {
const projectId = project._id.toString()
logger.debug(
{ projectId, active: project.active },
'seeing if need to reactivate project'
@@ -539,17 +539,16 @@ const _ProjectController = {
const responses = await pProps({
userValues: userId ? getUserValues(userId) : defaultUserValues(),
project: ProjectGetter.promises.getProject(projectId, {
_id: 1,
name: 1,
active: 1,
deferredTpdsFlushCounter: 1,
lastUpdated: 1,
track_changes: 1,
owner_ref: 1,
brandVariationId: 1,
overleaf: 1,
tokens: 1,
tokenAccessReadAndWrite_refs: 1, // used for link sharing analytics
collaberator_refs: 1, // used for link sharing analytics
pendingEditor_refs: 1, // used for link sharing analytics
reviewer_refs: 1,
}),
userIsMemberOfGroupSubscription: sessionUser
? (async () =>
@@ -559,16 +558,15 @@ const _ProjectController = {
)
).isMember)()
: false,
_flushToTpds:
TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(projectId),
_activate:
InactiveProjectManager.promises.reactivateProjectIfRequired(
projectId
),
})
const { project, userValues, userIsMemberOfGroupSubscription } = responses
await Promise.all([
InactiveProjectManager.promises.reactivateProjectIfRequired(project),
TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(project),
])
const {
user,
learnedWords,
@@ -705,9 +703,11 @@ const _ProjectController = {
project.owner_ref
)
if (userId) {
const projectAccess =
await CollaboratorsGetter.promises.getProjectAccess(projectId)
const planLimit = ownerFeatures?.collaborators || 0
const namedEditors = project.collaberator_refs?.length || 0
const pendingEditors = project.pendingEditor_refs?.length || 0
const { namedEditors, pendingEditors, tokenEditors } =
projectAccess.getStats()
const exceedAtLimit = planLimit > -1 && namedEditors >= planLimit
let mode = 'edit'
@@ -727,7 +727,7 @@ const _ProjectController = {
projectId: project._id,
namedEditors,
pendingEditors,
tokenEditors: project.tokenAccessReadAndWrite_refs?.length || 0,
tokenEditors,
planLimit,
exceedAtLimit,
}
@@ -2,7 +2,6 @@
import logger from '@overleaf/logger'
import ProjectGetter from '../Project/ProjectGetter.mjs'
import UserGetter from '../User/UserGetter.mjs'
import SubscriptionLocator from './SubscriptionLocator.mjs'
import Settings from '@overleaf/settings'
@@ -15,10 +14,9 @@ import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
* @param {any} projectId
*/
async function allowedNumberOfCollaboratorsInProject(projectId) {
const project = await ProjectGetter.promises.getProject(projectId, {
owner_ref: true,
})
return await allowedNumberOfCollaboratorsForUser(project.owner_ref)
const ownerId =
await CollaboratorsGetter.promises.getProjectOwnerId(projectId)
return await allowedNumberOfCollaboratorsForUser(ownerId)
}
/**
@@ -34,13 +34,10 @@ async function flushProjectToTpds(projectId) {
* Flush a project to TPDS if a flush is pending. This is called when
* projects are loaded in the editor and triggers a sync to dropbox for
* projects that were imported from Overleaf v1.
* @param {{name: string, deferredTpdsFlushCounter: number, overleaf: { history: { id: (string|number)}} }} project
* @return {Promise<void>}
*/
async function flushProjectToTpdsIfNeeded(projectId) {
const project = await ProjectGetter.promises.getProject(projectId, {
name: true,
deferredTpdsFlushCounter: true,
'overleaf.history.id': 1,
})
async function flushProjectToTpdsIfNeeded(project) {
if (project.deferredTpdsFlushCounter > 0) {
await _flushProjectToTpds(project)
}
@@ -4,6 +4,7 @@ import ProjectUploadController from './ProjectUploadController.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.mjs'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
import Settings from '@overleaf/settings'
import AsyncLocalStorage from '../../infrastructure/AsyncLocalStorage.mjs'
const rateLimiters = {
projectUpload: new RateLimiter('project-upload', {
@@ -47,6 +48,7 @@ export default {
webRouter.post(
fileUploadEndpoint,
fileUploadRateLimit,
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
ProjectUploadController.multerMiddleware,
ProjectUploadController.uploadFile
@@ -56,6 +58,7 @@ export default {
fileUploadEndpoint,
fileUploadRateLimit,
AuthenticationController.requireLogin(),
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
ProjectUploadController.multerMiddleware,
ProjectUploadController.uploadFile
+6
View File
@@ -589,6 +589,7 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
RateLimiterMiddleware.rateLimit(rateLimiters.compileProjectHttp, {
params: ['Project_id'],
}),
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanReadProject,
CompileController.compile
)
@@ -601,6 +602,7 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
webRouter.get(
'/project/:Project_id/output/cached/output.overleaf.json',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanReadProject,
ClsiCacheController.getLatestBuildFromCache
)
@@ -680,11 +682,13 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
)
webRouter.get(
'/project/:Project_id/sync/code',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanReadProject,
CompileController.proxySyncCode
)
webRouter.get(
'/project/:Project_id/sync/pdf',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanReadProject,
CompileController.proxySyncPdf
)
@@ -778,6 +782,7 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
webRouter.get(
'/project/:project_id/metadata',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanReadProject,
Settings.allowAnonymousReadAndWriteSharing
? (req, res, next) => {
@@ -788,6 +793,7 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
)
webRouter.post(
'/project/:project_id/doc/:doc_id/metadata',
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanReadProject,
Settings.allowAnonymousReadAndWriteSharing
? (req, res, next) => {
@@ -5,11 +5,22 @@ import mongodb from 'mongodb-legacy'
import cheerio from 'cheerio'
import { Subscription } from '../../../app/src/models/Subscription.mjs'
import Features from '../../../app/src/infrastructure/Features.mjs'
import Metrics from './helpers/metrics.mjs'
const ObjectId = mongodb.ObjectId
const User = UserHelper.promises
async function getProjectAccessStats() {
const hit = await Metrics.promises.sumMetrics(
s => s.startsWith('project_access_cache') && s.includes('"hit"')
)
const miss = await Metrics.promises.sumMetrics(
s => s.startsWith('project_access_cache') && s.includes('"miss"')
)
return { hit, miss }
}
describe('Project CRUD', function () {
beforeEach(async function () {
this.user = new User()
@@ -65,6 +76,22 @@ describe('Project CRUD', function () {
'<meta name="ol-showUpgradePrompt" data-type="boolean">'
)
})
it('should cache the project access', async function () {
const prev = await getProjectAccessStats()
await loadProject(this.user, this.projectId)
if (Features.hasFeature('saas')) {
expect(await getProjectAccessStats()).to.deep.equal({
hit: prev.hit + 7,
miss: prev.miss + 1,
})
} else {
expect(await getProjectAccessStats()).to.deep.equal({
hit: prev.hit + 4,
miss: prev.miss + 1,
})
}
})
})
describe("when project doesn't exist", function () {
@@ -2,6 +2,10 @@ import { callbackify } from 'node:util'
import request from './request.js'
import metrics from '@overleaf/metrics'
/**
* @param {(s: string) => bool} matcher
* @return {Promise<number>}
*/
async function getMetric(matcher) {
const { body } = await request.promises.request('/metrics')
const found = body.split('\n').find(matcher)
@@ -9,6 +13,16 @@ async function getMetric(matcher) {
return parseInt(found.split(' ')[1], 0)
}
/**
* @param {(s: string) => bool} matcher
* @return {Promise<number>}
*/
async function sumMetrics(matcher) {
const { body = '' } = await request.promises.request('/metrics')
const found = body.split('\n').filter(matcher)
return found.reduce((sum, next) => sum + parseInt(next.split(' ')[1], 0), 0)
}
/* sets all metrics to zero
https://github.com/siimon/prom-client?tab=readme-ov-file#resetting-metrics
*/
@@ -21,5 +35,6 @@ export default {
resetMetrics,
promises: {
getMetric,
sumMetrics,
},
}
@@ -70,6 +70,10 @@ describe('CollaboratorsGetter', function () {
buildUserModelView: sinon.stub(),
}
vi.doMock('@overleaf/metrics', () => ({
default: { inc: sinon.stub() },
}))
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
@@ -56,10 +56,6 @@ describe('InactiveProjectManager', function () {
})
)
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: ctx.ProjectGetter,
}))
vi.doMock('../../../../app/src/models/Project', () => ({}))
vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
@@ -79,15 +75,14 @@ describe('InactiveProjectManager', function () {
describe('reactivateProjectIfRequired', function () {
beforeEach(function (ctx) {
ctx.project = { active: false }
ctx.ProjectGetter.promises.getProject.resolves(ctx.project)
ctx.project = { _id: ctx.project_id, active: false }
ctx.ProjectUpdateHandler.promises.markAsActive.resolves()
})
it('should call unarchiveProject', async function (ctx) {
ctx.DocstoreManager.promises.unarchiveProject.resolves()
await ctx.InactiveProjectManager.promises.reactivateProjectIfRequired(
ctx.project_id
ctx.project
)
ctx.DocstoreManager.promises.unarchiveProject
@@ -102,7 +97,7 @@ describe('InactiveProjectManager', function () {
ctx.DocstoreManager.promises.unarchiveProject.rejects()
await expect(
ctx.InactiveProjectManager.promises.reactivateProjectIfRequired(
ctx.project_id
ctx.project
)
).to.be.rejected
@@ -118,7 +113,7 @@ describe('InactiveProjectManager', function () {
ctx.project.active = true
ctx.DocstoreManager.promises.unarchiveProject.resolves()
await ctx.InactiveProjectManager.promises.reactivateProjectIfRequired(
ctx.project_id
ctx.project
)
ctx.DocstoreManager.promises.unarchiveProject
.calledWith(ctx.project_id)
@@ -112,7 +112,9 @@ describe('ProjectController', function () {
},
}
ctx.InactiveProjectManager = {
promises: { reactivateProjectIfRequired: sinon.stub() },
promises: {
reactivateProjectIfRequired: sinon.stub(),
},
}
ctx.ProjectUpdateHandler = {
promises: {
@@ -144,6 +146,15 @@ describe('ProjectController', function () {
}
ctx.CollaboratorsGetter = {
promises: {
getProjectAccess: sinon.stub().resolves({
getStats() {
return {
namedEditors: 1,
pendingEditors: 2,
tokenEditors: 3,
}
},
}),
userIsTokenMember: sinon.stub().resolves(false),
isUserInvitedMemberOfProject: sinon.stub().resolves(true),
userIsReadWriteTokenMember: sinon.stub().resolves(false),
@@ -1018,11 +1029,11 @@ describe('ProjectController', function () {
})
})
it('should reactivateProjectIfRequired', async function (ctx) {
it('should call reactivateProjectIfRequired', async function (ctx) {
await new Promise(resolve => {
ctx.res.render = (pageName, opts) => {
ctx.InactiveProjectManager.promises.reactivateProjectIfRequired
.calledWith(ctx.project_id)
.calledWith(ctx.project)
.should.equal(true)
resolve()
}
@@ -1096,7 +1107,7 @@ describe('ProjectController', function () {
await new Promise(resolve => {
ctx.res.render = () => {
ctx.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded.should.have.been.calledWith(
ctx.project_id
ctx.project
)
resolve()
}
@@ -13,17 +13,6 @@ describe('LimitationsManager', function () {
_id: (ctx.projectId = 'project-id'),
owner_ref: ctx.userId,
}
ctx.ProjectGetter = {
promises: {
getProject: sinon.stub().callsFake(async (projectId, fields) => {
if (projectId === ctx.projectId) {
return ctx.project
} else {
return null
}
}),
},
}
ctx.UserGetter = {
promises: {
getUser: sinon.stub().callsFake(async (userId, filter) => {
@@ -48,6 +37,7 @@ describe('LimitationsManager', function () {
promises: {
getInvitedEditCollaboratorCount: sinon.stub().resolves(0),
getMemberIdPrivilegeLevel: sinon.stub(),
getProjectOwnerId: sinon.stub().resolves(ctx.project.owner_ref),
},
}
@@ -57,10 +47,6 @@ describe('LimitationsManager', function () {
},
}
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: ctx.ProjectGetter,
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
@@ -197,7 +197,7 @@ describe('TpdsProjectFlusher', function () {
beforeEach(async function (ctx) {
ctx.project.deferredTpdsFlushCounter = counterValue
await ctx.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(
ctx.project._id
ctx.project
)
})
@@ -232,7 +232,7 @@ describe('TpdsProjectFlusher', function () {
.chain('exec')
.resolves()
await ctx.TpdsProjectFlusher.promises.flushProjectToTpdsIfNeeded(
ctx.project._id
ctx.project
)
})