[web] Convert some Features files to ES modules (part 3) (#28494)

* Rename files to mjs

* Rename test files to mjs

* Update CODEOWNERS

* Update files to ESM

* Update test files to ESM

* Update RestoreManager.test.mjs

* Remove unused `AdminAuthorizationHelper` mock and stub

* Remove unnecessary return

GitOrigin-RevId: 2b9ef126de1d8964afbc6e5641cca36712655866
This commit is contained in:
Antoine Clausse
2025-09-16 10:43:59 +02:00
committed by Copybot
parent eda1bd697f
commit 71f0b28a84
54 changed files with 3274 additions and 3075 deletions
@@ -1,21 +1,26 @@
const AuthorizationManager = require('./AuthorizationManager')
const logger = require('@overleaf/logger')
const { ObjectId } = require('mongodb-legacy')
const Errors = require('../Errors/Errors')
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const AuthenticationController = require('../Authentication/AuthenticationController')
const SessionManager = require('../Authentication/SessionManager')
const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
const { expressify } = require('@overleaf/promise-utils')
const {
canRedirectToAdminDomain,
} = require('../Helpers/AdminAuthorizationHelper')
const { getSafeAdminDomainRedirect } = require('../Helpers/UrlHelper')
import AuthorizationManager from './AuthorizationManager.js'
import logger from '@overleaf/logger'
import mongodb from 'mongodb-legacy'
import Errors from '../Errors/Errors.js'
import HttpErrorHandler from '../Errors/HttpErrorHandler.js'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import SessionManager from '../Authentication/SessionManager.js'
import TokenAccessHandler from '../TokenAccess/TokenAccessHandler.js'
import { expressify } from '@overleaf/promise-utils'
import AdminAuthorizationHelper from '../Helpers/AdminAuthorizationHelper.js'
import UrlHelper from '../Helpers/UrlHelper.js'
const { ObjectId } = mongodb
function _handleAdminDomainRedirect(req, res) {
if (canRedirectToAdminDomain(SessionManager.getSessionUser(req.session))) {
if (
AdminAuthorizationHelper.canRedirectToAdminDomain(
SessionManager.getSessionUser(req.session)
)
) {
logger.warn({ req }, 'redirecting admin user to admin domain')
res.redirect(getSafeAdminDomainRedirect(req.originalUrl))
res.redirect(UrlHelper.getSafeAdminDomainRedirect(req.originalUrl))
return true
}
return false
@@ -255,7 +260,7 @@ function restricted(req, res, next) {
res.redirect('/login')
}
module.exports = {
export default {
ensureUserCanReadMultipleProjects: expressify(
ensureUserCanReadMultipleProjects
),
@@ -1,15 +1,9 @@
// @ts-check
const { ForbiddenError, UserNotFoundError } = require('../Errors/Errors')
const {
getUserCapabilities,
getUserRestrictions,
combineGroupPolicies,
combineAllowedProperties,
} = require('./PermissionsManager')
const { assertUserPermissions } = require('./PermissionsManager').promises
const Modules = require('../../infrastructure/Modules')
const { expressify } = require('@overleaf/promise-utils')
const Features = require('../../infrastructure/Features')
import { ForbiddenError, UserNotFoundError } from '../Errors/Errors.js'
import PermissionsManager from './PermissionsManager.js'
import Modules from '../../infrastructure/Modules.js'
import { expressify } from '@overleaf/promise-utils'
import Features from '../../infrastructure/Features.js'
/**
* @typedef {(import('express').Request)} Request
@@ -18,6 +12,14 @@ const Features = require('../../infrastructure/Features')
* @typedef {import('./PermissionsManager').Capability} Capability
*/
const {
getUserCapabilities,
getUserRestrictions,
combineGroupPolicies,
combineAllowedProperties,
promises: { assertUserPermissions },
} = PermissionsManager
/**
* Function that returns middleware to add an `assertPermission` function to the request object to check if the user has a specific capability.
* @returns {() => (req: Request, res: Response, next: NextFunction) => void} The middleware function that adds the `assertPermission` function to the request object.
@@ -116,7 +118,7 @@ function requirePermission(...requiredCapabilities) {
return doRequest
}
module.exports = {
export default {
requirePermission,
useCapabilities,
}
@@ -1,12 +1,12 @@
const OError = require('@overleaf/o-error')
const { URL } = require('url')
const settings = require('@overleaf/settings')
const logger = require('@overleaf/logger')
const V1Api = require('../V1/V1Api')
const sanitizeHtml = require('sanitize-html')
const { promisify } = require('@overleaf/promise-utils')
import OError from '@overleaf/o-error'
import { URL } from 'node:url'
import settings from '@overleaf/settings'
import logger from '@overleaf/logger'
import V1Api from '../V1/V1Api.js'
import sanitizeHtml from 'sanitize-html'
import { promisify } from '@overleaf/promise-utils'
module.exports = {
export default {
getBrandVariationById,
promises: {
getBrandVariationById: promisify(getBrandVariationById),
@@ -3,7 +3,7 @@ import logger from '@overleaf/logger'
import Settings from '@overleaf/settings'
import Metrics from '@overleaf/metrics'
import OError from '@overleaf/o-error'
import DeviceHistory from './DeviceHistory.js'
import DeviceHistory from './DeviceHistory.mjs'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import { expressify } from '@overleaf/promise-utils'
import EmailsHelper from '../Helpers/EmailHelper.js'
@@ -1,7 +1,7 @@
const crypto = require('crypto')
const jose = require('jose')
const Metrics = require('@overleaf/metrics')
const Settings = require('@overleaf/settings')
import crypto from 'node:crypto'
import * as jose from 'jose'
import Metrics from '@overleaf/metrics'
import Settings from '@overleaf/settings'
const COOKIE_NAME = Settings.deviceHistory.cookieName
const ENTRY_EXPIRY = Settings.deviceHistory.entryExpiry
@@ -100,4 +100,4 @@ class DeviceHistory {
}
}
module.exports = DeviceHistory
export default DeviceHistory
@@ -12,7 +12,7 @@ import logger from '@overleaf/logger'
import { expressify } from '@overleaf/promise-utils'
import { hasAdminAccess } from '../Helpers/AdminAuthorizationHelper.js'
import TokenAccessHandler from '../TokenAccess/TokenAccessHandler.js'
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js'
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.mjs'
import LimitationsManager from '../Subscription/LimitationsManager.js'
import Features from '../../infrastructure/Features.js'
@@ -12,7 +12,7 @@ import AnalyticsManager from '../Analytics/AnalyticsManager.js'
import SessionManager from '../Authentication/SessionManager.js'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import { expressify } from '@overleaf/promise-utils'
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js'
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.mjs'
import Errors from '../Errors/Errors.js'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
@@ -10,7 +10,7 @@ import ProjectGetter from '../Project/ProjectGetter.js'
import NotificationsBuilder from '../Notifications/NotificationsBuilder.js'
import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
import LimitationsManager from '../Subscription/LimitationsManager.js'
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js'
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.mjs'
import _ from 'lodash'
const CollaboratorsInviteHandler = {
@@ -1,6 +1,6 @@
import CollaboratorsController from './CollaboratorsController.mjs'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
import CollaboratorsInviteController from './CollaboratorsInviteController.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
@@ -7,7 +7,7 @@ import EmailHandler from '../Email/EmailHandler.js'
import Errors from '../Errors/Errors.js'
import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.js'
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js'
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.mjs'
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
import OError from '@overleaf/o-error'
import TagsHandler from '../Tags/TagsHandler.js'
@@ -7,7 +7,7 @@ import Path from 'node:path'
import { pipeline } from 'node:stream/promises'
import logger from '@overleaf/logger'
import ClsiCacheManager from './ClsiCacheManager.js'
import CompileController from './CompileController.js'
import CompileController from './CompileController.mjs'
import { expressify } from '@overleaf/promise-utils'
import ClsiCacheHandler from './ClsiCacheHandler.js'
import ProjectGetter from '../Project/ProjectGetter.js'
@@ -1,28 +1,31 @@
const { URL } = require('url')
const { pipeline } = require('stream/promises')
const { Cookie } = require('tough-cookie')
const OError = require('@overleaf/o-error')
const Metrics = require('@overleaf/metrics')
const ProjectGetter = require('../Project/ProjectGetter')
const CompileManager = require('./CompileManager')
const ClsiManager = require('./ClsiManager')
const logger = require('@overleaf/logger')
const Settings = require('@overleaf/settings')
const Errors = require('../Errors/Errors')
const SessionManager = require('../Authentication/SessionManager')
const { RateLimiter } = require('../../infrastructure/RateLimiter')
const { z, zz, validateReq } = require('../../infrastructure/Validation')
const ClsiCookieManager = require('./ClsiCookieManager')(
Settings.apis.clsi?.backendGroupName
)
const Path = require('path')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const { expressify } = require('@overleaf/promise-utils')
const {
import { URL } from 'node:url'
import { pipeline } from 'node:stream/promises'
import { Cookie } from 'tough-cookie'
import OError from '@overleaf/o-error'
import Metrics from '@overleaf/metrics'
import ProjectGetter from '../Project/ProjectGetter.js'
import CompileManager from './CompileManager.js'
import ClsiManager from './ClsiManager.js'
import logger from '@overleaf/logger'
import Settings from '@overleaf/settings'
import Errors from '../Errors/Errors.js'
import SessionManager from '../Authentication/SessionManager.js'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import Validation from '../../infrastructure/Validation.js'
import ClsiCookieManagerFactory from './ClsiCookieManager.js'
import Path from 'node:path'
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
import { expressify } from '@overleaf/promise-utils'
import {
fetchStreamWithResponse,
RequestFailedError,
} = require('@overleaf/fetch-utils')
} from '@overleaf/fetch-utils'
const { z, zz, validateReq } = Validation
const ClsiCookieManager = ClsiCookieManagerFactory(
Settings.apis.clsi?.backendGroupName
)
const COMPILE_TIMEOUT_MS = 10 * 60 * 1000
@@ -744,4 +747,4 @@ const CompileController = {
_proxyToClsiWithLimits: _CompileController._proxyToClsiWithLimits,
}
module.exports = CompileController
export default CompileController
@@ -1,6 +1,6 @@
import EditorHttpController from './EditorHttpController.mjs'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import { validate, Joi } from '../../infrastructure/Validation.js'
@@ -1,6 +1,6 @@
const { UserSchema } = require('../../models/User')
import { UserSchema } from '../../models/User.js'
module.exports = {
export default {
hasAnyStaffAccess,
}
@@ -22,7 +22,7 @@ import Errors from '../Errors/Errors.js'
import HistoryManager from './HistoryManager.js'
import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.js'
import ProjectEntityUpdateHandler from '../Project/ProjectEntityUpdateHandler.js'
import RestoreManager from './RestoreManager.js'
import RestoreManager from './RestoreManager.mjs'
import { prepareZipAttachment } from '../../infrastructure/Response.js'
import Features from '../../infrastructure/Features.js'
@@ -4,7 +4,7 @@ import Settings from '@overleaf/settings'
import { Joi, validate } from '../../infrastructure/Validation.js'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import HistoryController from './HistoryController.mjs'
@@ -1,23 +1,23 @@
const Settings = require('@overleaf/settings')
const Path = require('path')
const FileWriter = require('../../infrastructure/FileWriter')
const Metrics = require('../../infrastructure/Metrics')
const FileSystemImportManager = require('../Uploads/FileSystemImportManager')
const EditorController = require('../Editor/EditorController')
const Errors = require('../Errors/Errors')
const moment = require('moment')
const { callbackifyAll } = require('@overleaf/promise-utils')
const { fetchJson } = require('@overleaf/fetch-utils')
const ProjectLocator = require('../Project/ProjectLocator')
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
const ChatApiHandler = require('../Chat/ChatApiHandler')
const DocstoreManager = require('../Docstore/DocstoreManager')
const logger = require('@overleaf/logger')
const EditorRealTimeController = require('../Editor/EditorRealTimeController')
const ChatManager = require('../Chat/ChatManager')
const OError = require('@overleaf/o-error')
const ProjectGetter = require('../Project/ProjectGetter')
const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
import Settings from '@overleaf/settings'
import Path from 'node:path'
import FileWriter from '../../infrastructure/FileWriter.js'
import Metrics from '../../infrastructure/Metrics.js'
import FileSystemImportManager from '../Uploads/FileSystemImportManager.js'
import EditorController from '../Editor/EditorController.js'
import Errors from '../Errors/Errors.js'
import moment from 'moment'
import { callbackifyAll } from '@overleaf/promise-utils'
import { fetchJson } from '@overleaf/fetch-utils'
import ProjectLocator from '../Project/ProjectLocator.js'
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.js'
import ChatApiHandler from '../Chat/ChatApiHandler.js'
import DocstoreManager from '../Docstore/DocstoreManager.js'
import logger from '@overleaf/logger'
import EditorRealTimeController from '../Editor/EditorRealTimeController.js'
import ChatManager from '../Chat/ChatManager.js'
import OError from '@overleaf/o-error'
import ProjectGetter from '../Project/ProjectGetter.js'
import ProjectEntityHandler from '../Project/ProjectEntityHandler.js'
async function getCommentThreadIds(projectId) {
await DocumentUpdaterHandler.promises.flushProjectToMongo(projectId)
@@ -441,4 +441,4 @@ const RestoreManager = {
},
}
module.exports = { ...callbackifyAll(RestoreManager), promises: RestoreManager }
export default { ...callbackifyAll(RestoreManager), promises: RestoreManager }
@@ -9,7 +9,7 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import InactiveProjectManager from './InactiveProjectManager.js'
import InactiveProjectManager from './InactiveProjectManager.mjs'
export default {
deactivateOldProjects(req, res) {
@@ -1,14 +1,14 @@
const OError = require('@overleaf/o-error')
const logger = require('@overleaf/logger')
const DocstoreManager = require('../Docstore/DocstoreManager')
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
const ProjectGetter = require('../Project/ProjectGetter')
const ProjectUpdateHandler = require('../Project/ProjectUpdateHandler')
const { Project } = require('../../models/Project')
const Modules = require('../../infrastructure/Modules')
const { READ_PREFERENCE_SECONDARY } = require('../../infrastructure/mongodb')
const { callbackifyAll } = require('@overleaf/promise-utils')
const Metrics = require('@overleaf/metrics')
import OError from '@overleaf/o-error'
import logger from '@overleaf/logger'
import DocstoreManager from '../Docstore/DocstoreManager.js'
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.js'
import ProjectGetter from '../Project/ProjectGetter.js'
import ProjectUpdateHandler from '../Project/ProjectUpdateHandler.js'
import { Project } from '../../models/Project.js'
import Modules from '../../infrastructure/Modules.js'
import { READ_PREFERENCE_SECONDARY } from '../../infrastructure/mongodb.js'
import { callbackifyAll } from '@overleaf/promise-utils'
import Metrics from '@overleaf/metrics'
const MILISECONDS_IN_DAY = 86400000
@@ -132,7 +132,7 @@ const InactiveProjectManager = {
},
}
module.exports = {
export default {
...callbackifyAll(InactiveProjectManager),
promises: InactiveProjectManager,
findInactiveProjects,
@@ -14,7 +14,7 @@ import SessionManager from '../Authentication/SessionManager.js'
import Settings from '@overleaf/settings'
import _ from 'lodash'
import AnalyticsManager from '../../../../app/src/Features/Analytics/AnalyticsManager.js'
import LinkedFilesHandler from './LinkedFilesHandler.js'
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
import {
CompileFailedError,
UrlFetchFailedError,
@@ -1,14 +1,14 @@
const FileWriter = require('../../infrastructure/FileWriter')
const EditorController = require('../Editor/EditorController')
const ProjectLocator = require('../Project/ProjectLocator')
const { Project } = require('../../models/Project')
const ProjectGetter = require('../Project/ProjectGetter')
const {
import FileWriter from '../../infrastructure/FileWriter.js'
import EditorController from '../Editor/EditorController.js'
import ProjectLocator from '../Project/ProjectLocator.js'
import { Project } from '../../models/Project.js'
import ProjectGetter from '../Project/ProjectGetter.js'
import {
ProjectNotFoundError,
V1ProjectNotFoundError,
BadDataError,
} = require('./LinkedFilesErrors')
const { callbackifyAll } = require('@overleaf/promise-utils')
} from './LinkedFilesErrors.js'
import { callbackifyAll } from '@overleaf/promise-utils'
const LinkedFilesHandler = {
async getFileById(projectId, fileId) {
@@ -100,7 +100,7 @@ const LinkedFilesHandler = {
},
}
module.exports = {
export default {
promises: LinkedFilesHandler,
...callbackifyAll(LinkedFilesHandler, {
multiResult: { getFileById: ['file', 'path', 'parentFolder'] },
@@ -1,4 +1,4 @@
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
@@ -15,7 +15,7 @@ import ProjectLocator from '../Project/ProjectLocator.js'
import DocstoreManager from '../Docstore/DocstoreManager.js'
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.js'
import _ from 'lodash'
import LinkedFilesHandler from './LinkedFilesHandler.js'
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
import {
BadDataError,
@@ -9,7 +9,7 @@ import {
AccessDeniedError,
} from './LinkedFilesErrors.js'
import { OutputFileFetchFailedError } from '../Errors/Errors.js'
import LinkedFilesHandler from './LinkedFilesHandler.js'
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
import { promisify } from '@overleaf/promise-utils'
function _prepare(projectId, linkedFileData, userId, callback) {
@@ -1,7 +1,7 @@
import logger from '@overleaf/logger'
import urlValidator from 'valid-url'
import { InvalidUrlError, UrlFetchFailedError } from './LinkedFilesErrors.js'
import LinkedFilesHandler from './LinkedFilesHandler.js'
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
import UrlHelper from '../Helpers/UrlHelper.js'
import { fetchStream, RequestFailedError } from '@overleaf/fetch-utils'
import { callbackify } from '@overleaf/promise-utils'
@@ -1,8 +1,8 @@
const logger = require('@overleaf/logger')
const { ProjectAuditLogEntry } = require('../../models/ProjectAuditLogEntry')
const { callbackify } = require('@overleaf/promise-utils')
import logger from '@overleaf/logger'
import { ProjectAuditLogEntry } from '../../models/ProjectAuditLogEntry.js'
import { callbackify } from '@overleaf/promise-utils'
module.exports = {
export default {
promises: {
addEntry,
},
@@ -7,7 +7,7 @@ import logger from '@overleaf/logger'
import { expressify } from '@overleaf/promise-utils'
import mongodb from 'mongodb-legacy'
import ProjectDeleter from './ProjectDeleter.js'
import ProjectDuplicator from './ProjectDuplicator.js'
import ProjectDuplicator from './ProjectDuplicator.mjs'
import ProjectCreationHandler from './ProjectCreationHandler.js'
import EditorController from '../Editor/EditorController.js'
import ProjectHelper from './ProjectHelper.js'
@@ -18,7 +18,7 @@ import { isPaidSubscription } from '../Subscription/SubscriptionHelper.js'
import LimitationsManager from '../Subscription/LimitationsManager.js'
import Settings from '@overleaf/settings'
import AuthorizationManager from '../Authorization/AuthorizationManager.js'
import InactiveProjectManager from '../InactiveData/InactiveProjectManager.js'
import InactiveProjectManager from '../InactiveData/InactiveProjectManager.mjs'
import ProjectUpdateHandler from './ProjectUpdateHandler.js'
import ProjectGetter from './ProjectGetter.js'
import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
@@ -29,7 +29,7 @@ import CollaboratorsGetter from '../Collaborators/CollaboratorsGetter.js'
import ProjectEntityHandler from './ProjectEntityHandler.js'
import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.js'
import Features from '../../infrastructure/Features.js'
import BrandVariationsHandler from '../BrandVariations/BrandVariationsHandler.js'
import BrandVariationsHandler from '../BrandVariations/BrandVariationsHandler.mjs'
import UserController from '../User/UserController.mjs'
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
@@ -39,7 +39,7 @@ import SpellingHandler from '../Spelling/SpellingHandler.js'
import { hasAdminAccess } from '../Helpers/AdminAuthorizationHelper.js'
import InstitutionsFeatures from '../Institutions/InstitutionsFeatures.js'
import InstitutionsGetter from '../Institutions/InstitutionsGetter.js'
import ProjectAuditLogHandler from './ProjectAuditLogHandler.js'
import ProjectAuditLogHandler from './ProjectAuditLogHandler.mjs'
import PublicAccessLevels from '../Authorization/PublicAccessLevels.js'
import TagsHandler from '../Tags/TagsHandler.js'
import TutorialHandler from '../Tutorial/TutorialHandler.js'
@@ -1,27 +1,27 @@
const { callbackify } = require('util')
const Path = require('path')
const logger = require('@overleaf/logger')
const OError = require('@overleaf/o-error')
const { promiseMapWithLimit } = require('@overleaf/promise-utils')
const { Doc } = require('../../models/Doc')
const { File } = require('../../models/File')
const DocstoreManager = require('../Docstore/DocstoreManager')
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
const HistoryManager = require('../History/HistoryManager')
const ProjectCreationHandler = require('./ProjectCreationHandler')
const ProjectDeleter = require('./ProjectDeleter')
const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler')
const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler')
const ProjectGetter = require('./ProjectGetter')
const ProjectLocator = require('./ProjectLocator')
const ProjectOptionsHandler = require('./ProjectOptionsHandler')
const SafePath = require('./SafePath')
const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
const _ = require('lodash')
const TagsHandler = require('../Tags/TagsHandler')
const ClsiCacheManager = require('../Compile/ClsiCacheManager')
import { callbackify } from 'node:util'
import Path from 'node:path'
import logger from '@overleaf/logger'
import OError from '@overleaf/o-error'
import { promiseMapWithLimit } from '@overleaf/promise-utils'
import { Doc } from '../../models/Doc.js'
import { File } from '../../models/File.js'
import DocstoreManager from '../Docstore/DocstoreManager.js'
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.js'
import HistoryManager from '../History/HistoryManager.js'
import ProjectCreationHandler from './ProjectCreationHandler.js'
import ProjectDeleter from './ProjectDeleter.js'
import ProjectEntityMongoUpdateHandler from './ProjectEntityMongoUpdateHandler.js'
import ProjectEntityUpdateHandler from './ProjectEntityUpdateHandler.js'
import ProjectGetter from './ProjectGetter.js'
import ProjectLocator from './ProjectLocator.js'
import ProjectOptionsHandler from './ProjectOptionsHandler.js'
import SafePath from './SafePath.js'
import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.js'
import _ from 'lodash'
import TagsHandler from '../Tags/TagsHandler.js'
import ClsiCacheManager from '../Compile/ClsiCacheManager.js'
module.exports = {
export default {
duplicate: callbackify(duplicate),
promises: {
duplicate,
@@ -1,5 +1,5 @@
import AuthenticationController from '../Authentication/AuthenticationController.js'
import PermissionsController from '../Authorization/PermissionsController.js'
import PermissionsController from '../Authorization/PermissionsController.mjs'
import SubscriptionController from './SubscriptionController.js'
import SubscriptionGroupController from './SubscriptionGroupController.mjs'
import TeamInvitesController from './TeamInvitesController.mjs'
@@ -7,7 +7,7 @@ import OError from '@overleaf/o-error'
import { expressify } from '@overleaf/promise-utils'
import AuthorizationManager from '../Authorization/AuthorizationManager.js'
import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js'
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.mjs'
import CollaboratorsInviteHandler from '../Collaborators/CollaboratorsInviteHandler.mjs'
import CollaboratorsHandler from '../Collaborators/CollaboratorsHandler.js'
import EditorRealTimeController from '../Editor/EditorRealTimeController.js'
@@ -1,5 +1,5 @@
import AuthenticationController from '../Authentication/AuthenticationController.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import TokenAccessController from './TokenAccessController.mjs'
export default {
@@ -1,4 +1,4 @@
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import ProjectUploadController from './ProjectUploadController.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
+3 -3
View File
@@ -13,7 +13,7 @@ import UploadsRouter from './Features/Uploads/UploadsRouter.mjs'
import metrics from '@overleaf/metrics'
import ReferalController from './Features/Referal/ReferalController.mjs'
import AuthenticationController from './Features/Authentication/AuthenticationController.js'
import PermissionsController from './Features/Authorization/PermissionsController.js'
import PermissionsController from './Features/Authorization/PermissionsController.mjs'
import SessionManager from './Features/Authentication/SessionManager.js'
import TagsController from './Features/Tags/TagsController.mjs'
import NotificationsController from './Features/Notifications/NotificationsController.mjs'
@@ -25,7 +25,7 @@ import UserPagesController from './Features/User/UserPagesController.mjs'
import TutorialController from './Features/Tutorial/TutorialController.mjs'
import DocumentController from './Features/Documents/DocumentController.mjs'
import CompileManager from './Features/Compile/CompileManager.js'
import CompileController from './Features/Compile/CompileController.js'
import CompileController from './Features/Compile/CompileController.mjs'
import HealthCheckController from './Features/HealthCheck/HealthCheckController.mjs'
import ProjectDownloadsController from './Features/Downloads/ProjectDownloadsController.mjs'
import FileStoreController from './Features/FileStore/FileStoreController.mjs'
@@ -45,7 +45,7 @@ import RateLimiterMiddleware from './Features/Security/RateLimiterMiddleware.js'
import InactiveProjectController from './Features/InactiveData/InactiveProjectController.mjs'
import ContactRouter from './Features/Contacts/ContactRouter.mjs'
import ReferencesController from './Features/References/ReferencesController.mjs'
import AuthorizationMiddleware from './Features/Authorization/AuthorizationMiddleware.js'
import AuthorizationMiddleware from './Features/Authorization/AuthorizationMiddleware.mjs'
import BetaProgramController from './Features/BetaProgram/BetaProgramController.mjs'
import AnalyticsRouter from './Features/Analytics/AnalyticsRouter.mjs'
import MetaController from './Features/Metadata/MetaController.mjs'
@@ -2,7 +2,7 @@ import logger from '@overleaf/logger'
import LaunchpadController from './LaunchpadController.mjs'
import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.js'
import AuthorizationMiddleware from '../../../../app/src/Features/Authorization/AuthorizationMiddleware.js'
import AuthorizationMiddleware from '../../../../app/src/Features/Authorization/AuthorizationMiddleware.mjs'
export default {
apply(webRouter) {
@@ -1,7 +1,7 @@
import logger from '@overleaf/logger'
import UserActivateController from './UserActivateController.mjs'
import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.js'
import AuthorizationMiddleware from '../../../../app/src/Features/Authorization/AuthorizationMiddleware.js'
import AuthorizationMiddleware from '../../../../app/src/Features/Authorization/AuthorizationMiddleware.mjs'
export default {
apply(webRouter) {
+1 -1
View File
@@ -70,7 +70,7 @@ function findPathCounts(projectId, docEntries, fileEntries) {
return pathCounts
}
// copied from services/web/app/src/Features/Project/ProjectDuplicator.js
// copied from services/web/app/src/Features/Project/ProjectDuplicator.mjs
function _getFolderEntries(folder, folderPath = '/') {
const docEntries = []
const fileEntries = []
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import minimist from 'minimist'
import PQueue from 'p-queue'
import InactiveProjectManager from '../app/src/Features/InactiveData/InactiveProjectManager.js'
import InactiveProjectManager from '../app/src/Features/InactiveData/InactiveProjectManager.mjs'
import { gracefulShutdown } from '../app/src/infrastructure/GracefulShutdown.js'
import logger from '@overleaf/logger'
@@ -1,26 +1,27 @@
const sinon = require('sinon')
const { expect } = require('chai')
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb-legacy')
const Errors = require('../../../../app/src/Features/Errors/Errors')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
const { ObjectId } = mongodb
const MODULE_PATH =
'../../../../app/src/Features/Authorization/AuthorizationMiddleware.js'
'../../../../app/src/Features/Authorization/AuthorizationMiddleware.mjs'
describe('AuthorizationMiddleware', function () {
beforeEach(function () {
this.userId = new ObjectId().toString()
this.project_id = new ObjectId().toString()
this.doc_id = new ObjectId().toString()
this.thread_id = new ObjectId().toString()
this.token = 'some-token'
this.AuthenticationController = {}
this.SessionManager = {
beforeEach(async function (ctx) {
ctx.userId = new ObjectId().toString()
ctx.project_id = new ObjectId().toString()
ctx.doc_id = new ObjectId().toString()
ctx.thread_id = new ObjectId().toString()
ctx.token = 'some-token'
ctx.AuthenticationController = {}
ctx.SessionManager = {
getSessionUser: sinon.stub().returns(null),
getLoggedInUserId: sinon.stub().returns(this.userId),
getLoggedInUserId: sinon.stub().returns(ctx.userId),
isUserLoggedIn: sinon.stub().returns(true),
}
this.AuthorizationManager = {
ctx.AuthorizationManager = {
promises: {
canUserReadProject: sinon.stub(),
canUserWriteProjectSettings: sinon.stub(),
@@ -33,46 +34,88 @@ describe('AuthorizationMiddleware', function () {
isRestrictedUserForProject: sinon.stub(),
},
}
this.HttpErrorHandler = {
ctx.HttpErrorHandler = {
forbidden: sinon.stub(),
}
this.TokenAccessHandler = {
getRequestToken: sinon.stub().returns(this.token),
ctx.TokenAccessHandler = {
getRequestToken: sinon.stub().returns(ctx.token),
}
this.DocumentUpdaterHandler = {
ctx.DocumentUpdaterHandler = {
promises: {
getComment: sinon.stub().resolves(),
},
}
this.AuthorizationMiddleware = SandboxedModule.require(MODULE_PATH, {
requires: {
'./AuthorizationManager': this.AuthorizationManager,
'../Errors/HttpErrorHandler': this.HttpErrorHandler,
'mongodb-legacy': { ObjectId },
'../Authentication/AuthenticationController':
this.AuthenticationController,
'../Authentication/SessionManager': this.SessionManager,
'../TokenAccess/TokenAccessHandler': this.TokenAccessHandler,
'../Helpers/AdminAuthorizationHelper': {
vi.doMock('../../../../app/src/Features/Errors/Errors.js', () => ({
default: Errors,
}))
vi.doMock(
'../../../../app/src/Features/Authorization/AuthorizationManager',
() => ({
default: ctx.AuthorizationManager,
})
)
vi.doMock('../../../../app/src/Features/Errors/HttpErrorHandler', () => ({
default: ctx.HttpErrorHandler,
}))
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
vi.doMock(
'../../../../app/src/Features/Authentication/AuthenticationController',
() => ({
default: ctx.AuthenticationController,
})
)
vi.doMock(
'../../../../app/src/Features/Authentication/SessionManager',
() => ({
default: ctx.SessionManager,
})
)
vi.doMock(
'../../../../app/src/Features/TokenAccess/TokenAccessHandler',
() => ({
default: ctx.TokenAccessHandler,
})
)
vi.doMock(
'../../../../app/src/Features/Helpers/AdminAuthorizationHelper',
() => ({
default: {
canRedirectToAdminDomain: sinon.stub().returns(false),
},
'../DocumentUpdater/DocumentUpdaterHandler':
this.DocumentUpdaterHandler,
},
})
this.req = {
})
)
vi.doMock(
'../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
() => ({
default: ctx.DocumentUpdaterHandler,
})
)
ctx.AuthorizationMiddleware = (await import(MODULE_PATH)).default
ctx.req = {
params: {
project_id: this.project_id,
project_id: ctx.project_id,
},
body: {},
}
this.res = {
ctx.res = {
redirect: sinon.stub(),
locals: {
currentUrl: '/current/url',
},
}
this.next = sinon.stub()
ctx.next = sinon.stub()
})
describe('ensureCanReadProject', function () {
@@ -94,13 +137,13 @@ describe('AuthorizationMiddleware', function () {
})
describe('ensureUserCanDeleteOrResolveThread', function () {
beforeEach(function () {
this.req.params.thread_id = this.thread_id
beforeEach(function (ctx) {
ctx.req.params.thread_id = ctx.thread_id
})
describe('when user has permission', function () {
beforeEach(function () {
this.AuthorizationManager.promises.canUserDeleteOrResolveThread
.withArgs(this.userId, this.project_id, this.thread_id, this.token)
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserDeleteOrResolveThread
.withArgs(ctx.userId, ctx.project_id, ctx.thread_id, ctx.token)
.resolves(true)
})
@@ -109,9 +152,9 @@ describe('AuthorizationMiddleware', function () {
})
describe("when user doesn't have permission", function () {
beforeEach(function () {
this.AuthorizationManager.promises.canUserDeleteOrResolveThread
.withArgs(this.userId, this.project_id, this.thread_id, this.token)
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserDeleteOrResolveThread
.withArgs(ctx.userId, ctx.project_id, ctx.thread_id, ctx.token)
.resolves(false)
})
@@ -122,8 +165,8 @@ describe('AuthorizationMiddleware', function () {
describe('ensureUserCanWriteProjectSettings', function () {
describe('when renaming a project', function () {
beforeEach(function () {
this.req.body.name = 'new project name'
beforeEach(function (ctx) {
ctx.req.body.name = 'new project name'
})
testMiddleware(
@@ -133,8 +176,8 @@ describe('AuthorizationMiddleware', function () {
})
describe('when setting another parameter', function () {
beforeEach(function () {
this.req.body.compiler = 'texlive-2017'
beforeEach(function (ctx) {
ctx.req.body.compiler = 'texlive-2017'
})
testMiddleware(
@@ -201,18 +244,18 @@ describe('AuthorizationMiddleware', function () {
})
describe('ensureUserCanReadMultipleProjects', function () {
beforeEach(function () {
this.req.query = { project_ids: 'project1,project2' }
beforeEach(function (ctx) {
ctx.req.query = { project_ids: 'project1,project2' }
})
describe('with logged in user', function () {
describe('when user has permission to access all projects', function () {
beforeEach(function () {
this.AuthorizationManager.promises.canUserReadProject
.withArgs(this.userId, 'project1', this.token)
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project1', ctx.token)
.resolves(true)
this.AuthorizationManager.promises.canUserReadProject
.withArgs(this.userId, 'project2', this.token)
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project2', ctx.token)
.resolves(true)
})
@@ -221,12 +264,12 @@ describe('AuthorizationMiddleware', function () {
})
describe("when user doesn't have permission to access one of the projects", function () {
beforeEach(function () {
this.AuthorizationManager.promises.canUserReadProject
.withArgs(this.userId, 'project1', this.token)
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project1', ctx.token)
.resolves(true)
this.AuthorizationManager.promises.canUserReadProject
.withArgs(this.userId, 'project2', this.token)
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project2', ctx.token)
.resolves(false)
})
@@ -238,12 +281,12 @@ describe('AuthorizationMiddleware', function () {
describe('with oauth user', function () {
setupOAuthUser()
beforeEach(function () {
this.AuthorizationManager.promises.canUserReadProject
.withArgs(this.userId, 'project1', this.token)
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project1', ctx.token)
.resolves(true)
this.AuthorizationManager.promises.canUserReadProject
.withArgs(this.userId, 'project2', this.token)
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project2', ctx.token)
.resolves(true)
})
@@ -256,12 +299,12 @@ describe('AuthorizationMiddleware', function () {
describe('when user has permission', function () {
describe('when user has permission to access all projects', function () {
beforeEach(function () {
this.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project1', this.token)
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project1', ctx.token)
.resolves(true)
this.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project2', this.token)
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project2', ctx.token)
.resolves(true)
})
@@ -270,12 +313,12 @@ describe('AuthorizationMiddleware', function () {
})
describe("when user doesn't have permission to access one of the projects", function () {
beforeEach(function () {
this.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project1', this.token)
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project1', ctx.token)
.resolves(true)
this.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project2', this.token)
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project2', ctx.token)
.resolves(false)
})
@@ -350,99 +393,101 @@ function testMiddleware(middleware, permission) {
}
function setupAnonymousUser() {
beforeEach('set up anonymous user', function () {
this.SessionManager.getLoggedInUserId.returns(null)
this.SessionManager.isUserLoggedIn.returns(false)
beforeEach(function (ctx) {
ctx.SessionManager.getLoggedInUserId.returns(null)
ctx.SessionManager.isUserLoggedIn.returns(false)
})
}
function setupOAuthUser() {
beforeEach('set up oauth user', function () {
this.SessionManager.getLoggedInUserId.returns(null)
this.req.oauth_user = { _id: this.userId }
beforeEach(function (ctx) {
ctx.SessionManager.getLoggedInUserId.returns(null)
ctx.req.oauth_user = { _id: ctx.userId }
})
}
function setupPermission(permission, value) {
beforeEach(`set permission ${permission} to ${value}`, function () {
this.AuthorizationManager.promises[permission]
.withArgs(this.userId, this.project_id, this.token)
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises[permission]
.withArgs(ctx.userId, ctx.project_id, ctx.token)
.resolves(value)
})
}
function setupAnonymousPermission(permission, value) {
beforeEach(`set anonymous permission ${permission} to ${value}`, function () {
this.AuthorizationManager.promises[permission]
.withArgs(null, this.project_id, this.token)
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises[permission]
.withArgs(null, ctx.project_id, ctx.token)
.resolves(value)
})
}
function setupSiteAdmin(value) {
beforeEach(`set site admin to ${value}`, function () {
this.AuthorizationManager.promises.isUserSiteAdmin
.withArgs(this.userId)
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.isUserSiteAdmin
.withArgs(ctx.userId)
.resolves(value)
})
}
function setupMissingProjectId() {
beforeEach('set up missing project id', function () {
delete this.req.params.project_id
beforeEach(function (ctx) {
delete ctx.req.params.project_id
})
}
function setupMalformedProjectId() {
beforeEach('set up malformed project id', function () {
this.req.params = { project_id: 'bad-project-id' }
beforeEach(function (ctx) {
ctx.req.params = { project_id: 'bad-project-id' }
})
}
function invokeMiddleware(method) {
beforeEach(`invoke ${method}`, function (done) {
this.next.callsFake(() => done())
this.HttpErrorHandler.forbidden.callsFake(() => done())
this.res.redirect.callsFake(() => done())
this.AuthorizationMiddleware[method](this.req, this.res, this.next)
beforeEach(async function (ctx) {
await new Promise(resolve => {
ctx.next.callsFake(() => resolve())
ctx.HttpErrorHandler.forbidden.callsFake(() => resolve())
ctx.res.redirect.callsFake(() => resolve())
ctx.AuthorizationMiddleware[method](ctx.req, ctx.res, ctx.next)
})
})
}
function expectNext() {
it('calls the next middleware', function () {
expect(this.next).to.have.been.calledWithExactly()
it('calls the next middleware', function (ctx) {
expect(ctx.next).to.have.been.calledWithExactly()
})
}
function expectError() {
it('calls the error middleware', function () {
expect(this.next).to.have.been.calledWith(sinon.match.instanceOf(Error))
it('calls the error middleware', function (ctx) {
expect(ctx.next).to.have.been.calledWith(sinon.match.instanceOf(Error))
})
}
function expectNotFound() {
it('raises a 404', function () {
expect(this.next).to.have.been.calledWith(
it('raises a 404', function (ctx) {
expect(ctx.next).to.have.been.calledWith(
sinon.match.instanceOf(Errors.NotFoundError)
)
})
}
function expectForbidden() {
it('raises a 403', function () {
expect(this.HttpErrorHandler.forbidden).to.have.been.calledWith(
this.req,
this.res
it('raises a 403', function (ctx) {
expect(ctx.HttpErrorHandler.forbidden).to.have.been.calledWith(
ctx.req,
ctx.res
)
expect(this.next).not.to.have.been.called
expect(ctx.next).not.to.have.been.called
})
}
function expectRedirectToRestricted() {
it('redirects to restricted', function () {
expect(this.res.redirect).to.have.been.calledWith(
it('redirects to restricted', function (ctx) {
expect(ctx.res.redirect).to.have.been.calledWith(
'/restricted?from=%2Fcurrent%2Furl'
)
expect(this.next).not.to.have.been.called
expect(ctx.next).not.to.have.been.called
})
}
@@ -1,29 +1,12 @@
/* eslint-disable
n/handle-callback-err,
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 { expect } = require('chai')
const SandboxedModule = require('sandboxed-module')
const assert = require('assert')
const path = require('path')
const sinon = require('sinon')
const modulePath = path.join(
__dirname,
'../../../../app/src/Features/BrandVariations/BrandVariationsHandler'
)
import { vi, expect } from 'vitest'
import sinon from 'sinon'
const modulePath =
'../../../../app/src/Features/BrandVariations/BrandVariationsHandler.mjs'
describe('BrandVariationsHandler', function () {
beforeEach(function () {
this.settings = {
beforeEach(async function (ctx) {
ctx.settings = {
apis: {
v1: {
publicUrl: 'http://overleaf.example.com',
@@ -40,14 +23,18 @@ describe('BrandVariationsHandler', function () {
},
},
}
this.V1Api = { request: sinon.stub() }
this.BrandVariationsHandler = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/settings': this.settings,
'../V1/V1Api': this.V1Api,
},
})
return (this.mockedBrandVariationDetails = {
ctx.V1Api = { request: sinon.stub() }
vi.doMock('@overleaf/settings', () => ({
default: ctx.settings,
}))
vi.doMock('../../../../app/src/Features/V1/V1Api', () => ({
default: ctx.V1Api,
}))
ctx.BrandVariationsHandler = (await import(modulePath)).default
ctx.mockedBrandVariationDetails = {
id: '12',
active: true,
brand_name: 'The journal',
@@ -55,81 +42,81 @@ describe('BrandVariationsHandler', function () {
journal_cover_url: 'http://my.cdn.tld/journal-cover.jpg',
home_url: 'http://www.thejournal.com/',
publish_menu_link_html: 'Submit your paper to the <em>The Journal</em>',
})
}
})
describe('getBrandVariationById', function () {
it('should reject with an error when the branding variation id is not provided', async function () {
it('should reject with an error when the branding variation id is not provided', async function (ctx) {
await expect(
this.BrandVariationsHandler.promises.getBrandVariationById(null)
ctx.BrandVariationsHandler.promises.getBrandVariationById(null)
).to.be.rejected
})
it('should reject with an error when the request errors', async function () {
this.V1Api.request.callsArgWith(1, new Error())
it('should reject with an error when the request errors', async function (ctx) {
ctx.V1Api.request.callsArgWith(1, new Error())
await expect(
this.BrandVariationsHandler.promises.getBrandVariationById('12')
ctx.BrandVariationsHandler.promises.getBrandVariationById('12')
).to.be.rejected
})
it('should return branding details when request succeeds', async function () {
this.V1Api.request.callsArgWith(
it('should return branding details when request succeeds', async function (ctx) {
ctx.V1Api.request.callsArgWith(
1,
null,
{ statusCode: 200 },
this.mockedBrandVariationDetails
ctx.mockedBrandVariationDetails
)
const brandVariationDetails =
await this.BrandVariationsHandler.promises.getBrandVariationById('12')
await ctx.BrandVariationsHandler.promises.getBrandVariationById('12')
expect(brandVariationDetails).to.deep.equal(
this.mockedBrandVariationDetails
ctx.mockedBrandVariationDetails
)
})
it('should transform relative URLs in v1 absolute ones', async function () {
this.mockedBrandVariationDetails.logo_url = '/journal-logo.png'
this.V1Api.request.callsArgWith(
it('should transform relative URLs in v1 absolute ones', async function (ctx) {
ctx.mockedBrandVariationDetails.logo_url = '/journal-logo.png'
ctx.V1Api.request.callsArgWith(
1,
null,
{ statusCode: 200 },
this.mockedBrandVariationDetails
ctx.mockedBrandVariationDetails
)
const brandVariationDetails =
await this.BrandVariationsHandler.promises.getBrandVariationById('12')
await ctx.BrandVariationsHandler.promises.getBrandVariationById('12')
expect(
brandVariationDetails.logo_url.startsWith(
this.settings.apis.v1.publicUrl
ctx.settings.apis.v1.publicUrl
)
).to.be.true
})
it("should sanitize 'submit_button_html'", async function () {
this.mockedBrandVariationDetails.submit_button_html =
it("should sanitize 'submit_button_html'", async function (ctx) {
ctx.mockedBrandVariationDetails.submit_button_html =
'<br class="break"/><strong style="color:#B39500">AGU Journal</strong><iframe>hello</iframe>'
this.V1Api.request.callsArgWith(
ctx.V1Api.request.callsArgWith(
1,
null,
{ statusCode: 200 },
this.mockedBrandVariationDetails
ctx.mockedBrandVariationDetails
)
const brandVariationDetails =
await this.BrandVariationsHandler.promises.getBrandVariationById('12')
await ctx.BrandVariationsHandler.promises.getBrandVariationById('12')
expect(brandVariationDetails.submit_button_html).to.equal(
'<br /><strong style="color:#B39500">AGU Journal</strong>hello'
)
})
it("should sanitize and remove breaks in 'submit_button_html_no_br'", async function () {
this.mockedBrandVariationDetails.submit_button_html =
it("should sanitize and remove breaks in 'submit_button_html_no_br'", async function (ctx) {
ctx.mockedBrandVariationDetails.submit_button_html =
'Submit to<br class="break"/><strong style="color:#B39500">AGU Journal</strong><iframe>hello</iframe>'
this.V1Api.request.callsArgWith(
ctx.V1Api.request.callsArgWith(
1,
null,
{ statusCode: 200 },
this.mockedBrandVariationDetails
ctx.mockedBrandVariationDetails
)
const brandVariationDetails =
await this.BrandVariationsHandler.promises.getBrandVariationById('12')
await ctx.BrandVariationsHandler.promises.getBrandVariationById('12')
expect(brandVariationDetails.submit_button_html_no_br).to.equal(
'Submit to <strong style="color:#B39500">AGU Journal</strong>hello'
)
@@ -141,7 +141,7 @@ describe('CollaboratorsController', function () {
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectAuditLogHandler.js',
'../../../../app/src/Features/Project/ProjectAuditLogHandler.mjs',
() => ({
default: ctx.ProjectAuditLogHandler,
})
@@ -130,7 +130,7 @@ describe('CollaboratorsInviteController', function () {
}))
vi.doMock(
'../../../../app/src/Features/Project/ProjectAuditLogHandler.js',
'../../../../app/src/Features/Project/ProjectAuditLogHandler.mjs',
() => ({
default: ctx.ProjectAuditLogHandler,
})
@@ -148,7 +148,7 @@ describe('CollaboratorsInviteHandler', function () {
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectAuditLogHandler.js',
'../../../../app/src/Features/Project/ProjectAuditLogHandler.mjs',
() => ({
default: ctx.ProjectAuditLogHandler,
})
@@ -109,7 +109,7 @@ describe('OwnershipTransferHandler', function () {
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectAuditLogHandler.js',
'../../../../app/src/Features/Project/ProjectAuditLogHandler.mjs',
() => ({
default: ctx.ProjectAuditLogHandler,
})
File diff suppressed because it is too large Load Diff
@@ -1,972 +0,0 @@
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath = '../../../../app/src/Features/Compile/CompileController.js'
const SandboxedModule = require('sandboxed-module')
const MockRequest = require('../helpers/MockRequest')
const MockResponse = require('../helpers/MockResponse')
const { Headers } = require('node-fetch')
const { ReadableString } = require('@overleaf/stream-utils')
describe('CompileController', function () {
beforeEach(function () {
this.user_id = 'wat'
this.user = {
_id: this.user_id,
email: 'user@example.com',
features: {
compileGroup: 'premium',
compileTimeout: 100,
},
}
this.CompileManager = {
promises: {
compile: sinon.stub(),
getProjectCompileLimits: sinon.stub(),
syncTeX: sinon.stub(),
},
}
this.ClsiManager = {
promises: {},
}
this.UserGetter = { getUser: sinon.stub() }
this.rateLimiter = {
consume: sinon.stub().resolves(),
}
this.RateLimiter = {
RateLimiter: sinon.stub().returns(this.rateLimiter),
}
this.settings = {
apis: {
clsi: {
url: 'http://clsi.example.com',
submissionBackendClass: 'n2d',
},
clsi_priority: {
url: 'http://clsi-priority.example.com',
},
},
defaultFeatures: {
compileGroup: 'standard',
compileTimeout: 60,
},
clsiCookie: {
key: 'cookie-key',
},
}
this.ClsiCookieManager = {
promises: {
getServerId: sinon.stub().resolves('clsi-server-id-from-redis'),
},
}
this.SessionManager = {
getLoggedInUserId: sinon.stub().returns(this.user_id),
getSessionUser: sinon.stub().returns(this.user),
isUserLoggedIn: sinon.stub().returns(true),
}
this.pipeline = sinon.stub().callsFake(async (stream, res) => {
if (res.callback) res.callback()
})
this.clsiStream = new ReadableString('{}')
this.clsiResponse = {
headers: new Headers({
'Content-Length': '2',
'Content-Type': 'application/json',
}),
}
this.fetchUtils = {
fetchStreamWithResponse: sinon.stub().resolves({
stream: this.clsiStream,
response: this.clsiResponse,
}),
}
this.CompileController = SandboxedModule.require(modulePath, {
requires: {
'stream/promises': { pipeline: this.pipeline },
'@overleaf/settings': this.settings,
'@overleaf/fetch-utils': this.fetchUtils,
'../Project/ProjectGetter': (this.ProjectGetter = {
promises: {},
}),
'@overleaf/metrics': (this.Metrics = {
inc: sinon.stub(),
Timer: class {
constructor() {
this.labels = {}
}
done() {}
},
}),
// TODO: remove this once we remove Joi/Celebrate
celebrate: (this.celebrate = {
celebrate: sinon.stub(),
errors: sinon.stub(),
Joi: {
any: sinon.stub(),
extend: sinon.stub(),
},
}),
'./CompileManager': this.CompileManager,
'../User/UserGetter': this.UserGetter,
'./ClsiManager': this.ClsiManager,
'../Authentication/SessionManager': this.SessionManager,
'../../infrastructure/RateLimiter': this.RateLimiter,
'./ClsiCookieManager': () => this.ClsiCookieManager,
'../SplitTests/SplitTestHandler': {
getAssignment: (this.getAssignment = sinon.stub().yields(null, {
variant: 'default',
})),
promises: {
getAssignment: sinon.stub().resolves({
variant: 'default',
}),
},
},
'../Analytics/AnalyticsManager': {
recordEventForSession: sinon.stub(),
},
},
})
this.projectId = 'abc123def456abc123def456'
this.build_id = '18fbe9e7564-30dcb2f71250c690'
this.next = sinon.stub()
this.req = new MockRequest()
this.res = new MockResponse()
this.res = new MockResponse()
})
describe('compile', function () {
beforeEach(function () {
this.req.params = { Project_id: this.projectId }
this.req.session = {}
this.CompileManager.promises.compile = sinon.stub().resolves({
status: (this.status = 'success'),
outputFiles: (this.outputFiles = [
{
path: 'output.pdf',
url: `/project/${this.projectId}/user/${this.user_id}/build/id/output.pdf`,
type: 'pdf',
},
]),
clsiServerId: undefined,
limits: undefined,
validationProblems: undefined,
stats: undefined,
timings: undefined,
outputUrlPrefix: undefined,
buildId: this.build_id,
})
})
describe('pdfDownloadDomain', function () {
beforeEach(function () {
this.settings.pdfDownloadDomain = 'https://compiles.overleaf.test'
})
describe('when clsi does not emit zone prefix', function () {
beforeEach(async function () {
await this.CompileController.compile(this.req, this.res, this.next)
})
it('should add domain verbatim', function () {
this.res.statusCode.should.equal(200)
this.res.body.should.equal(
JSON.stringify({
status: this.status,
outputFiles: [
{
path: 'output.pdf',
url: `/project/${this.projectId}/user/${this.user_id}/build/id/output.pdf`,
type: 'pdf',
},
],
outputFilesArchive: {
path: 'output.zip',
url: `/project/${this.projectId}/user/wat/build/${this.build_id}/output/output.zip`,
type: 'zip',
},
pdfDownloadDomain: 'https://compiles.overleaf.test',
})
)
})
})
describe('when clsi emits a zone prefix', function () {
beforeEach(async function () {
this.CompileManager.promises.compile = sinon.stub().resolves({
status: (this.status = 'success'),
outputFiles: (this.outputFiles = [
{
path: 'output.pdf',
url: `/project/${this.projectId}/user/${this.user_id}/build/id/output.pdf`,
type: 'pdf',
},
]),
clsiServerId: undefined,
limits: undefined,
validationProblems: undefined,
stats: undefined,
timings: undefined,
outputUrlPrefix: '/zone/b',
buildId: this.build_id,
})
await this.CompileController.compile(this.req, this.res, this.next)
})
it('should add the zone prefix', function () {
this.res.statusCode.should.equal(200)
this.res.body.should.equal(
JSON.stringify({
status: this.status,
outputFiles: [
{
path: 'output.pdf',
url: `/project/${this.projectId}/user/${this.user_id}/build/id/output.pdf`,
type: 'pdf',
},
],
outputFilesArchive: {
path: 'output.zip',
url: `/project/${this.projectId}/user/wat/build/${this.build_id}/output/output.zip`,
type: 'zip',
},
outputUrlPrefix: '/zone/b',
pdfDownloadDomain: 'https://compiles.overleaf.test/zone/b',
})
)
})
})
})
describe('when not an auto compile', function () {
beforeEach(async function () {
await this.CompileController.compile(this.req, this.res, this.next)
})
it('should look up the user id', function () {
this.SessionManager.getLoggedInUserId
.calledWith(this.req.session)
.should.equal(true)
})
it('should do the compile without the auto compile flag', function () {
this.CompileManager.promises.compile.should.have.been.calledWith(
this.projectId,
this.user_id,
{
isAutoCompile: false,
compileFromClsiCache: false,
populateClsiCache: false,
enablePdfCaching: false,
fileLineErrors: false,
stopOnFirstError: false,
editorId: undefined,
}
)
})
it('should set the content-type of the response to application/json', function () {
this.res.type.should.equal('application/json')
})
it('should send a successful response reporting the status and files', function () {
this.res.statusCode.should.equal(200)
this.res.body.should.equal(
JSON.stringify({
status: this.status,
outputFiles: this.outputFiles,
outputFilesArchive: {
path: 'output.zip',
url: `/project/${this.projectId}/user/wat/build/${this.build_id}/output/output.zip`,
type: 'zip',
},
})
)
})
})
describe('when an auto compile', function () {
beforeEach(async function () {
this.req.query = { auto_compile: 'true' }
await this.CompileController.compile(this.req, this.res, this.next)
})
it('should do the compile with the auto compile flag', function () {
this.CompileManager.promises.compile.should.have.been.calledWith(
this.projectId,
this.user_id,
{
isAutoCompile: true,
compileFromClsiCache: false,
populateClsiCache: false,
enablePdfCaching: false,
fileLineErrors: false,
stopOnFirstError: false,
editorId: undefined,
}
)
})
})
describe('with the draft attribute', function () {
beforeEach(async function () {
this.req.body = { draft: true }
await this.CompileController.compile(this.req, this.res, this.next)
})
it('should do the compile without the draft compile flag', function () {
this.CompileManager.promises.compile.should.have.been.calledWith(
this.projectId,
this.user_id,
{
isAutoCompile: false,
compileFromClsiCache: false,
populateClsiCache: false,
enablePdfCaching: false,
draft: true,
fileLineErrors: false,
stopOnFirstError: false,
editorId: undefined,
}
)
})
})
describe('with an editor id', function () {
beforeEach(async function () {
this.req.body = { editorId: 'the-editor-id' }
await this.CompileController.compile(this.req, this.res, this.next)
})
it('should pass the editor id to the compiler', function () {
this.CompileManager.promises.compile.should.have.been.calledWith(
this.projectId,
this.user_id,
{
isAutoCompile: false,
compileFromClsiCache: false,
populateClsiCache: false,
enablePdfCaching: false,
fileLineErrors: false,
stopOnFirstError: false,
editorId: 'the-editor-id',
}
)
})
})
})
describe('compileSubmission', function () {
beforeEach(function () {
this.submission_id = 'sub-1234'
this.req.params = { submission_id: this.submission_id }
this.req.body = {}
this.ClsiManager.promises.sendExternalRequest = sinon.stub().resolves({
status: (this.status = 'success'),
outputFiles: (this.outputFiles = ['mock-output-files']),
clsiServerId: 'mock-server-id',
validationProblems: null,
})
})
it('should set the content-type of the response to application/json', async function () {
await this.CompileController.compileSubmission(
this.req,
this.res,
this.next
)
this.res.contentType.calledWith('application/json').should.equal(true)
})
it('should send a successful response reporting the status and files', async function () {
await this.CompileController.compileSubmission(
this.req,
this.res,
this.next
)
this.res.statusCode.should.equal(200)
this.res.body.should.equal(
JSON.stringify({
status: this.status,
outputFiles: this.outputFiles,
clsiServerId: 'mock-server-id',
validationProblems: null,
})
)
})
describe('with compileGroup and timeout', function () {
beforeEach(function () {
this.req.body = {
compileGroup: 'special',
timeout: 600,
}
this.CompileController.compileSubmission(this.req, this.res, this.next)
})
it('should use the supplied values', function () {
this.ClsiManager.promises.sendExternalRequest.should.have.been.calledWith(
this.submission_id,
{ compileGroup: 'special', timeout: 600 },
{ compileGroup: 'special', compileBackendClass: 'n2d', timeout: 600 }
)
})
})
describe('with other supported options but not compileGroup and timeout', function () {
beforeEach(function () {
this.req.body = {
rootResourcePath: 'main.tex',
compiler: 'lualatex',
draft: true,
check: 'validate',
}
this.CompileController.compileSubmission(this.req, this.res, this.next)
})
it('should use the other options but default values for compileGroup and timeout', function () {
this.ClsiManager.promises.sendExternalRequest.should.have.been.calledWith(
this.submission_id,
{
rootResourcePath: 'main.tex',
compiler: 'lualatex',
draft: true,
check: 'validate',
},
{
rootResourcePath: 'main.tex',
compiler: 'lualatex',
draft: true,
check: 'validate',
compileGroup: 'standard',
compileBackendClass: 'n2d',
timeout: 60,
}
)
})
})
})
describe('downloadPdf', function () {
beforeEach(function () {
this.CompileController._proxyToClsi = sinon.stub().resolves()
this.req.params = { Project_id: this.projectId }
this.project = { name: 'test namè; 1' }
this.ProjectGetter.promises.getProject = sinon
.stub()
.resolves(this.project)
})
describe('when downloading for embedding', function () {
beforeEach(async function () {
await this.CompileController.downloadPdf(this.req, this.res, this.next)
})
it('should look up the project', function () {
this.ProjectGetter.promises.getProject
.calledWith(this.projectId, { name: 1 })
.should.equal(true)
})
it('should set the content-type of the response to application/pdf', function () {
this.res.contentType.calledWith('application/pdf').should.equal(true)
})
it('should set the content-disposition header with a safe version of the project name', function () {
this.res.setContentDisposition.should.be.calledWith('inline', {
filename: 'test_namè__1.pdf',
})
})
it('should increment the pdf-downloads metric', function () {
this.Metrics.inc.calledWith('pdf-downloads').should.equal(true)
})
it('should proxy the PDF from the CLSI', function () {
this.CompileController._proxyToClsi
.calledWith(
this.projectId,
'output-file',
`/project/${this.projectId}/user/${this.user_id}/output/output.pdf`,
{},
this.req,
this.res
)
.should.equal(true)
})
})
describe('when a build-id is provided', function () {
beforeEach(async function () {
this.req.params.build_id = this.build_id
await this.CompileController.downloadPdf(this.req, this.res, this.next)
})
it('should proxy the PDF from the CLSI, with a build-id', function () {
this.CompileController._proxyToClsi
.calledWith(
this.projectId,
'output-file',
`/project/${this.projectId}/user/${this.user_id}/build/${this.build_id}/output/output.pdf`,
{},
this.req,
this.res
)
.should.equal(true)
})
})
describe('when rate-limited', function () {
beforeEach(async function () {
this.rateLimiter.consume.rejects({
msBeforeNext: 250,
remainingPoints: 0,
consumedPoints: 5,
isFirstInDuration: false,
})
})
it('should return 500', async function () {
await this.CompileController.downloadPdf(this.req, this.res, this.next)
// should it be 429 instead?
this.res.sendStatus.calledWith(500).should.equal(true)
this.CompileController._proxyToClsi.should.not.have.been.called
})
})
describe('when rate-limit errors', function () {
beforeEach(async function () {
this.rateLimiter.consume.rejects(new Error('uh oh'))
})
it('should return 500', async function () {
await this.CompileController.downloadPdf(this.req, this.res, this.next)
this.res.sendStatus.calledWith(500).should.equal(true)
this.CompileController._proxyToClsi.should.not.have.been.called
})
})
})
describe('getFileFromClsiWithoutUser', function () {
beforeEach(function () {
this.submission_id = 'sub-1234'
this.file = 'output.pdf'
this.req.params = {
submission_id: this.submission_id,
build_id: this.build_id,
file: this.file,
}
this.req.body = {}
this.expected_url = `/project/${this.submission_id}/build/${this.build_id}/output/${this.file}`
this.CompileController._proxyToClsiWithLimits = sinon.stub()
})
describe('without limits specified', function () {
beforeEach(async function () {
await this.CompileController.getFileFromClsiWithoutUser(
this.req,
this.res,
this.next
)
})
it('should proxy to CLSI with correct URL and default limits', function () {
this.CompileController._proxyToClsiWithLimits.should.have.been.calledWith(
this.submission_id,
'output-file',
this.expected_url,
{},
{ compileGroup: 'standard', compileBackendClass: 'n2d' }
)
})
})
describe('with limits specified', function () {
beforeEach(function () {
this.req.body = { compileTimeout: 600, compileGroup: 'special' }
this.CompileController.getFileFromClsiWithoutUser(
this.req,
this.res,
this.next
)
})
it('should proxy to CLSI with correct URL and specified limits', function () {
this.CompileController._proxyToClsiWithLimits.should.have.been.calledWith(
this.submission_id,
'output-file',
this.expected_url,
{},
{
compileGroup: 'special',
compileBackendClass: 'n2d',
}
)
})
})
})
describe('proxySyncCode', function () {
let file, line, column, imageName, editorId, buildId, clsiServerId
beforeEach(async function () {
this.req.params = { Project_id: this.projectId }
clsiServerId = 'clsi-1'
file = 'main.tex'
line = String(Date.now())
column = String(Date.now() + 1)
editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
buildId = '195b4a3f9e7-03e5be430a9e7796'
this.req.query = {
file,
line,
column,
editorId,
buildId,
clsiserverid: clsiServerId,
}
imageName = 'foo/bar:tag-0'
this.ProjectGetter.promises.getProject = sinon
.stub()
.resolves({ imageName })
this.CompileController._proxyToClsi = sinon.stub().resolves()
await this.CompileController.proxySyncCode(this.req, this.res, this.next)
})
it('should parse the parameters', function () {
expect(this.CompileManager.promises.syncTeX).to.have.been.calledWith(
this.projectId,
this.user_id,
{
direction: 'code',
compileFromClsiCache: false,
validatedOptions: {
file,
line,
column,
editorId,
buildId,
},
clsiServerId,
}
)
})
})
describe('proxySyncPdf', function () {
let page, h, v, imageName, editorId, buildId, clsiServerId
beforeEach(async function () {
this.req.params = { Project_id: this.projectId }
clsiServerId = 'clsi-1'
page = String(Date.now())
h = String(Math.random())
v = String(Math.random())
editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
buildId = '195b4a3f9e7-03e5be430a9e7796'
this.req.query = {
page,
h,
v,
editorId,
buildId,
clsiserverid: clsiServerId,
}
imageName = 'foo/bar:tag-1'
this.ProjectGetter.promises.getProject = sinon
.stub()
.resolves({ imageName })
this.CompileController._proxyToClsi = sinon.stub()
await this.CompileController.proxySyncPdf(this.req, this.res, this.next)
})
it('should parse the parameters', function () {
expect(this.CompileManager.promises.syncTeX).to.have.been.calledWith(
this.projectId,
this.user_id,
{
direction: 'pdf',
compileFromClsiCache: false,
validatedOptions: {
page,
h,
v,
editorId,
buildId,
},
clsiServerId,
}
)
})
})
describe('_proxyToClsi', function () {
beforeEach(function () {
this.req.method = 'mock-method'
this.req.headers = {
Mock: 'Headers',
Range: '123-456',
'If-Range': 'abcdef',
'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT',
}
})
describe('old pdf viewer', function () {
describe('user with standard priority', function () {
beforeEach(async function () {
this.CompileManager.promises.getProjectCompileLimits = sinon
.stub()
.resolves({
compileGroup: 'standard',
compileBackendClass: 'n2d',
})
await this.CompileController._proxyToClsi(
this.projectId,
'output-file',
(this.url = '/test'),
{ query: 'foo' },
this.req,
this.res,
this.next
)
})
it('should open a request to the CLSI', function () {
this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
`${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=n2d&query=foo`
)
})
it('should pass the request on to the client', function () {
this.pipeline.should.have.been.calledWith(this.clsiStream, this.res)
})
})
describe('user with priority compile', function () {
beforeEach(async function () {
this.CompileManager.promises.getProjectCompileLimits = sinon
.stub()
.resolves({
compileGroup: 'priority',
compileBackendClass: 'c2d',
})
await this.CompileController._proxyToClsi(
this.projectId,
'output-file',
(this.url = '/test'),
{},
this.req,
this.res,
this.next
)
})
it('should open a request to the CLSI', function () {
this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
`${this.settings.apis.clsi.url}${this.url}?compileGroup=priority&compileBackendClass=c2d`
)
})
})
describe('user with standard priority via query string', function () {
beforeEach(async function () {
this.req.query = { compileGroup: 'standard' }
this.CompileManager.promises.getProjectCompileLimits = sinon
.stub()
.resolves({
compileGroup: 'standard',
compileBackendClass: 'n2d',
})
await this.CompileController._proxyToClsi(
this.projectId,
'output-file',
(this.url = '/test'),
{},
this.req,
this.res,
this.next
)
})
it('should open a request to the CLSI', function () {
this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
`${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=n2d`
)
})
it('should pass the request on to the client', function () {
this.pipeline.should.have.been.calledWith(this.clsiStream, this.res)
})
})
describe('user with non-existent priority via query string', function () {
beforeEach(async function () {
this.req.query = { compileGroup: 'foobar' }
this.CompileManager.promises.getProjectCompileLimits = sinon
.stub()
.resolves({
compileGroup: 'standard',
compileBackendClass: 'n2d',
})
await this.CompileController._proxyToClsi(
this.projectId,
'output-file',
(this.url = '/test'),
{},
this.req,
this.res,
this.next
)
})
it('should proxy to the standard url', function () {
this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
`${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=n2d`
)
})
})
describe('user with build parameter via query string', function () {
beforeEach(async function () {
this.CompileManager.promises.getProjectCompileLimits = sinon
.stub()
.resolves({
compileGroup: 'standard',
compileBackendClass: 'n2d',
})
this.req.query = { build: 1234 }
await this.CompileController._proxyToClsi(
this.projectId,
'output-file',
(this.url = '/test'),
{},
this.req,
this.res,
this.next
)
})
it('should proxy to the standard url without the build parameter', function () {
this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
`${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=n2d`
)
})
})
})
})
describe('deleteAuxFiles', function () {
beforeEach(async function () {
this.CompileManager.promises.deleteAuxFiles = sinon.stub().resolves()
this.req.params = { Project_id: this.projectId }
this.req.query = { clsiserverid: 'node-1' }
this.res.sendStatus = sinon.stub()
await this.CompileController.deleteAuxFiles(this.req, this.res, this.next)
})
it('should proxy to the CLSI', function () {
this.CompileManager.promises.deleteAuxFiles
.calledWith(this.projectId, this.user_id, 'node-1')
.should.equal(true)
})
it('should return a 200', function () {
this.res.sendStatus.calledWith(200).should.equal(true)
})
})
describe('compileAndDownloadPdf', function () {
beforeEach(function () {
this.req = {
params: {
project_id: this.projectId,
},
}
this.downloadPath = `/project/${this.projectId}/build/123/output/output.pdf`
this.CompileManager.promises.compile.resolves({
status: 'success',
outputFiles: [{ path: 'output.pdf', url: this.downloadPath }],
})
this.CompileController._proxyToClsi = sinon.stub()
this.res = { send: () => {}, sendStatus: sinon.stub() }
})
it('should call compile in the compile manager', async function () {
await this.CompileController.compileAndDownloadPdf(this.req, this.res)
this.CompileManager.promises.compile
.calledWith(this.projectId)
.should.equal(true)
})
it('should proxy the res to the clsi with correct url', async function () {
await this.CompileController.compileAndDownloadPdf(this.req, this.res)
sinon.assert.calledWith(
this.CompileController._proxyToClsi,
this.projectId,
'output-file',
this.downloadPath,
{},
this.req,
this.res
)
this.CompileController._proxyToClsi
.calledWith(
this.projectId,
'output-file',
this.downloadPath,
{},
this.req,
this.res
)
.should.equal(true)
})
it('should not download anything on compilation failures', async function () {
this.CompileManager.promises.compile.rejects(new Error('failed'))
await this.CompileController.compileAndDownloadPdf(
this.req,
this.res,
this.next
)
this.res.sendStatus.should.have.been.calledWith(500)
this.CompileController._proxyToClsi.should.not.have.been.called
})
it('should not download anything on missing pdf', async function () {
this.CompileManager.promises.compile.resolves({
status: 'success',
outputFiles: [],
})
await this.CompileController.compileAndDownloadPdf(this.req, this.res)
this.res.sendStatus.should.have.been.calledWith(500)
this.CompileController._proxyToClsi.should.not.have.been.called
})
})
describe('wordCount', function () {
beforeEach(async function () {
this.CompileManager.promises.wordCount = sinon
.stub()
.resolves({ content: 'body' })
this.req.params = { Project_id: this.projectId }
this.req.query = { clsiserverid: 'node-42' }
this.res.json = sinon.stub()
this.res.contentType = sinon.stub()
await this.CompileController.wordCount(this.req, this.res, this.next)
})
it('should proxy to the CLSI', function () {
this.CompileManager.promises.wordCount
.calledWith(this.projectId, this.user_id, false, 'node-42')
.should.equal(true)
})
it('should return a 200 and body', function () {
this.res.json.calledWith({ content: 'body' }).should.equal(true)
})
})
})
@@ -0,0 +1,67 @@
import { vi, expect } from 'vitest'
const modulePath = '../../../../app/src/Features/Helpers/AuthorizationHelper'
describe('AuthorizationHelper', function () {
beforeEach(async function (ctx) {
vi.doMock('../../../../app/src/models/User', () => ({
UserSchema: {
obj: {
staffAccess: {
publisherMetrics: {},
publisherManagement: {},
institutionMetrics: {},
institutionManagement: {},
groupMetrics: {},
groupManagement: {},
adminMetrics: {},
},
},
},
}))
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: (ctx.ProjectGetter = { promises: {} }),
}))
vi.doMock(
'../../../../app/src/Features/SplitTests/SplitTestHandler',
() => ({
default: (ctx.SplitTestHandler = {
promises: {},
}),
})
)
ctx.AuthorizationHelper = (await import(modulePath)).default
})
describe('hasAnyStaffAccess', function () {
it('with empty user', function (ctx) {
const user = {}
expect(ctx.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.false
})
it('with no access user', function (ctx) {
const user = { isAdmin: false, staffAccess: { adminMetrics: false } }
expect(ctx.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.false
})
it('with admin user', function (ctx) {
const user = { isAdmin: true }
expect(ctx.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.false
})
it('with staff user', function (ctx) {
const user = { staffAccess: { adminMetrics: true, somethingElse: false } }
expect(ctx.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.true
})
it('with non-staff user with extra attributes', function (ctx) {
// make sure that staffAccess attributes not declared on the model don't
// give user access
const user = { staffAccess: { adminMetrics: false, somethingElse: true } }
expect(ctx.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.false
})
})
})
@@ -1,66 +0,0 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath = '../../../../app/src/Features/Helpers/AuthorizationHelper'
describe('AuthorizationHelper', function () {
beforeEach(function () {
this.AuthorizationHelper = SandboxedModule.require(modulePath, {
requires: {
'./AdminAuthorizationHelper': (this.AdminAuthorizationHelper = {
hasAdminAccess: sinon.stub().returns(false),
}),
'../../models/User': {
UserSchema: {
obj: {
staffAccess: {
publisherMetrics: {},
publisherManagement: {},
institutionMetrics: {},
institutionManagement: {},
groupMetrics: {},
groupManagement: {},
adminMetrics: {},
},
},
},
},
'../Project/ProjectGetter': (this.ProjectGetter = { promises: {} }),
'../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
promises: {},
}),
},
})
})
describe('hasAnyStaffAccess', function () {
it('with empty user', function () {
const user = {}
expect(this.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.false
})
it('with no access user', function () {
const user = { isAdmin: false, staffAccess: { adminMetrics: false } }
expect(this.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.false
})
it('with admin user', function () {
const user = { isAdmin: true }
this.AdminAuthorizationHelper.hasAdminAccess.returns(true)
expect(this.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.false
})
it('with staff user', function () {
const user = { staffAccess: { adminMetrics: true, somethingElse: false } }
this.AdminAuthorizationHelper.hasAdminAccess.returns(true)
expect(this.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.true
})
it('with non-staff user with extra attributes', function () {
// make sure that staffAccess attributes not declared on the model don't
// give user access
const user = { staffAccess: { adminMetrics: false, somethingElse: true } }
expect(this.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.false
})
})
})
@@ -106,9 +106,12 @@ describe('HistoryController', function () {
default: (ctx.ProjectGetter = {}),
}))
vi.doMock('../../../../app/src/Features/History/RestoreManager.js', () => ({
default: (ctx.RestoreManager = {}),
}))
vi.doMock(
'../../../../app/src/Features/History/RestoreManager.mjs',
() => ({
default: (ctx.RestoreManager = {}),
})
)
vi.doMock('../../../../app/src/infrastructure/Features.js', () => ({
default: (ctx.Features = sinon.stub().withArgs('saas').returns(true)),
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,181 @@
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import mongodb, { ReadPreference } from 'mongodb-legacy'
const { ObjectId } = mongodb
const modulePath =
'../../../../app/src/Features/InactiveData/InactiveProjectManager'
describe('InactiveProjectManager', function () {
beforeEach(async function (ctx) {
ctx.settings = {}
ctx.metrics = { inc: sinon.stub() }
ctx.DocstoreManager = {
promises: {
unarchiveProject: sinon.stub(),
archiveProject: sinon.stub(),
},
}
ctx.DocumentUpdaterHandler = {
promises: {
flushProjectToMongoAndDelete: sinon.stub(),
},
}
ctx.ProjectUpdateHandler = {
promises: {
markAsActive: sinon.stub(),
markAsInactive: sinon.stub(),
},
}
ctx.ProjectGetter = { promises: { getProject: sinon.stub() } }
ctx.Modules = { promises: { hooks: { fire: sinon.stub() } } }
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
vi.doMock('@overleaf/settings', () => ({
default: ctx.settings,
}))
vi.doMock('@overleaf/metrics', () => ({
default: ctx.metrics,
}))
vi.doMock('../../../../app/src/Features/Docstore/DocstoreManager', () => ({
default: ctx.DocstoreManager,
}))
vi.doMock(
'../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
() => ({
default: ctx.DocumentUpdaterHandler,
})
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectUpdateHandler',
() => ({
default: ctx.ProjectUpdateHandler,
})
)
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: ctx.ProjectGetter,
}))
vi.doMock('../../../../app/src/models/Project', () => ({}))
vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
default: ctx.Modules,
}))
vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({
default: {
ObjectId,
READ_PREFERENCE_SECONDARY: ReadPreference.secondaryPreferred.mode,
},
}))
ctx.InactiveProjectManager = (await import(modulePath)).default
ctx.project_id = '1234'
})
describe('reactivateProjectIfRequired', function () {
beforeEach(function (ctx) {
ctx.project = { active: false }
ctx.ProjectGetter.promises.getProject.resolves(ctx.project)
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.DocstoreManager.promises.unarchiveProject
.calledWith(ctx.project_id)
.should.equal(true)
ctx.ProjectUpdateHandler.promises.markAsActive
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should not mark project as active if error with unarchiving', async function (ctx) {
ctx.DocstoreManager.promises.unarchiveProject.rejects()
await expect(
ctx.InactiveProjectManager.promises.reactivateProjectIfRequired(
ctx.project_id
)
).to.be.rejected
ctx.DocstoreManager.promises.unarchiveProject
.calledWith(ctx.project_id)
.should.equal(true)
ctx.ProjectUpdateHandler.promises.markAsActive
.calledWith(ctx.project_id)
.should.equal(false)
})
it('should not call unarchiveProject if it is active', async function (ctx) {
ctx.project.active = true
ctx.DocstoreManager.promises.unarchiveProject.resolves()
await ctx.InactiveProjectManager.promises.reactivateProjectIfRequired(
ctx.project_id
)
ctx.DocstoreManager.promises.unarchiveProject
.calledWith(ctx.project_id)
.should.equal(false)
ctx.ProjectUpdateHandler.promises.markAsActive
.calledWith(ctx.project_id)
.should.equal(false)
})
})
describe('deactivateProject', function () {
it('should call archiveProject and markAsInactive after flushing', async function (ctx) {
ctx.DocstoreManager.promises.archiveProject.resolves()
ctx.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete.resolves()
ctx.ProjectUpdateHandler.promises.markAsInactive.resolves()
ctx.Modules.promises.hooks.fire.resolves()
await ctx.InactiveProjectManager.promises.deactivateProject(
ctx.project_id
)
ctx.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete
.calledWith(ctx.project_id)
.should.equal(true)
ctx.Modules.promises.hooks.fire
.calledWith('deactivateProject', ctx.project_id)
.should.equal(true)
ctx.DocstoreManager.promises.archiveProject
.calledWith(ctx.project_id)
.should.equal(true)
ctx.ProjectUpdateHandler.promises.markAsInactive
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should not call markAsInactive if there was a problem archiving in docstore', async function (ctx) {
ctx.DocstoreManager.promises.archiveProject.rejects()
ctx.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete.resolves()
ctx.ProjectUpdateHandler.promises.markAsInactive.resolves()
ctx.Modules.promises.hooks.fire.resolves()
await expect(
ctx.InactiveProjectManager.promises.deactivateProject(ctx.project_id)
).to.be.rejected
ctx.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete
.calledWith(ctx.project_id)
.should.equal(true)
ctx.DocstoreManager.promises.archiveProject
.calledWith(ctx.project_id)
.should.equal(true)
ctx.ProjectUpdateHandler.promises.markAsInactive
.calledWith(ctx.project_id)
.should.equal(false)
})
})
})
@@ -1,151 +0,0 @@
const SandboxedModule = require('sandboxed-module')
const path = require('path')
const sinon = require('sinon')
const modulePath = path.join(
__dirname,
'../../../../app/src/Features/InactiveData/InactiveProjectManager'
)
const { ObjectId, ReadPreference } = require('mongodb-legacy')
const { expect } = require('chai')
describe('InactiveProjectManager', function () {
beforeEach(function () {
this.settings = {}
this.metrics = { inc: sinon.stub() }
this.DocstoreManager = {
promises: {
unarchiveProject: sinon.stub(),
archiveProject: sinon.stub(),
},
}
this.DocumentUpdaterHandler = {
promises: {
flushProjectToMongoAndDelete: sinon.stub(),
},
}
this.ProjectUpdateHandler = {
promises: {
markAsActive: sinon.stub(),
markAsInactive: sinon.stub(),
},
}
this.ProjectGetter = { promises: { getProject: sinon.stub() } }
this.Modules = { promises: { hooks: { fire: sinon.stub() } } }
this.InactiveProjectManager = SandboxedModule.require(modulePath, {
requires: {
'mongodb-legacy': { ObjectId },
'@overleaf/settings': this.settings,
'@overleaf/metrics': this.metrics,
'../Docstore/DocstoreManager': this.DocstoreManager,
'../DocumentUpdater/DocumentUpdaterHandler':
this.DocumentUpdaterHandler,
'../Project/ProjectUpdateHandler': this.ProjectUpdateHandler,
'../Project/ProjectGetter': this.ProjectGetter,
'../../models/Project': {},
'../../infrastructure/Modules': this.Modules,
'../../infrastructure/mongodb': {
ObjectId,
READ_PREFERENCE_SECONDARY: ReadPreference.secondaryPreferred.mode,
},
},
})
this.project_id = '1234'
})
describe('reactivateProjectIfRequired', function () {
beforeEach(function () {
this.project = { active: false }
this.ProjectGetter.promises.getProject.resolves(this.project)
this.ProjectUpdateHandler.promises.markAsActive.resolves()
})
it('should call unarchiveProject', async function () {
this.DocstoreManager.promises.unarchiveProject.resolves()
await this.InactiveProjectManager.promises.reactivateProjectIfRequired(
this.project_id
)
this.DocstoreManager.promises.unarchiveProject
.calledWith(this.project_id)
.should.equal(true)
this.ProjectUpdateHandler.promises.markAsActive
.calledWith(this.project_id)
.should.equal(true)
})
it('should not mark project as active if error with unarchiving', async function () {
this.DocstoreManager.promises.unarchiveProject.rejects()
await expect(
this.InactiveProjectManager.promises.reactivateProjectIfRequired(
this.project_id
)
).to.be.rejected
this.DocstoreManager.promises.unarchiveProject
.calledWith(this.project_id)
.should.equal(true)
this.ProjectUpdateHandler.promises.markAsActive
.calledWith(this.project_id)
.should.equal(false)
})
it('should not call unarchiveProject if it is active', async function () {
this.project.active = true
this.DocstoreManager.promises.unarchiveProject.resolves()
await this.InactiveProjectManager.promises.reactivateProjectIfRequired(
this.project_id
)
this.DocstoreManager.promises.unarchiveProject
.calledWith(this.project_id)
.should.equal(false)
this.ProjectUpdateHandler.promises.markAsActive
.calledWith(this.project_id)
.should.equal(false)
})
})
describe('deactivateProject', function () {
it('should call archiveProject and markAsInactive after flushing', async function () {
this.DocstoreManager.promises.archiveProject.resolves()
this.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete.resolves()
this.ProjectUpdateHandler.promises.markAsInactive.resolves()
this.Modules.promises.hooks.fire.resolves()
await this.InactiveProjectManager.promises.deactivateProject(
this.project_id
)
this.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete
.calledWith(this.project_id)
.should.equal(true)
this.Modules.promises.hooks.fire
.calledWith('deactivateProject', this.project_id)
.should.equal(true)
this.DocstoreManager.promises.archiveProject
.calledWith(this.project_id)
.should.equal(true)
this.ProjectUpdateHandler.promises.markAsInactive
.calledWith(this.project_id)
.should.equal(true)
})
it('should not call markAsInactive if there was a problem archiving in docstore', async function () {
this.DocstoreManager.promises.archiveProject.rejects()
this.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete.resolves()
this.ProjectUpdateHandler.promises.markAsInactive.resolves()
this.Modules.promises.hooks.fire.resolves()
await expect(
this.InactiveProjectManager.promises.deactivateProject(this.project_id)
).to.be.rejected
this.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete
.calledWith(this.project_id)
.should.equal(true)
this.DocstoreManager.promises.archiveProject
.calledWith(this.project_id)
.should.equal(true)
this.ProjectUpdateHandler.promises.markAsInactive
.calledWith(this.project_id)
.should.equal(false)
})
})
})
@@ -0,0 +1,461 @@
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'
const { ObjectId } = mongodb
const MODULE_PATH = '../../../../app/src/Features/Project/ProjectDuplicator.mjs'
describe('ProjectDuplicator', function () {
beforeEach(async function (ctx) {
ctx.doc0 = { _id: 'doc0_id', name: 'rootDocHere' }
ctx.doc1 = { _id: 'doc1_id', name: 'level1folderDocName' }
ctx.doc2 = { _id: 'doc2_id', name: 'level2folderDocName' }
ctx.doc0Lines = ['zero']
ctx.doc1Lines = ['one']
ctx.doc2Lines = ['two']
ctx.file0 = { name: 'file0', _id: 'file0', hash: 'abcde' }
ctx.file1 = { name: 'file1', _id: 'file1', hash: 'fffff' }
ctx.file2 = {
name: 'file2',
_id: 'file2',
created: '2024-07-05T14:18:31.401+00:00',
linkedFileData: { provider: 'url' },
hash: '123456',
}
ctx.level2folder = {
name: 'level2folderName',
_id: 'level2folderId',
docs: [ctx.doc2, undefined],
folders: [],
fileRefs: [ctx.file2],
}
ctx.level1folder = {
name: 'level1folder',
_id: 'level1folderId',
docs: [ctx.doc1],
folders: [ctx.level2folder],
fileRefs: [ctx.file1, null], // the null is intentional to test null docs/files
}
ctx.rootFolder = {
name: 'rootFolder',
_id: 'rootFolderId',
docs: [ctx.doc0],
folders: [ctx.level1folder, {}],
fileRefs: [ctx.file0],
}
ctx.project = {
_id: 'this_is_the_old_project_id',
rootDoc_id: ctx.doc0._id,
rootFolder: [ctx.rootFolder],
compiler: 'this_is_a_Compiler',
overleaf: { history: { id: 123456 } },
}
ctx.doc0Path = '/rootDocHere'
ctx.doc1Path = '/level1folder/level1folderDocName'
ctx.doc2Path = '/level1folder/level2folderName/level2folderDocName'
ctx.file0Path = '/file0'
ctx.file1Path = '/level1folder/file1'
ctx.file2Path = '/level1folder/level2folderName/file2'
ctx.docContents = [
{ _id: ctx.doc0._id, lines: ctx.doc0Lines },
{ _id: ctx.doc1._id, lines: ctx.doc1Lines },
{ _id: ctx.doc2._id, lines: ctx.doc2Lines },
]
ctx.rootDoc = ctx.doc0
ctx.rootDocPath = '/rootDocHere'
ctx.owner = { _id: 'this_is_the_owner' }
ctx.newBlankProject = {
_id: 'new_project_id',
overleaf: { history: { id: 339123 } },
readOnly_refs: [],
collaberator_refs: [],
rootFolder: [{ _id: 'new_root_folder_id' }],
}
ctx.newFolder = { _id: 'newFolderId' }
ctx.filestoreUrl = 'filestore-url'
ctx.newProjectVersion = 2
ctx.newDocId = new ObjectId()
ctx.newFileId = new ObjectId()
ctx.newDoc0 = { ...ctx.doc0, _id: ctx.newDocId }
ctx.newDoc1 = { ...ctx.doc1, _id: ctx.newDocId }
ctx.newDoc2 = { ...ctx.doc2, _id: ctx.newDocId }
ctx.newFile0 = { ...ctx.file0, _id: ctx.newFileId }
ctx.newFile1 = { ...ctx.file1, _id: ctx.newFileId }
ctx.newFile2 = { ...ctx.file2, _id: ctx.newFileId }
ctx.docEntries = [
{
path: ctx.doc0Path,
doc: ctx.newDoc0,
docLines: ctx.doc0Lines.join('\n'),
},
{
path: ctx.doc1Path,
doc: ctx.newDoc1,
docLines: ctx.doc1Lines.join('\n'),
},
{
path: ctx.doc2Path,
doc: ctx.newDoc2,
docLines: ctx.doc2Lines.join('\n'),
},
]
ctx.fileEntries = [
{
createdBlob: true,
path: ctx.file0Path,
file: ctx.newFile0,
},
{
createdBlob: true,
path: ctx.file1Path,
file: ctx.newFile1,
},
{
createdBlob: true,
path: ctx.file2Path,
file: ctx.newFile2,
},
]
ctx.Doc = sinon.stub().callsFake(props => ({ _id: ctx.newDocId, ...props }))
ctx.File = sinon
.stub()
.callsFake(props => ({ _id: ctx.newFileId, ...props }))
ctx.DocstoreManager = {
promises: {
updateDoc: sinon.stub().resolves(),
getAllDocs: sinon.stub().resolves(ctx.docContents),
},
}
ctx.DocumentUpdaterHandler = {
promises: {
flushProjectToMongo: sinon.stub().resolves(),
updateProjectStructure: sinon.stub().resolves(),
},
}
ctx.HistoryManager = {
promises: {
copyBlob: sinon.stub().callsFake((historyId, newHistoryId, hash) => {
if (hash === '500') {
return Promise.reject(new Error('copy blob error'))
}
return Promise.resolve()
}),
},
}
ctx.TagsHandler = {
promises: {
addProjectToTags: sinon.stub().resolves({
_id: 'project-1',
}),
countTagsForProject: sinon.stub().resolves(1),
},
}
ctx.ProjectCreationHandler = {
promises: {
createBlankProject: sinon.stub().resolves(ctx.newBlankProject),
},
}
ctx.ProjectDeleter = {
promises: {
deleteProject: sinon.stub().resolves(),
},
}
ctx.ProjectEntityMongoUpdateHandler = {
promises: {
createNewFolderStructure: sinon.stub().resolves(ctx.newProjectVersion),
},
}
ctx.ProjectEntityUpdateHandler = {
isPathValidForRootDoc: sinon.stub().returns(true),
promises: {
setRootDoc: sinon.stub().resolves(),
},
}
ctx.ProjectGetter = {
promises: {
getProject: sinon
.stub()
.withArgs(ctx.project._id)
.resolves(ctx.project),
},
}
ctx.ProjectLocator = {
promises: {
findRootDoc: sinon.stub().resolves({
element: ctx.rootDoc,
path: { fileSystem: ctx.rootDocPath },
}),
findElementByPath: sinon
.stub()
.withArgs({
project_id: ctx.newBlankProject._id,
path: ctx.rootDocPath,
exactCaseMatch: true,
})
.resolves({ element: ctx.doc0 }),
},
}
ctx.ProjectOptionsHandler = {
promises: {
setCompiler: sinon.stub().resolves(),
},
}
ctx.TpdsProjectFlusher = {
promises: {
flushProjectToTpds: sinon.stub().resolves(),
},
}
vi.doMock('../../../../app/src/models/Doc', () => ({
Doc: ctx.Doc,
}))
vi.doMock('../../../../app/src/models/File', () => ({
File: ctx.File,
}))
vi.doMock('../../../../app/src/Features/Docstore/DocstoreManager', () => ({
default: ctx.DocstoreManager,
}))
vi.doMock(
'../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
() => ({
default: ctx.DocumentUpdaterHandler,
})
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectCreationHandler',
() => ({
default: ctx.ProjectCreationHandler,
})
)
vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({
default: ctx.ProjectDeleter,
}))
vi.doMock(
'../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler',
() => ({
default: ctx.ProjectEntityMongoUpdateHandler,
})
)
vi.doMock(
'../../../../app/src/Features/Project/ProjectEntityUpdateHandler',
() => ({
default: ctx.ProjectEntityUpdateHandler,
})
)
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: ctx.ProjectGetter,
}))
vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({
default: ctx.ProjectLocator,
}))
vi.doMock(
'../../../../app/src/Features/Project/ProjectOptionsHandler',
() => ({
default: ctx.ProjectOptionsHandler,
})
)
vi.doMock(
'../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher',
() => ({
default: ctx.TpdsProjectFlusher,
})
)
vi.doMock('../../../../app/src/Features/Tags/TagsHandler', () => ({
default: ctx.TagsHandler,
}))
vi.doMock('../../../../app/src/Features/History/HistoryManager', () => ({
default: ctx.HistoryManager,
}))
vi.doMock('../../../../app/src/Features/Compile/ClsiCacheManager', () => ({
default: {
prepareClsiCache: sinon.stub().rejects(new Error('ignore this')),
},
}))
ctx.ProjectDuplicator = (await import(MODULE_PATH)).default
})
describe('when the copy succeeds', function () {
beforeEach(async function (ctx) {
ctx.newProjectName = 'New project name'
ctx.newProject = await ctx.ProjectDuplicator.promises.duplicate(
ctx.owner,
ctx.project._id,
ctx.newProjectName
)
})
it('should flush the original project to mongo', function (ctx) {
ctx.DocumentUpdaterHandler.promises.flushProjectToMongo.should.have.been.calledWith(
ctx.project._id
)
})
it('should copy docs to docstore', function (ctx) {
for (const docLines of [ctx.doc0Lines, ctx.doc1Lines, ctx.doc2Lines]) {
ctx.DocstoreManager.promises.updateDoc.should.have.been.calledWith(
ctx.newProject._id.toString(),
ctx.newDocId.toString(),
docLines,
0,
{}
)
}
})
it('should duplicate the files with hashes by copying the blobs in history v1', function (ctx) {
for (const file of [ctx.file0, ctx.file1, ctx.file2]) {
ctx.HistoryManager.promises.copyBlob.should.have.been.calledWith(
ctx.project.overleaf.history.id,
ctx.newProject.overleaf.history.id,
file.hash
)
}
})
it('should create a blank project', function (ctx) {
ctx.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
ctx.owner._id,
ctx.newProjectName
)
ctx.newProject._id.should.equal(ctx.newBlankProject._id)
})
it('should use the same compiler', function (ctx) {
ctx.ProjectOptionsHandler.promises.setCompiler.should.have.been.calledWith(
ctx.newProject._id,
ctx.project.compiler
)
})
it('should use the same root doc', function (ctx) {
ctx.ProjectEntityUpdateHandler.promises.setRootDoc.should.have.been.calledWith(
ctx.newProject._id,
ctx.rootFolder.docs[0]._id
)
})
it('should not copy the collaborators or read only refs', function (ctx) {
ctx.newProject.collaberator_refs.length.should.equal(0)
ctx.newProject.readOnly_refs.length.should.equal(0)
})
it('should copy all documents and files', function (ctx) {
ctx.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
ctx.newProject._id,
ctx.docEntries,
ctx.fileEntries
)
})
it('should notify document updater of changes', function (ctx) {
ctx.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
ctx.newProject._id,
ctx.newProject.overleaf.history.id,
ctx.owner._id,
{
newDocs: ctx.docEntries,
newFiles: ctx.fileEntries,
newProject: { version: ctx.newProjectVersion },
},
null
)
})
it('should flush the project to TPDS', function (ctx) {
ctx.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
ctx.newProject._id
)
})
})
describe('without a root doc', function () {
beforeEach(async function (ctx) {
ctx.ProjectLocator.promises.findRootDoc.resolves({
element: null,
path: null,
})
ctx.newProject = await ctx.ProjectDuplicator.promises.duplicate(
ctx.owner,
ctx.project._id,
'Copy of project'
)
})
it('should not set the root doc on the copy', function (ctx) {
ctx.ProjectEntityUpdateHandler.promises.setRootDoc.should.not.have.been
.called
})
})
describe('with an invalid root doc', function () {
beforeEach(async function (ctx) {
ctx.ProjectEntityUpdateHandler.isPathValidForRootDoc.returns(false)
ctx.newProject = await ctx.ProjectDuplicator.promises.duplicate(
ctx.owner,
ctx.project._id,
'Copy of project'
)
})
it('should not set the root doc on the copy', function (ctx) {
ctx.ProjectEntityUpdateHandler.promises.setRootDoc.should.not.have.been
.called
})
})
describe('when cloning in history-v1 fails', function () {
it('should fail the clone operation', async function (ctx) {
ctx.file0.hash = '500'
await expect(
ctx.ProjectDuplicator.promises.duplicate(
ctx.owner,
ctx.project._id,
'name'
)
).to.be.rejectedWith('copy blob error')
})
})
describe('when there is an error', function () {
beforeEach(async function (ctx) {
ctx.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.rejects()
await expect(
ctx.ProjectDuplicator.promises.duplicate(ctx.owner, ctx.project._id, '')
).to.be.rejected
})
it('should delete the broken cloned project', function (ctx) {
ctx.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
ctx.newBlankProject._id
)
})
it('should not delete the original project', function (ctx) {
ctx.ProjectDeleter.promises.deleteProject.should.not.have.been.calledWith(
ctx.project._id
)
})
})
})
@@ -1,408 +0,0 @@
const { expect } = require('chai')
const sinon = require('sinon')
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb-legacy')
const MODULE_PATH = '../../../../app/src/Features/Project/ProjectDuplicator.js'
describe('ProjectDuplicator', function () {
beforeEach(function () {
this.doc0 = { _id: 'doc0_id', name: 'rootDocHere' }
this.doc1 = { _id: 'doc1_id', name: 'level1folderDocName' }
this.doc2 = { _id: 'doc2_id', name: 'level2folderDocName' }
this.doc0Lines = ['zero']
this.doc1Lines = ['one']
this.doc2Lines = ['two']
this.file0 = { name: 'file0', _id: 'file0', hash: 'abcde' }
this.file1 = { name: 'file1', _id: 'file1', hash: 'fffff' }
this.file2 = {
name: 'file2',
_id: 'file2',
created: '2024-07-05T14:18:31.401+00:00',
linkedFileData: { provider: 'url' },
hash: '123456',
}
this.level2folder = {
name: 'level2folderName',
_id: 'level2folderId',
docs: [this.doc2, undefined],
folders: [],
fileRefs: [this.file2],
}
this.level1folder = {
name: 'level1folder',
_id: 'level1folderId',
docs: [this.doc1],
folders: [this.level2folder],
fileRefs: [this.file1, null], // the null is intentional to test null docs/files
}
this.rootFolder = {
name: 'rootFolder',
_id: 'rootFolderId',
docs: [this.doc0],
folders: [this.level1folder, {}],
fileRefs: [this.file0],
}
this.project = {
_id: 'this_is_the_old_project_id',
rootDoc_id: this.doc0._id,
rootFolder: [this.rootFolder],
compiler: 'this_is_a_Compiler',
overleaf: { history: { id: 123456 } },
}
this.doc0Path = '/rootDocHere'
this.doc1Path = '/level1folder/level1folderDocName'
this.doc2Path = '/level1folder/level2folderName/level2folderDocName'
this.file0Path = '/file0'
this.file1Path = '/level1folder/file1'
this.file2Path = '/level1folder/level2folderName/file2'
this.docContents = [
{ _id: this.doc0._id, lines: this.doc0Lines },
{ _id: this.doc1._id, lines: this.doc1Lines },
{ _id: this.doc2._id, lines: this.doc2Lines },
]
this.rootDoc = this.doc0
this.rootDocPath = '/rootDocHere'
this.owner = { _id: 'this_is_the_owner' }
this.newBlankProject = {
_id: 'new_project_id',
overleaf: { history: { id: 339123 } },
readOnly_refs: [],
collaberator_refs: [],
rootFolder: [{ _id: 'new_root_folder_id' }],
}
this.newFolder = { _id: 'newFolderId' }
this.filestoreUrl = 'filestore-url'
this.newProjectVersion = 2
this.newDocId = new ObjectId()
this.newFileId = new ObjectId()
this.newDoc0 = { ...this.doc0, _id: this.newDocId }
this.newDoc1 = { ...this.doc1, _id: this.newDocId }
this.newDoc2 = { ...this.doc2, _id: this.newDocId }
this.newFile0 = { ...this.file0, _id: this.newFileId }
this.newFile1 = { ...this.file1, _id: this.newFileId }
this.newFile2 = { ...this.file2, _id: this.newFileId }
this.docEntries = [
{
path: this.doc0Path,
doc: this.newDoc0,
docLines: this.doc0Lines.join('\n'),
},
{
path: this.doc1Path,
doc: this.newDoc1,
docLines: this.doc1Lines.join('\n'),
},
{
path: this.doc2Path,
doc: this.newDoc2,
docLines: this.doc2Lines.join('\n'),
},
]
this.fileEntries = [
{
createdBlob: true,
path: this.file0Path,
file: this.newFile0,
},
{
createdBlob: true,
path: this.file1Path,
file: this.newFile1,
},
{
createdBlob: true,
path: this.file2Path,
file: this.newFile2,
},
]
this.Doc = sinon
.stub()
.callsFake(props => ({ _id: this.newDocId, ...props }))
this.File = sinon
.stub()
.callsFake(props => ({ _id: this.newFileId, ...props }))
this.DocstoreManager = {
promises: {
updateDoc: sinon.stub().resolves(),
getAllDocs: sinon.stub().resolves(this.docContents),
},
}
this.DocumentUpdaterHandler = {
promises: {
flushProjectToMongo: sinon.stub().resolves(),
updateProjectStructure: sinon.stub().resolves(),
},
}
this.HistoryManager = {
promises: {
copyBlob: sinon.stub().callsFake((historyId, newHistoryId, hash) => {
if (hash === '500') {
return Promise.reject(new Error('copy blob error'))
}
return Promise.resolve()
}),
},
}
this.TagsHandler = {
promises: {
addProjectToTags: sinon.stub().resolves({
_id: 'project-1',
}),
countTagsForProject: sinon.stub().resolves(1),
},
}
this.ProjectCreationHandler = {
promises: {
createBlankProject: sinon.stub().resolves(this.newBlankProject),
},
}
this.ProjectDeleter = {
promises: {
deleteProject: sinon.stub().resolves(),
},
}
this.ProjectEntityMongoUpdateHandler = {
promises: {
createNewFolderStructure: sinon.stub().resolves(this.newProjectVersion),
},
}
this.ProjectEntityUpdateHandler = {
isPathValidForRootDoc: sinon.stub().returns(true),
promises: {
setRootDoc: sinon.stub().resolves(),
},
}
this.ProjectGetter = {
promises: {
getProject: sinon
.stub()
.withArgs(this.project._id)
.resolves(this.project),
},
}
this.ProjectLocator = {
promises: {
findRootDoc: sinon.stub().resolves({
element: this.rootDoc,
path: { fileSystem: this.rootDocPath },
}),
findElementByPath: sinon
.stub()
.withArgs({
project_id: this.newBlankProject._id,
path: this.rootDocPath,
exactCaseMatch: true,
})
.resolves({ element: this.doc0 }),
},
}
this.ProjectOptionsHandler = {
promises: {
setCompiler: sinon.stub().resolves(),
},
}
this.TpdsProjectFlusher = {
promises: {
flushProjectToTpds: sinon.stub().resolves(),
},
}
this.ProjectDuplicator = SandboxedModule.require(MODULE_PATH, {
requires: {
'../../models/Doc': { Doc: this.Doc },
'../../models/File': { File: this.File },
'../Docstore/DocstoreManager': this.DocstoreManager,
'../DocumentUpdater/DocumentUpdaterHandler':
this.DocumentUpdaterHandler,
'./ProjectCreationHandler': this.ProjectCreationHandler,
'./ProjectDeleter': this.ProjectDeleter,
'./ProjectEntityMongoUpdateHandler':
this.ProjectEntityMongoUpdateHandler,
'./ProjectEntityUpdateHandler': this.ProjectEntityUpdateHandler,
'./ProjectGetter': this.ProjectGetter,
'./ProjectLocator': this.ProjectLocator,
'./ProjectOptionsHandler': this.ProjectOptionsHandler,
'../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
'../Tags/TagsHandler': this.TagsHandler,
'../History/HistoryManager': this.HistoryManager,
'../Compile/ClsiCacheManager': {
prepareClsiCache: sinon.stub().rejects(new Error('ignore this')),
},
},
})
})
describe('when the copy succeeds', function () {
beforeEach(async function () {
this.newProjectName = 'New project name'
this.newProject = await this.ProjectDuplicator.promises.duplicate(
this.owner,
this.project._id,
this.newProjectName
)
})
it('should flush the original project to mongo', function () {
this.DocumentUpdaterHandler.promises.flushProjectToMongo.should.have.been.calledWith(
this.project._id
)
})
it('should copy docs to docstore', function () {
for (const docLines of [this.doc0Lines, this.doc1Lines, this.doc2Lines]) {
this.DocstoreManager.promises.updateDoc.should.have.been.calledWith(
this.newProject._id.toString(),
this.newDocId.toString(),
docLines,
0,
{}
)
}
})
it('should duplicate the files with hashes by copying the blobs in history v1', function () {
for (const file of [this.file0, this.file1, this.file2]) {
this.HistoryManager.promises.copyBlob.should.have.been.calledWith(
this.project.overleaf.history.id,
this.newProject.overleaf.history.id,
file.hash
)
}
})
it('should create a blank project', function () {
this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
this.owner._id,
this.newProjectName
)
this.newProject._id.should.equal(this.newBlankProject._id)
})
it('should use the same compiler', function () {
this.ProjectOptionsHandler.promises.setCompiler.should.have.been.calledWith(
this.newProject._id,
this.project.compiler
)
})
it('should use the same root doc', function () {
this.ProjectEntityUpdateHandler.promises.setRootDoc.should.have.been.calledWith(
this.newProject._id,
this.rootFolder.docs[0]._id
)
})
it('should not copy the collaborators or read only refs', function () {
this.newProject.collaberator_refs.length.should.equal(0)
this.newProject.readOnly_refs.length.should.equal(0)
})
it('should copy all documents and files', function () {
this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
this.newProject._id,
this.docEntries,
this.fileEntries
)
})
it('should notify document updater of changes', function () {
this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
this.newProject._id,
this.newProject.overleaf.history.id,
this.owner._id,
{
newDocs: this.docEntries,
newFiles: this.fileEntries,
newProject: { version: this.newProjectVersion },
},
null
)
})
it('should flush the project to TPDS', function () {
this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
this.newProject._id
)
})
})
describe('without a root doc', function () {
beforeEach(async function () {
this.ProjectLocator.promises.findRootDoc.resolves({
element: null,
path: null,
})
this.newProject = await this.ProjectDuplicator.promises.duplicate(
this.owner,
this.project._id,
'Copy of project'
)
})
it('should not set the root doc on the copy', function () {
this.ProjectEntityUpdateHandler.promises.setRootDoc.should.not.have.been
.called
})
})
describe('with an invalid root doc', function () {
beforeEach(async function () {
this.ProjectEntityUpdateHandler.isPathValidForRootDoc.returns(false)
this.newProject = await this.ProjectDuplicator.promises.duplicate(
this.owner,
this.project._id,
'Copy of project'
)
})
it('should not set the root doc on the copy', function () {
this.ProjectEntityUpdateHandler.promises.setRootDoc.should.not.have.been
.called
})
})
describe('when cloning in history-v1 fails', function () {
it('should fail the clone operation', async function () {
this.file0.hash = '500'
await expect(
this.ProjectDuplicator.promises.duplicate(
this.owner,
this.project._id,
'name'
)
).to.be.rejectedWith('copy blob error')
})
})
describe('when there is an error', function () {
beforeEach(async function () {
this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.rejects()
await expect(
this.ProjectDuplicator.promises.duplicate(
this.owner,
this.project._id,
''
)
).to.be.rejected
})
it('should delete the broken cloned project', function () {
this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
this.newBlankProject._id
)
})
it('should not delete the original project', function () {
this.ProjectDeleter.promises.deleteProject.should.not.have.been.calledWith(
this.project._id
)
})
})
})