[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:
+21
-16
@@ -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
|
||||
),
|
||||
+14
-12
@@ -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,
|
||||
}
|
||||
+8
-8
@@ -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'
|
||||
|
||||
+5
-5
@@ -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'
|
||||
|
||||
+27
-24
@@ -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'
|
||||
|
||||
+2
-2
@@ -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'
|
||||
|
||||
|
||||
+21
-21
@@ -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) {
|
||||
|
||||
+12
-12
@@ -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,
|
||||
|
||||
+9
-9
@@ -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'
|
||||
|
||||
+4
-4
@@ -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'
|
||||
|
||||
+23
-23
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
|
||||
+160
-115
@@ -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
|
||||
})
|
||||
}
|
||||
+46
-59
@@ -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
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user