Merge pull request #28544 from overleaf/ac-some-web-esm-migration-4

[web] Convert some Features files to ES modules (part 4)

GitOrigin-RevId: cf11a7584e39c4d4de08e2f924240e488a4066c4
This commit is contained in:
Antoine Clausse
2025-09-23 11:20:07 +02:00
committed by Copybot
parent a6036c579c
commit 6b663a8509
44 changed files with 2806 additions and 2642 deletions

View File

@@ -5,7 +5,7 @@ import EditorRealTimeController from '../Editor/EditorRealTimeController.js'
import SessionManager from '../Authentication/SessionManager.js'
import UserInfoManager from '../User/UserInfoManager.js'
import UserInfoController from '../User/UserInfoController.js'
import ChatManager from './ChatManager.js'
import ChatManager from './ChatManager.mjs'
async function sendMessage(req, res) {
const { project_id: projectId } = req.params

View File

@@ -1,6 +1,6 @@
const UserInfoController = require('../User/UserInfoController')
const UserGetter = require('../User/UserGetter')
const { callbackify } = require('@overleaf/promise-utils')
import UserInfoController from '../User/UserInfoController.js'
import UserGetter from '../User/UserGetter.js'
import { callbackify } from '@overleaf/promise-utils'
async function injectUserInfoIntoThreads(threads) {
const userIds = new Set()
@@ -38,7 +38,7 @@ async function injectUserInfoIntoThreads(threads) {
return threads
}
module.exports = {
export default {
injectUserInfoIntoThreads: callbackify(injectUserInfoIntoThreads),
promises: {
injectUserInfoIntoThreads,

View File

@@ -4,7 +4,7 @@ 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 CompileManager from './CompileManager.mjs'
import ClsiManager from './ClsiManager.js'
import logger from '@overleaf/logger'
import Settings from '@overleaf/settings'

View File

@@ -1,19 +1,16 @@
import Crypto from 'node:crypto'
import Settings from '@overleaf/settings'
import RedisWrapper from '../../infrastructure/RedisWrapper.js'
import ProjectGetter from '../Project/ProjectGetter.js'
import ProjectRootDocManager from '../Project/ProjectRootDocManager.js'
import UserGetter from '../User/UserGetter.js'
import ClsiManager from './ClsiManager.js'
import Metrics from '@overleaf/metrics'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import UserAnalyticsIdCache from '../Analytics/UserAnalyticsIdCache.js'
import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
let CompileManager
const Crypto = require('crypto')
const Settings = require('@overleaf/settings')
const RedisWrapper = require('../../infrastructure/RedisWrapper')
const rclient = RedisWrapper.client('clsi_recently_compiled')
const ProjectGetter = require('../Project/ProjectGetter')
const ProjectRootDocManager = require('../Project/ProjectRootDocManager')
const UserGetter = require('../User/UserGetter')
const ClsiManager = require('./ClsiManager')
const Metrics = require('@overleaf/metrics')
const { RateLimiter } = require('../../infrastructure/RateLimiter')
const UserAnalyticsIdCache = require('../Analytics/UserAnalyticsIdCache')
const {
callbackify,
callbackifyMultiResult,
} = require('@overleaf/promise-utils')
function instrumentWithTimer(fn, key) {
return async (...args) => {
@@ -196,7 +193,7 @@ async function deleteAuxFiles(projectId, userId, clsiserverid) {
)
}
module.exports = CompileManager = {
export default CompileManager = {
promises: {
compile: instrumentedCompile,
deleteAuxFiles,

View File

@@ -1,7 +1,7 @@
import { isZodErrorLike, fromZodError } from 'zod-validation-error'
import Errors from './Errors.js'
import SessionManager from '../Authentication/SessionManager.js'
import SamlLogHandler from '../SamlLog/SamlLogHandler.js'
import SamlLogHandler from '../SamlLog/SamlLogHandler.mjs'
import HttpErrorHandler from './HttpErrorHandler.js'
import { plainTextResponse } from '../../infrastructure/Response.js'
import { expressifyErrorHandler } from '@overleaf/promise-utils'

View File

@@ -14,7 +14,7 @@ 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 ChatManager from '../Chat/ChatManager.mjs'
import OError from '@overleaf/o-error'
import ProjectGetter from '../Project/ProjectGetter.js'
import ProjectEntityHandler from '../Project/ProjectEntityHandler.js'

View File

@@ -15,21 +15,7 @@ import Settings from '@overleaf/settings'
import _ from 'lodash'
import AnalyticsManager from '../../../../app/src/Features/Analytics/AnalyticsManager.js'
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
import {
CompileFailedError,
UrlFetchFailedError,
InvalidUrlError,
AccessDeniedError,
BadEntityTypeError,
BadDataError,
ProjectNotFoundError,
V1ProjectNotFoundError,
SourceFileNotFoundError,
NotOriginalImporterError,
FeatureNotAvailableError,
RemoteServiceError,
FileCannotRefreshError,
} from './LinkedFilesErrors.js'
import LinkedFilesErrors from './LinkedFilesErrors.mjs'
import {
OutputFileFetchFailedError,
FileTooLargeError,
@@ -45,6 +31,22 @@ import ProjectOutputFileAgent from './ProjectOutputFileAgent.mjs'
import ProjectFileAgent from './ProjectFileAgent.mjs'
import UrlAgent from './UrlAgent.mjs'
const {
CompileFailedError,
UrlFetchFailedError,
InvalidUrlError,
AccessDeniedError,
BadEntityTypeError,
BadDataError,
ProjectNotFoundError,
V1ProjectNotFoundError,
SourceFileNotFoundError,
NotOriginalImporterError,
FeatureNotAvailableError,
RemoteServiceError,
FileCannotRefreshError,
} = LinkedFilesErrors
let LinkedFilesController
const createLinkedFileSchema = z.object({

View File

@@ -1,4 +1,4 @@
const { BackwardCompatibleError } = require('../Errors/Errors')
import { BackwardCompatibleError } from '../Errors/Errors.js'
class UrlFetchFailedError extends BackwardCompatibleError {}
@@ -26,7 +26,7 @@ class RemoteServiceError extends BackwardCompatibleError {}
class FileCannotRefreshError extends BackwardCompatibleError {}
module.exports = {
export default {
CompileFailedError,
UrlFetchFailedError,
InvalidUrlError,

View File

@@ -3,13 +3,12 @@ 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,
} from './LinkedFilesErrors.js'
import LinkedFilesErrors from './LinkedFilesErrors.mjs'
import { callbackifyAll } from '@overleaf/promise-utils'
const { ProjectNotFoundError, V1ProjectNotFoundError, BadDataError } =
LinkedFilesErrors
const LinkedFilesHandler = {
async getFileById(projectId, fileId) {
const { element, path, folder } = await ProjectLocator.promises.findElement(

View File

@@ -16,16 +16,16 @@ import DocstoreManager from '../Docstore/DocstoreManager.js'
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.js'
import _ from 'lodash'
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
import LinkedFilesErrors from './LinkedFilesErrors.mjs'
import { promisify } from '@overleaf/promise-utils'
import HistoryManager from '../History/HistoryManager.js'
import {
const {
BadDataError,
AccessDeniedError,
BadEntityTypeError,
SourceFileNotFoundError,
} from './LinkedFilesErrors.js'
import { promisify } from '@overleaf/promise-utils'
import HistoryManager from '../History/HistoryManager.js'
} = LinkedFilesErrors
let ProjectFileAgent

View File

@@ -1,17 +1,16 @@
import AuthorizationManager from '../Authorization/AuthorizationManager.js'
import CompileManager from '../Compile/CompileManager.js'
import CompileManager from '../Compile/CompileManager.mjs'
import ClsiManager from '../Compile/ClsiManager.js'
import ProjectFileAgent from './ProjectFileAgent.mjs'
import _ from 'lodash'
import {
CompileFailedError,
BadDataError,
AccessDeniedError,
} from './LinkedFilesErrors.js'
import LinkedFilesErrors from './LinkedFilesErrors.mjs'
import { OutputFileFetchFailedError } from '../Errors/Errors.js'
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
import { promisify } from '@overleaf/promise-utils'
const { CompileFailedError, BadDataError, AccessDeniedError } =
LinkedFilesErrors
function _prepare(projectId, linkedFileData, userId, callback) {
_checkAuth(projectId, linkedFileData, userId, (err, allowed) => {
if (err) {

View File

@@ -1,12 +1,14 @@
import logger from '@overleaf/logger'
import urlValidator from 'valid-url'
import { InvalidUrlError, UrlFetchFailedError } from './LinkedFilesErrors.js'
import LinkedFilesErrors from './LinkedFilesErrors.mjs'
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
import UrlHelper from '../Helpers/UrlHelper.js'
import { fetchStream, RequestFailedError } from '@overleaf/fetch-utils'
import { callbackify } from '@overleaf/promise-utils'
import { FileTooLargeError } from '../Errors/Errors.js'
const { InvalidUrlError, UrlFetchFailedError } = LinkedFilesErrors
async function createLinkedFile(
projectId,
linkedFileData,

View File

@@ -35,20 +35,20 @@ import AnalyticsManager from '../Analytics/AnalyticsManager.js'
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
import SplitTestSessionHandler from '../SplitTests/SplitTestSessionHandler.js'
import FeaturesUpdater from '../Subscription/FeaturesUpdater.js'
import SpellingHandler from '../Spelling/SpellingHandler.js'
import SpellingHandler from '../Spelling/SpellingHandler.mjs'
import { hasAdminAccess } from '../Helpers/AdminAuthorizationHelper.js'
import InstitutionsFeatures from '../Institutions/InstitutionsFeatures.js'
import InstitutionsGetter from '../Institutions/InstitutionsGetter.js'
import ProjectAuditLogHandler from './ProjectAuditLogHandler.mjs'
import PublicAccessLevels from '../Authorization/PublicAccessLevels.js'
import TagsHandler from '../Tags/TagsHandler.js'
import TutorialHandler from '../Tutorial/TutorialHandler.js'
import TutorialHandler from '../Tutorial/TutorialHandler.mjs'
import UserUpdater from '../User/UserUpdater.js'
import Modules from '../../infrastructure/Modules.js'
import { z, zz, validateReq } from '../../infrastructure/Validation.js'
import UserGetter from '../User/UserGetter.js'
import { isStandaloneAiAddOnPlanCode } from '../Subscription/AiHelper.js'
import SubscriptionController from '../Subscription/SubscriptionController.js'
import SubscriptionController from '../Subscription/SubscriptionController.mjs'
import { formatCurrency } from '../../util/currency.js'
const { ObjectId } = mongodb

View File

@@ -25,7 +25,7 @@ import NotificationsBuilder from '../Notifications/NotificationsBuilder.js'
import GeoIpLookup from '../../infrastructure/GeoIpLookup.js'
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
import SplitTestSessionHandler from '../SplitTests/SplitTestSessionHandler.js'
import TutorialHandler from '../Tutorial/TutorialHandler.js'
import TutorialHandler from '../Tutorial/TutorialHandler.mjs'
import SubscriptionHelper from '../Subscription/SubscriptionHelper.js'
import PermissionsManager from '../Authorization/PermissionsManager.js'
import AnalyticsManager from '../Analytics/AnalyticsManager.js'

View File

@@ -1,9 +1,9 @@
const { SamlLog } = require('../../models/SamlLog')
const SessionManager = require('../Authentication/SessionManager')
const logger = require('@overleaf/logger')
const { err: errSerializer } = require('@overleaf/logger/serializers')
const { callbackify } = require('util')
const Settings = require('@overleaf/settings')
import { SamlLog } from '../../models/SamlLog.js'
import SessionManager from '../Authentication/SessionManager.js'
import logger from '@overleaf/logger'
import loggerSerializers from '@overleaf/logger/serializers.js'
import { callbackify } from 'node:util'
import Settings from '@overleaf/settings'
const ALLOWED_PATHS = Settings.saml?.logAllowList || ['/saml/']
@@ -33,7 +33,7 @@ async function log(req, data, samlAssertion) {
data.samlSession = saml
if (data.error instanceof Error) {
const errSerialized = errSerializer(data.error)
const errSerialized = loggerSerializers.err(data.error)
if (data.error.tryAgain) {
errSerialized.tryAgain = data.error.tryAgain
}
@@ -82,4 +82,4 @@ const SamlLogHandler = {
},
}
module.exports = SamlLogHandler
export default SamlLogHandler

View File

@@ -5,7 +5,7 @@ import Settings from '@overleaf/settings'
import TpdsUpdateSender from '../ThirdPartyDataStore/TpdsUpdateSender.js'
import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.js'
import EditorRealTimeController from '../Editor/EditorRealTimeController.js'
import SystemMessageManager from '../SystemMessages/SystemMessageManager.js'
import SystemMessageManager from '../SystemMessages/SystemMessageManager.mjs'
const AdminController = {
_sendDisconnectAllUsersMessage: delay => {

View File

@@ -1,9 +1,9 @@
const OError = require('@overleaf/o-error')
const Metrics = require('@overleaf/metrics')
const { promisifyAll } = require('@overleaf/promise-utils')
const LearnedWordsManager = require('./LearnedWordsManager')
import OError from '@overleaf/o-error'
import Metrics from '@overleaf/metrics'
import { promisifyAll } from '@overleaf/promise-utils'
import LearnedWordsManager from './LearnedWordsManager.js'
module.exports = {
const SpellingHandler = {
getUserDictionary(userId, callback) {
const timer = new Metrics.Timer('spelling_get_dict')
LearnedWordsManager.getLearnedWords(userId, (error, words) => {
@@ -26,4 +26,4 @@ module.exports = {
},
}
module.exports.promises = promisifyAll(module.exports)
export default { ...SpellingHandler, promises: promisifyAll(SpellingHandler) }

View File

@@ -1,48 +1,48 @@
// @ts-check
const SessionManager = require('../Authentication/SessionManager')
const SubscriptionHandler = require('./SubscriptionHandler')
const SubscriptionHelper = require('./SubscriptionHelper')
const SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder')
const LimitationsManager = require('./LimitationsManager')
const RecurlyWrapper = require('./RecurlyWrapper')
const Settings = require('@overleaf/settings')
const logger = require('@overleaf/logger')
const GeoIpLookup = require('../../infrastructure/GeoIpLookup')
const FeaturesUpdater = require('./FeaturesUpdater')
const GroupPlansData = require('./GroupPlansData')
const V1SubscriptionManager = require('./V1SubscriptionManager')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const RecurlyEventHandler = require('./RecurlyEventHandler')
const { expressify } = require('@overleaf/promise-utils')
const OError = require('@overleaf/o-error')
import SessionManager from '../Authentication/SessionManager.js'
import SubscriptionHandler from './SubscriptionHandler.js'
import SubscriptionHelper from './SubscriptionHelper.js'
import SubscriptionViewModelBuilder from './SubscriptionViewModelBuilder.js'
import LimitationsManager from './LimitationsManager.js'
import RecurlyWrapper from './RecurlyWrapper.js'
import Settings from '@overleaf/settings'
import logger from '@overleaf/logger'
import GeoIpLookup from '../../infrastructure/GeoIpLookup.js'
import FeaturesUpdater from './FeaturesUpdater.js'
import GroupPlansData from './GroupPlansData.js'
import V1SubscriptionManager from './V1SubscriptionManager.js'
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
import RecurlyEventHandler from './RecurlyEventHandler.js'
import { expressify } from '@overleaf/promise-utils'
import OError from '@overleaf/o-error'
import Errors from './Errors.js'
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
import AuthorizationManager from '../Authorization/AuthorizationManager.js'
import Modules from '../../infrastructure/Modules.js'
import async from 'async'
import HttpErrorHandler from '../Errors/HttpErrorHandler.js'
import RecurlyClient from './RecurlyClient.js'
import {
AI_ADD_ON_CODE,
subscriptionChangeIsAiAssistUpgrade,
} from './AiHelper.js'
import PlansLocator from './PlansLocator.js'
import { User } from '../../models/User.js'
import UserGetter from '../User/UserGetter.js'
import PermissionsManager from '../Authorization/PermissionsManager.js'
import { sanitizeSessionUserForFrontEnd } from '../../infrastructure/FrontEndUser.js'
import { z, validateReq } from '../../infrastructure/Validation.js'
import { IndeterminateInvoiceError } from '../Errors/Errors.js'
import SubscriptionLocator from './SubscriptionLocator.js'
const {
DuplicateAddOnError,
AddOnNotPresentError,
PaymentActionRequiredError,
PaymentFailedError,
MissingBillingInfoError,
} = require('./Errors')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const AuthorizationManager = require('../Authorization/AuthorizationManager')
const Modules = require('../../infrastructure/Modules')
const async = require('async')
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const RecurlyClient = require('./RecurlyClient')
const {
AI_ADD_ON_CODE,
subscriptionChangeIsAiAssistUpgrade,
} = require('./AiHelper')
const PlansLocator = require('./PlansLocator')
const { User } = require('../../models/User')
const UserGetter = require('../User/UserGetter')
const PermissionsManager = require('../Authorization/PermissionsManager')
const {
sanitizeSessionUserForFrontEnd,
} = require('../../infrastructure/FrontEndUser')
const { z, validateReq } = require('../../infrastructure/Validation')
const { IndeterminateInvoiceError } = require('../Errors/Errors')
const SubscriptionLocator = require('./SubscriptionLocator')
} = Errors
const SUBSCRIPTION_PAUSED_REDIRECT_PATH =
'/user/subscription?redirect-reason=subscription-paused'
@@ -1101,7 +1101,7 @@ function makeChangePreview(
}
}
module.exports = {
export default {
userSubscriptionPage: expressify(userSubscriptionPage),
successfulSubscription: expressify(successfulSubscription),
cancelSubscription,

View File

@@ -3,7 +3,7 @@ import _ from 'lodash'
import OError from '@overleaf/o-error'
import SubscriptionUpdater from './SubscriptionUpdater.js'
import SubscriptionLocator from './SubscriptionLocator.js'
import SubscriptionController from './SubscriptionController.js'
import SubscriptionController from './SubscriptionController.mjs'
import SubscriptionHelper from './SubscriptionHelper.js'
import { Subscription } from '../../models/Subscription.js'
import { User } from '../../models/User.js'

View File

@@ -1,6 +1,6 @@
import AuthenticationController from '../Authentication/AuthenticationController.js'
import PermissionsController from '../Authorization/PermissionsController.mjs'
import SubscriptionController from './SubscriptionController.js'
import SubscriptionController from './SubscriptionController.mjs'
import SubscriptionGroupController from './SubscriptionGroupController.mjs'
import TeamInvitesController from './TeamInvitesController.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'

View File

@@ -1,6 +1,6 @@
import Settings from '@overleaf/settings'
import SessionManager from '../Authentication/SessionManager.js'
import SystemMessageManager from './SystemMessageManager.js'
import SystemMessageManager from './SystemMessageManager.mjs'
const ProjectController = {
getMessages(req, res, next) {

View File

@@ -1,9 +1,7 @@
const { SystemMessage } = require('../../models/SystemMessage')
const {
addRequiredCleanupHandlerBeforeDrainingConnections,
} = require('../../infrastructure/GracefulShutdown')
const { callbackifyAll } = require('@overleaf/promise-utils')
const logger = require('@overleaf/logger')
import { SystemMessage } from '../../models/SystemMessage.js'
import { addRequiredCleanupHandlerBeforeDrainingConnections } from '../../infrastructure/GracefulShutdown.js'
import { callbackifyAll } from '@overleaf/promise-utils'
import logger from '@overleaf/logger'
const SystemMessageManager = {
_cachedMessages: [],
@@ -52,7 +50,7 @@ addRequiredCleanupHandlerBeforeDrainingConnections(
}
)
module.exports = {
export default {
getMessages: SystemMessageManager.getMessages.bind(SystemMessageManager),
...callbackifyAll(SystemMessageManager, { without: ['getMessages'] }),
promises: SystemMessageManager,

View File

@@ -1,16 +1,16 @@
const AuthenticationController = require('../Authentication/AuthenticationController')
const TemplatesController = require('./TemplatesController')
const TemplatesMiddleware = require('./TemplatesMiddleware')
const { RateLimiter } = require('../../infrastructure/RateLimiter')
const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware')
const AnalyticsRegistrationSourceMiddleware = require('../Analytics/AnalyticsRegistrationSourceMiddleware')
import AuthenticationController from '../Authentication/AuthenticationController.js'
import TemplatesController from './TemplatesController.js'
import TemplatesMiddleware from './TemplatesMiddleware.js'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
import AnalyticsRegistrationSourceMiddleware from '../Analytics/AnalyticsRegistrationSourceMiddleware.js'
const rateLimiter = new RateLimiter('create-project-from-template', {
points: 20,
duration: 60,
})
module.exports = {
export default {
rateLimiter,
apply(app) {
app.get(

View File

@@ -1,6 +1,6 @@
import { expressify } from '@overleaf/promise-utils'
import TpdsUpdateHandler from './TpdsUpdateHandler.mjs'
import UpdateMerger from './UpdateMerger.js'
import UpdateMerger from './UpdateMerger.mjs'
import Errors from '../Errors/Errors.js'
import logger from '@overleaf/logger'
import Path from 'node:path'

View File

@@ -1,5 +1,5 @@
import { callbackify } from 'node:util'
import UpdateMerger from './UpdateMerger.js'
import UpdateMerger from './UpdateMerger.mjs'
import logger from '@overleaf/logger'
import NotificationsBuilder from '../Notifications/NotificationsBuilder.js'
import ProjectCreationHandler from '../Project/ProjectCreationHandler.js'

View File

@@ -1,14 +1,14 @@
const { callbackify } = require('util')
const _ = require('lodash')
const fsPromises = require('fs/promises')
const fs = require('fs')
const logger = require('@overleaf/logger')
const EditorController = require('../Editor/EditorController')
const FileTypeManager = require('../Uploads/FileTypeManager')
const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
const crypto = require('crypto')
const Settings = require('@overleaf/settings')
const { pipeline } = require('stream/promises')
import { callbackify } from 'node:util'
import _ from 'lodash'
import fsPromises from 'node:fs/promises'
import fs from 'node:fs'
import logger from '@overleaf/logger'
import EditorController from '../Editor/EditorController.js'
import FileTypeManager from '../Uploads/FileTypeManager.js'
import ProjectEntityHandler from '../Project/ProjectEntityHandler.js'
import crypto from 'node:crypto'
import Settings from '@overleaf/settings'
import { pipeline } from 'node:stream/promises'
async function mergeUpdate(userId, projectId, path, updateRequest, source) {
const fsPath = await writeUpdateToDisk(projectId, updateRequest)
@@ -185,7 +185,7 @@ async function createFolder(projectId, path, userId) {
return folder
}
module.exports = {
export default {
mergeUpdate: callbackify(mergeUpdate),
_mergeUpdate: callbackify(_mergeUpdate),
deleteUpdate: callbackify(deleteUpdate),

View File

@@ -1,5 +1,5 @@
import SessionManager from '../Authentication/SessionManager.js'
import TutorialHandler from './TutorialHandler.js'
import TutorialHandler from './TutorialHandler.mjs'
import { expressify } from '@overleaf/promise-utils'
const VALID_KEYS = [

View File

@@ -1,4 +1,4 @@
const UserUpdater = require('../User/UserUpdater')
import UserUpdater from '../User/UserUpdater.js'
const POSTPONE_DURATION_MS = 24 * 60 * 60 * 1000 // 1 day
@@ -59,4 +59,4 @@ function getInactiveTutorials(user, tutorialKey) {
return inactiveTutorials
}
module.exports = { setTutorialState, getInactiveTutorials }
export default { setTutorialState, getInactiveTutorials }

View File

@@ -24,7 +24,7 @@ import UserEmailsController from './Features/User/UserEmailsController.js'
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 CompileManager from './Features/Compile/CompileManager.mjs'
import CompileController from './Features/Compile/CompileController.mjs'
import HealthCheckController from './Features/HealthCheck/HealthCheckController.mjs'
import ProjectDownloadsController from './Features/Downloads/ProjectDownloadsController.mjs'
@@ -52,7 +52,7 @@ import MetaController from './Features/Metadata/MetaController.mjs'
import TokenAccessController from './Features/TokenAccess/TokenAccessController.mjs'
import TokenAccessRouter from './Features/TokenAccess/TokenAccessRouter.mjs'
import LinkedFilesRouter from './Features/LinkedFiles/LinkedFilesRouter.mjs'
import TemplatesRouter from './Features/Templates/TemplatesRouter.js'
import TemplatesRouter from './Features/Templates/TemplatesRouter.mjs'
import UserMembershipRouter from './Features/UserMembership/UserMembershipRouter.mjs'
import SystemMessageController from './Features/SystemMessages/SystemMessageController.mjs'
import AnalyticsRegistrationSourceMiddleware from './Features/Analytics/AnalyticsRegistrationSourceMiddleware.js'

View File

@@ -1,5 +1,5 @@
import AbstractMockApi from './AbstractMockApi.mjs'
import SubscriptionController from '../../../../app/src/Features/Subscription/SubscriptionController.js'
import SubscriptionController from '../../../../app/src/Features/Subscription/SubscriptionController.mjs'
import { xmlResponse } from '../../../../app/src/infrastructure/Response.js'
class MockRecurlyApi extends AbstractMockApi {

View File

@@ -33,7 +33,7 @@ describe('ChatController', function () {
default: ctx.ChatApiHandler,
}))
vi.doMock('../../../../app/src/Features/Chat/ChatManager.js', () => ({
vi.doMock('../../../../app/src/Features/Chat/ChatManager.mjs', () => ({
default: ctx.ChatManager,
}))

View File

@@ -0,0 +1,129 @@
import { vi, expect } from 'vitest'
import sinon from 'sinon'
const modulePath = '../../../../app/src/Features/Chat/ChatManager.mjs'
describe('ChatManager', function () {
beforeEach(async function (ctx) {
ctx.user_id = 'mock-user-id'
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: (ctx.UserGetter = { promises: {} }),
}))
vi.doMock('../../../../app/src/Features/User/UserInfoController', () => ({
default: (ctx.UserInfoController = {}),
}))
ctx.ChatManager = (await import(modulePath)).default
ctx.req = {
params: {
project_id: ctx.project_id,
},
}
ctx.res = {
json: sinon.stub(),
send: sinon.stub(),
sendStatus: sinon.stub(),
}
})
describe('injectUserInfoIntoThreads', function () {
beforeEach(function (ctx) {
ctx.users = {
user_id_1: {
_id: 'user_id_1',
},
user_id_2: {
_id: 'user_id_2',
},
}
ctx.UserGetter.promises.getUsers = userIds =>
Promise.resolve(
Array.from(userIds)
.map(id => ctx.users[id])
.filter(u => !!u)
)
sinon.spy(ctx.UserGetter.promises, 'getUsers')
ctx.UserInfoController.formatPersonalInfo = user => ({
formatted: { id: user._id.toString() },
})
})
it('should inject a user object into messaged and resolved data', async function (ctx) {
const threads = await ctx.ChatManager.promises.injectUserInfoIntoThreads({
thread1: {
resolved: true,
resolved_by_user_id: 'user_id_1',
messages: [
{
user_id: 'user_id_1',
content: 'foo',
},
{
user_id: 'user_id_2',
content: 'bar',
},
],
},
thread2: {
messages: [
{
user_id: 'user_id_1',
content: 'baz',
},
],
},
})
expect(threads).to.deep.equal({
thread1: {
resolved: true,
resolved_by_user_id: 'user_id_1',
resolved_by_user: { formatted: { id: 'user_id_1' } },
messages: [
{
user_id: 'user_id_1',
user: { formatted: { id: 'user_id_1' } },
content: 'foo',
},
{
user_id: 'user_id_2',
user: { formatted: { id: 'user_id_2' } },
content: 'bar',
},
],
},
thread2: {
messages: [
{
user_id: 'user_id_1',
user: { formatted: { id: 'user_id_1' } },
content: 'baz',
},
],
},
})
})
it('should lookup all users in a single batch', async function (ctx) {
await ctx.ChatManager.promises.injectUserInfoIntoThreads([
{
messages: [
{
user_id: 'user_id_1',
content: 'foo',
},
{
user_id: 'user_id_1',
content: 'bar',
},
],
},
])
ctx.UserGetter.promises.getUsers.should.have.been.calledOnce
})
})
})

View File

@@ -1,131 +0,0 @@
const SandboxedModule = require('sandboxed-module')
const path = require('path')
const sinon = require('sinon')
const modulePath = path.join(
__dirname,
'../../../../app/src/Features/Chat/ChatManager'
)
const { expect } = require('chai')
describe('ChatManager', function () {
beforeEach(function () {
this.user_id = 'mock-user-id'
this.ChatManager = SandboxedModule.require(modulePath, {
requires: {
'../User/UserGetter': (this.UserGetter = { promises: {} }),
'../User/UserInfoController': (this.UserInfoController = {}),
},
})
this.req = {
params: {
project_id: this.project_id,
},
}
this.res = {
json: sinon.stub(),
send: sinon.stub(),
sendStatus: sinon.stub(),
}
})
describe('injectUserInfoIntoThreads', function () {
beforeEach(function () {
this.users = {
user_id_1: {
_id: 'user_id_1',
},
user_id_2: {
_id: 'user_id_2',
},
}
this.UserGetter.promises.getUsers = userIds =>
Promise.resolve(
Array.from(userIds)
.map(id => this.users[id])
.filter(u => !!u)
)
sinon.spy(this.UserGetter.promises, 'getUsers')
return (this.UserInfoController.formatPersonalInfo = user => ({
formatted: { id: user._id.toString() },
}))
})
it('should inject a user object into messaged and resolved data', async function () {
const threads = await this.ChatManager.promises.injectUserInfoIntoThreads(
{
thread1: {
resolved: true,
resolved_by_user_id: 'user_id_1',
messages: [
{
user_id: 'user_id_1',
content: 'foo',
},
{
user_id: 'user_id_2',
content: 'bar',
},
],
},
thread2: {
messages: [
{
user_id: 'user_id_1',
content: 'baz',
},
],
},
}
)
expect(threads).to.deep.equal({
thread1: {
resolved: true,
resolved_by_user_id: 'user_id_1',
resolved_by_user: { formatted: { id: 'user_id_1' } },
messages: [
{
user_id: 'user_id_1',
user: { formatted: { id: 'user_id_1' } },
content: 'foo',
},
{
user_id: 'user_id_2',
user: { formatted: { id: 'user_id_2' } },
content: 'bar',
},
],
},
thread2: {
messages: [
{
user_id: 'user_id_1',
user: { formatted: { id: 'user_id_1' } },
content: 'baz',
},
],
},
})
})
it('should lookup all users in a single batch', async function () {
await this.ChatManager.promises.injectUserInfoIntoThreads([
{
messages: [
{
user_id: 'user_id_1',
content: 'foo',
},
{
user_id: 'user_id_1',
content: 'bar',
},
],
},
])
this.UserGetter.promises.getUsers.should.have.been.calledOnce
})
})
})

View File

@@ -0,0 +1,457 @@
import { vi, expect } from 'vitest'
import sinon from 'sinon'
const MODULE_PATH = '../../../../app/src/Features/Compile/CompileManager.mjs'
describe('CompileManager', function () {
beforeEach(async function (ctx) {
ctx.rateLimiter = {
consume: sinon.stub().resolves(),
}
ctx.timer = {
done: sinon.stub(),
}
ctx.Metrics = {
Timer: sinon.stub().returns(ctx.timer),
inc: sinon.stub(),
}
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {
apis: {
clsi: { submissionBackendClass: 'n2d' },
},
redis: { web: { host: '127.0.0.1', port: 42 } },
rateLimit: { autoCompile: {} },
}),
}))
vi.doMock('../../../../app/src/infrastructure/RedisWrapper', () => ({
default: {
client: () =>
(ctx.rclient = {
auth() {},
}),
},
}))
vi.doMock(
'../../../../app/src/Features/Project/ProjectRootDocManager',
() => ({
default: (ctx.ProjectRootDocManager = {
promises: {},
}),
})
)
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: (ctx.ProjectGetter = { promises: {} }),
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: (ctx.UserGetter = { promises: {} }),
}))
vi.doMock('../../../../app/src/Features/Compile/ClsiManager', () => ({
default: (ctx.ClsiManager = { promises: {} }),
}))
vi.doMock('../../../../app/src/infrastructure/RateLimiter.js', () => ({
RateLimiter: sinon.stub().returns(ctx.rateLimiter),
}))
vi.doMock('@overleaf/metrics', () => ({
default: ctx.Metrics,
}))
vi.doMock(
'../../../../app/src/Features/Analytics/UserAnalyticsIdCache',
() => ({
default: (ctx.UserAnalyticsIdCache = {
get: sinon.stub().resolves('abc'),
}),
})
)
vi.doMock(
'../../../../app/src/Features/SplitTests/SplitTestHandler',
() => ({
default: (ctx.SplitTestHandler = {
promises: {},
}),
})
)
ctx.CompileManager = (await import(MODULE_PATH)).default
ctx.project_id = 'mock-project-id-123'
ctx.user_id = 'mock-user-id-123'
ctx.callback = sinon.stub()
ctx.limits = {
timeout: 42,
compileGroup: 'standard',
}
})
describe('compile', function () {
beforeEach(function (ctx) {
ctx.CompileManager._checkIfRecentlyCompiled = sinon.stub().resolves(false)
ctx.ProjectRootDocManager.promises.ensureRootDocumentIsSet = sinon
.stub()
.resolves()
ctx.CompileManager.promises.getProjectCompileLimits = sinon
.stub()
.resolves(ctx.limits)
ctx.ClsiManager.promises.sendRequest = sinon.stub().resolves({
status: (ctx.status = 'mock-status'),
outputFiles: (ctx.outputFiles = []),
clsiServerId: (ctx.output = 'mock output'),
})
})
describe('succesfully', function () {
let result
beforeEach(async function (ctx) {
ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit = async (
isAutoCompile,
compileGroup
) => true
ctx.ProjectGetter.promises.getProject = sinon
.stub()
.resolves(
(ctx.project = { owner_ref: (ctx.owner_id = 'owner-id-123') })
)
ctx.UserGetter.promises.getUser = sinon.stub().resolves(
(ctx.user = {
features: { compileTimeout: '20s', compileGroup: 'standard' },
analyticsId: 'abc',
})
)
result = await ctx.CompileManager.promises.compile(
ctx.project_id,
ctx.user_id,
{}
)
})
it('should check the project has not been recently compiled', function (ctx) {
ctx.CompileManager._checkIfRecentlyCompiled
.calledWith(ctx.project_id, ctx.user_id)
.should.equal(true)
})
it('should ensure that the root document is set', function (ctx) {
ctx.ProjectRootDocManager.promises.ensureRootDocumentIsSet
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should get the project compile limits', function (ctx) {
ctx.CompileManager.promises.getProjectCompileLimits
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should run the compile with the compile limits', function (ctx) {
ctx.ClsiManager.promises.sendRequest
.calledWith(ctx.project_id, ctx.user_id, {
timeout: ctx.limits.timeout,
compileGroup: 'standard',
buildId: sinon.match(/[a-f0-9]+-[a-f0-9]+/),
})
.should.equal(true)
})
it('should resolve with the output', function (ctx) {
expect(result).to.haveOwnProperty('status', ctx.status)
expect(result).to.haveOwnProperty('clsiServerId', ctx.output)
expect(result).to.haveOwnProperty('outputFiles', ctx.outputFiles)
})
it('should time the compile', function (ctx) {
ctx.timer.done.called.should.equal(true)
})
})
describe('when the project has been recently compiled', function () {
it('should return', async function (ctx) {
ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit = async (
isAutoCompile,
compileGroup
) => true
ctx.CompileManager._checkIfRecentlyCompiled = sinon
.stub()
.resolves(true)
const { status } = await ctx.CompileManager.promises.compile(
ctx.project_id,
ctx.user_id,
{}
)
status.should.equal('too-recently-compiled')
})
})
describe('should check the rate limit', function () {
it('should return', async function (ctx) {
ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit = sinon
.stub()
.resolves(false)
const { status } = await ctx.CompileManager.promises.compile(
ctx.project_id,
ctx.user_id,
{}
)
expect(status).to.equal('autocompile-backoff')
})
})
})
describe('getProjectCompileLimits', function () {
beforeEach(async function (ctx) {
ctx.features = {
compileTimeout: (ctx.timeout = 42),
compileGroup: (ctx.group = 'priority'),
}
ctx.ProjectGetter.promises.getProject = sinon
.stub()
.resolves(
(ctx.project = { owner_ref: (ctx.owner_id = 'owner-id-123') })
)
ctx.UserGetter.promises.getUser = sinon
.stub()
.resolves((ctx.user = { features: ctx.features, analyticsId: 'abc' }))
try {
const result =
await ctx.CompileManager.promises.getProjectCompileLimits(
ctx.project_id
)
ctx.callback(null, result)
} catch (error) {
ctx.callback(error)
}
})
it('should look up the owner of the project', function (ctx) {
ctx.ProjectGetter.promises.getProject
.calledWith(ctx.project_id, { owner_ref: 1 })
.should.equal(true)
})
it("should look up the owner's features", function (ctx) {
ctx.UserGetter.promises.getUser
.calledWith(ctx.project.owner_ref, {
_id: 1,
alphaProgram: 1,
analyticsId: 1,
betaProgram: 1,
features: 1,
})
.should.equal(true)
})
it('should return the limits', function (ctx) {
ctx.callback
.calledWith(null, {
timeout: ctx.timeout,
compileGroup: ctx.group,
compileBackendClass: 'c2d',
ownerAnalyticsId: 'abc',
})
.should.equal(true)
})
})
describe('compileBackendClass', function () {
beforeEach(function (ctx) {
ctx.features = {
compileTimeout: 42,
compileGroup: 'standard',
}
ctx.ProjectGetter.promises.getProject = sinon
.stub()
.resolves({ owner_ref: 'owner-id-123' })
ctx.UserGetter.promises.getUser = sinon
.stub()
.resolves({ features: ctx.features, analyticsId: 'abc' })
})
describe('with priority compile', function () {
beforeEach(function (ctx) {
ctx.features.compileGroup = 'priority'
})
it('should return the default class', async function (ctx) {
const { compileBackendClass } =
await ctx.CompileManager.promises.getProjectCompileLimits(
ctx.project_id
)
expect(compileBackendClass).to.equal('c2d')
})
})
})
describe('deleteAuxFiles', function () {
let result
beforeEach(async function (ctx) {
ctx.CompileManager.promises.getProjectCompileLimits = sinon
.stub()
.resolves((ctx.limits = { compileGroup: 'mock-compile-group' }))
ctx.ClsiManager.promises.deleteAuxFiles = sinon.stub().resolves('test')
result = await ctx.CompileManager.promises.deleteAuxFiles(
ctx.project_id,
ctx.user_id
)
})
it('should look up the compile group to use', function (ctx) {
ctx.CompileManager.promises.getProjectCompileLimits
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should delete the aux files', function (ctx) {
ctx.ClsiManager.promises.deleteAuxFiles
.calledWith(ctx.project_id, ctx.user_id, ctx.limits)
.should.equal(true)
})
it('should resolve', function () {
expect(result).not.to.be.undefined
})
})
describe('_checkIfRecentlyCompiled', function () {
describe('when the key exists in redis', function () {
let result
beforeEach(async function (ctx) {
ctx.rclient.set = sinon.stub().resolves(null)
result = await ctx.CompileManager._checkIfRecentlyCompiled(
ctx.project_id,
ctx.user_id
)
})
it('should try to set the key', function (ctx) {
ctx.rclient.set
.calledWith(
`compile:${ctx.project_id}:${ctx.user_id}`,
true,
'EX',
ctx.CompileManager.COMPILE_DELAY,
'NX'
)
.should.equal(true)
})
it('should resolve with true', function () {
result.should.equal(true)
})
})
describe('when the key does not exist in redis', function () {
let result
beforeEach(async function (ctx) {
ctx.rclient.set = sinon.stub().resolves('OK')
result = await ctx.CompileManager._checkIfRecentlyCompiled(
ctx.project_id,
ctx.user_id
)
})
it('should try to set the key', function (ctx) {
ctx.rclient.set
.calledWith(
`compile:${ctx.project_id}:${ctx.user_id}`,
true,
'EX',
ctx.CompileManager.COMPILE_DELAY,
'NX'
)
.should.equal(true)
})
it('should resolve with false', function () {
result.should.equal(false)
})
})
})
describe('_checkIfAutoCompileLimitHasBeenHit', function () {
it('should be able to compile if it is not an autocompile', async function (ctx) {
const canCompile =
await ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit(
false,
'everyone'
)
expect(canCompile).to.equal(true)
})
it('should be able to compile if rate limit has remaining', async function (ctx) {
const canCompile =
await ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit(
true,
'everyone'
)
expect(ctx.rateLimiter.consume).to.have.been.calledWith('global')
expect(canCompile).to.equal(true)
})
it('should be not able to compile if rate limit has no remianing', async function (ctx) {
ctx.rateLimiter.consume.rejects({ remainingPoints: 0 })
const canCompile =
await ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit(
true,
'everyone'
)
expect(canCompile).to.equal(false)
})
it('should return false if there is an error in the rate limit', async function (ctx) {
ctx.rateLimiter.consume.rejects(new Error('BOOM!'))
const canCompile =
await ctx.CompileManager._checkIfAutoCompileLimitHasBeenHit(
true,
'everyone'
)
expect(canCompile).to.equal(false)
})
})
describe('wordCount', function () {
let result
const wordCount = 1
beforeEach(async function (ctx) {
ctx.CompileManager.promises.getProjectCompileLimits = sinon
.stub()
.resolves((ctx.limits = { compileGroup: 'mock-compile-group' }))
ctx.ClsiManager.promises.wordCount = sinon.stub().resolves(wordCount)
result = await ctx.CompileManager.promises.wordCount(
ctx.project_id,
ctx.user_id,
false
)
})
it('should look up the compile group to use', function (ctx) {
ctx.CompileManager.promises.getProjectCompileLimits
.calledWith(ctx.project_id)
.should.equal(true)
})
it('should call wordCount for project', function (ctx) {
ctx.ClsiManager.promises.wordCount
.calledWith(ctx.project_id, ctx.user_id, false, ctx.limits)
.should.equal(true)
})
it('should resolve with the wordCount from the ClsiManager', function () {
expect(result).to.equal(wordCount)
})
})
})

View File

@@ -1,426 +0,0 @@
const { expect } = require('chai')
const sinon = require('sinon')
const SandboxedModule = require('sandboxed-module')
const MODULE_PATH = '../../../../app/src/Features/Compile/CompileManager.js'
describe('CompileManager', function () {
beforeEach(function () {
this.rateLimiter = {
consume: sinon.stub().resolves(),
}
this.RateLimiter = {
RateLimiter: sinon.stub().returns(this.rateLimiter),
}
this.timer = {
done: sinon.stub(),
}
this.Metrics = {
Timer: sinon.stub().returns(this.timer),
inc: sinon.stub(),
}
this.CompileManager = SandboxedModule.require(MODULE_PATH, {
requires: {
'@overleaf/settings': (this.settings = {
apis: {
clsi: { submissionBackendClass: 'n2d' },
},
redis: { web: { host: '127.0.0.1', port: 42 } },
rateLimit: { autoCompile: {} },
}),
'../../infrastructure/RedisWrapper': {
client: () =>
(this.rclient = {
auth() {},
}),
},
'../Project/ProjectRootDocManager': (this.ProjectRootDocManager = {
promises: {},
}),
'../Project/ProjectGetter': (this.ProjectGetter = { promises: {} }),
'../User/UserGetter': (this.UserGetter = { promises: {} }),
'./ClsiManager': (this.ClsiManager = { promises: {} }),
'../../infrastructure/RateLimiter': this.RateLimiter,
'@overleaf/metrics': this.Metrics,
'../Analytics/UserAnalyticsIdCache': (this.UserAnalyticsIdCache = {
get: sinon.stub().resolves('abc'),
}),
'../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
promises: {},
}),
},
})
this.project_id = 'mock-project-id-123'
this.user_id = 'mock-user-id-123'
this.callback = sinon.stub()
this.limits = {
timeout: 42,
compileGroup: 'standard',
}
})
describe('compile', function () {
beforeEach(function () {
this.CompileManager._checkIfRecentlyCompiled = sinon
.stub()
.resolves(false)
this.ProjectRootDocManager.promises.ensureRootDocumentIsSet = sinon
.stub()
.resolves()
this.CompileManager.promises.getProjectCompileLimits = sinon
.stub()
.resolves(this.limits)
this.ClsiManager.promises.sendRequest = sinon.stub().resolves({
status: (this.status = 'mock-status'),
outputFiles: (this.outputFiles = []),
clsiServerId: (this.output = 'mock output'),
})
})
describe('succesfully', function () {
let result
beforeEach(async function () {
this.CompileManager._checkIfAutoCompileLimitHasBeenHit = async (
isAutoCompile,
compileGroup
) => true
this.ProjectGetter.promises.getProject = sinon
.stub()
.resolves(
(this.project = { owner_ref: (this.owner_id = 'owner-id-123') })
)
this.UserGetter.promises.getUser = sinon.stub().resolves(
(this.user = {
features: { compileTimeout: '20s', compileGroup: 'standard' },
analyticsId: 'abc',
})
)
result = await this.CompileManager.promises.compile(
this.project_id,
this.user_id,
{}
)
})
it('should check the project has not been recently compiled', function () {
this.CompileManager._checkIfRecentlyCompiled
.calledWith(this.project_id, this.user_id)
.should.equal(true)
})
it('should ensure that the root document is set', function () {
this.ProjectRootDocManager.promises.ensureRootDocumentIsSet
.calledWith(this.project_id)
.should.equal(true)
})
it('should get the project compile limits', function () {
this.CompileManager.promises.getProjectCompileLimits
.calledWith(this.project_id)
.should.equal(true)
})
it('should run the compile with the compile limits', function () {
this.ClsiManager.promises.sendRequest
.calledWith(this.project_id, this.user_id, {
timeout: this.limits.timeout,
compileGroup: 'standard',
buildId: sinon.match(/[a-f0-9]+-[a-f0-9]+/),
})
.should.equal(true)
})
it('should resolve with the output', function () {
expect(result).to.haveOwnProperty('status', this.status)
expect(result).to.haveOwnProperty('clsiServerId', this.output)
expect(result).to.haveOwnProperty('outputFiles', this.outputFiles)
})
it('should time the compile', function () {
this.timer.done.called.should.equal(true)
})
})
describe('when the project has been recently compiled', function () {
it('should return', async function () {
this.CompileManager._checkIfAutoCompileLimitHasBeenHit = async (
isAutoCompile,
compileGroup
) => true
this.CompileManager._checkIfRecentlyCompiled = sinon
.stub()
.resolves(true)
const { status } = await this.CompileManager.promises.compile(
this.project_id,
this.user_id,
{}
)
status.should.equal('too-recently-compiled')
})
})
describe('should check the rate limit', function () {
it('should return', async function () {
this.CompileManager._checkIfAutoCompileLimitHasBeenHit = sinon
.stub()
.resolves(false)
const { status } = await this.CompileManager.promises.compile(
this.project_id,
this.user_id,
{}
)
expect(status).to.equal('autocompile-backoff')
})
})
})
describe('getProjectCompileLimits', function () {
beforeEach(async function () {
this.features = {
compileTimeout: (this.timeout = 42),
compileGroup: (this.group = 'priority'),
}
this.ProjectGetter.promises.getProject = sinon
.stub()
.resolves(
(this.project = { owner_ref: (this.owner_id = 'owner-id-123') })
)
this.UserGetter.promises.getUser = sinon
.stub()
.resolves((this.user = { features: this.features, analyticsId: 'abc' }))
try {
const result =
await this.CompileManager.promises.getProjectCompileLimits(
this.project_id
)
this.callback(null, result)
} catch (error) {
this.callback(error)
}
})
it('should look up the owner of the project', function () {
this.ProjectGetter.promises.getProject
.calledWith(this.project_id, { owner_ref: 1 })
.should.equal(true)
})
it("should look up the owner's features", function () {
this.UserGetter.promises.getUser
.calledWith(this.project.owner_ref, {
_id: 1,
alphaProgram: 1,
analyticsId: 1,
betaProgram: 1,
features: 1,
})
.should.equal(true)
})
it('should return the limits', function () {
this.callback
.calledWith(null, {
timeout: this.timeout,
compileGroup: this.group,
compileBackendClass: 'c2d',
ownerAnalyticsId: 'abc',
})
.should.equal(true)
})
})
describe('compileBackendClass', function () {
beforeEach(function () {
this.features = {
compileTimeout: 42,
compileGroup: 'standard',
}
this.ProjectGetter.promises.getProject = sinon
.stub()
.resolves({ owner_ref: 'owner-id-123' })
this.UserGetter.promises.getUser = sinon
.stub()
.resolves({ features: this.features, analyticsId: 'abc' })
})
describe('with priority compile', function () {
beforeEach(function () {
this.features.compileGroup = 'priority'
})
it('should return the default class', async function () {
const { compileBackendClass } =
await this.CompileManager.promises.getProjectCompileLimits(
this.project_id
)
expect(compileBackendClass).to.equal('c2d')
})
})
})
describe('deleteAuxFiles', function () {
let result
beforeEach(async function () {
this.CompileManager.promises.getProjectCompileLimits = sinon
.stub()
.resolves((this.limits = { compileGroup: 'mock-compile-group' }))
this.ClsiManager.promises.deleteAuxFiles = sinon.stub().resolves('test')
result = await this.CompileManager.promises.deleteAuxFiles(
this.project_id,
this.user_id
)
})
it('should look up the compile group to use', function () {
this.CompileManager.promises.getProjectCompileLimits
.calledWith(this.project_id)
.should.equal(true)
})
it('should delete the aux files', function () {
this.ClsiManager.promises.deleteAuxFiles
.calledWith(this.project_id, this.user_id, this.limits)
.should.equal(true)
})
it('should resolve', function () {
expect(result).not.to.be.undefined
})
})
describe('_checkIfRecentlyCompiled', function () {
describe('when the key exists in redis', function () {
let result
beforeEach(async function () {
this.rclient.set = sinon.stub().resolves(null)
result = await this.CompileManager._checkIfRecentlyCompiled(
this.project_id,
this.user_id
)
})
it('should try to set the key', function () {
this.rclient.set
.calledWith(
`compile:${this.project_id}:${this.user_id}`,
true,
'EX',
this.CompileManager.COMPILE_DELAY,
'NX'
)
.should.equal(true)
})
it('should resolve with true', function () {
result.should.equal(true)
})
})
describe('when the key does not exist in redis', function () {
let result
beforeEach(async function () {
this.rclient.set = sinon.stub().resolves('OK')
result = await this.CompileManager._checkIfRecentlyCompiled(
this.project_id,
this.user_id
)
})
it('should try to set the key', function () {
this.rclient.set
.calledWith(
`compile:${this.project_id}:${this.user_id}`,
true,
'EX',
this.CompileManager.COMPILE_DELAY,
'NX'
)
.should.equal(true)
})
it('should resolve with false', function () {
result.should.equal(false)
})
})
})
describe('_checkIfAutoCompileLimitHasBeenHit', function () {
it('should be able to compile if it is not an autocompile', async function () {
const canCompile =
await this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
false,
'everyone'
)
expect(canCompile).to.equal(true)
})
it('should be able to compile if rate limit has remaining', async function () {
const canCompile =
await this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
true,
'everyone'
)
expect(this.rateLimiter.consume).to.have.been.calledWith('global')
expect(canCompile).to.equal(true)
})
it('should be not able to compile if rate limit has no remianing', async function () {
this.rateLimiter.consume.rejects({ remainingPoints: 0 })
const canCompile =
await this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
true,
'everyone'
)
expect(canCompile).to.equal(false)
})
it('should return false if there is an error in the rate limit', async function () {
this.rateLimiter.consume.rejects(new Error('BOOM!'))
const canCompile =
await this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
true,
'everyone'
)
expect(canCompile).to.equal(false)
})
})
describe('wordCount', function () {
let result
const wordCount = 1
beforeEach(async function () {
this.CompileManager.promises.getProjectCompileLimits = sinon
.stub()
.resolves((this.limits = { compileGroup: 'mock-compile-group' }))
this.ClsiManager.promises.wordCount = sinon.stub().resolves(wordCount)
result = await this.CompileManager.promises.wordCount(
this.project_id,
this.user_id,
false
)
})
it('should look up the compile group to use', function () {
this.CompileManager.promises.getProjectCompileLimits
.calledWith(this.project_id)
.should.equal(true)
})
it('should call wordCount for project', function () {
this.ClsiManager.promises.wordCount
.calledWith(this.project_id, this.user_id, false, this.limits)
.should.equal(true)
})
it('should resolve with the wordCount from the ClsiManager', function () {
expect(result).to.equal(wordCount)
})
})
})

View File

@@ -252,9 +252,12 @@ describe('EditorHttpController', function () {
default: ctx.SplitTestHandler,
})
)
vi.doMock('../../../../app/src/Features/Compile/CompileManager.js', () => ({
default: {},
}))
vi.doMock(
'../../../../app/src/Features/Compile/CompileManager.mjs',
() => ({
default: {},
})
)
vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({
default: ctx.UserGetter,
}))

View File

@@ -1,27 +1,31 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
const modulePath = '../../../../app/src/Features/SamlLog/SamlLogHandler'
const modulePath = '../../../../app/src/Features/SamlLog/SamlLogHandler.mjs'
describe('SamlLogHandler', function () {
let SamlLog, SamlLogHandler, SamlLogModel
let SamlLog, SamlLogHandler
let data, providerId, samlLog, sessionId
beforeEach(function () {
beforeEach(async function (ctx) {
samlLog = {
save: sinon.stub(),
}
SamlLog = function () {
return samlLog
}
SamlLogModel = { SamlLog }
SamlLogHandler = SandboxedModule.require(modulePath, {
requires: {
'../../models/SamlLog': SamlLogModel,
},
})
ctx.logger = {
error: sinon.stub(),
}
vi.doMock('@overleaf/logger', () => ({
default: ctx.logger,
}))
vi.doMock('../../../../app/src/models/SamlLog', () => ({ SamlLog }))
SamlLogHandler = (await import(modulePath)).default
data = { foo: true }
providerId = 'provider-id'
@@ -69,13 +73,13 @@ describe('SamlLogHandler', function () {
)
})
it('should log without data and log error', function () {
it('should log without data and log error', function (ctx) {
samlLog.providerId.should.equal(providerId)
samlLog.sessionId.should.equal(sessionId.substr(0, 8))
expect(samlLog.data).to.be.undefined
expect(samlLog.jsonData).to.be.undefined
samlLog.save.should.have.been.calledOnce
this.logger.error.should.have.been.calledOnce.and.calledWithMatch(
ctx.logger.error.should.have.been.calledOnce.and.calledWithMatch(
{ providerId, sessionId: sessionId.substr(0, 8) },
'SamlLog JSON.stringify Error'
)
@@ -99,8 +103,8 @@ describe('SamlLogHandler', function () {
)
})
it('should log error', function () {
this.logger.error.should.have.been.calledOnce.and.calledWithMatch(
it('should log error', function (ctx) {
ctx.logger.error.should.have.been.calledOnce.and.calledWithMatch(
{
err,
sessionId: sessionId.substr(0, 8),
@@ -127,8 +131,8 @@ describe('SamlLogHandler', function () {
)
})
it('should log error', function () {
this.logger.error.should.have.been.calledOnce.and.calledWithMatch(
it('should log error', function (ctx) {
ctx.logger.error.should.have.been.calledOnce.and.calledWithMatch(
{
err,
sessionId: sessionId.substr(0, 8),
@@ -155,8 +159,8 @@ describe('SamlLogHandler', function () {
)
})
it('should not log any error', function () {
this.logger.error.should.not.have.been.called
it('should not log any error', function (ctx) {
ctx.logger.error.should.not.have.been.called
})
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
import { vi } from 'vitest'
import sinon from 'sinon'
const modulePath =
'../../../../app/src/Features/SystemMessages/SystemMessageManager.mjs'
describe('SystemMessageManager', function () {
beforeEach(async function (ctx) {
ctx.messages = ['messages-stub']
ctx.SystemMessage = {
find: sinon.stub().returns({
exec: sinon.stub().resolves(ctx.messages),
}),
}
vi.doMock('../../../../app/src/models/SystemMessage', () => ({
SystemMessage: ctx.SystemMessage,
}))
ctx.SystemMessageManager = (await import(modulePath)).default
})
it('should look the messages up in the database on import', function (ctx) {
sinon.assert.called(ctx.SystemMessage.find)
})
describe('getMessage', function () {
beforeEach(function (ctx) {
ctx.SystemMessageManager._cachedMessages = ctx.messages
ctx.result = ctx.SystemMessageManager.getMessages()
})
it('should return the messages', function (ctx) {
ctx.result.should.equal(ctx.messages)
})
})
describe('clearMessages', function () {
beforeEach(function (ctx) {
ctx.SystemMessage.deleteMany = sinon.stub().returns({
exec: sinon.stub().resolves(),
})
ctx.SystemMessageManager.promises.clearMessages()
})
it('should remove the messages from the database', function (ctx) {
ctx.SystemMessage.deleteMany.calledWith({}).should.equal(true)
})
})
})

View File

@@ -1,50 +0,0 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const modulePath = require('path').join(
__dirname,
'../../../../app/src/Features/SystemMessages/SystemMessageManager.js'
)
describe('SystemMessageManager', function () {
beforeEach(function () {
this.messages = ['messages-stub']
this.SystemMessage = {
find: sinon.stub().returns({
exec: sinon.stub().resolves(this.messages),
}),
}
this.SystemMessageManager = SandboxedModule.require(modulePath, {
requires: {
'../../models/SystemMessage': { SystemMessage: this.SystemMessage },
},
})
})
it('should look the messages up in the database on import', function () {
sinon.assert.called(this.SystemMessage.find)
})
describe('getMessage', function () {
beforeEach(function () {
this.SystemMessageManager._cachedMessages = this.messages
this.result = this.SystemMessageManager.getMessages()
})
it('should return the messages', function () {
this.result.should.equal(this.messages)
})
})
describe('clearMessages', function () {
beforeEach(function () {
this.SystemMessage.deleteMany = sinon.stub().returns({
exec: sinon.stub().resolves(),
})
this.SystemMessageManager.promises.clearMessages()
})
it('should remove the messages from the database', function () {
this.SystemMessage.deleteMany.calledWith({}).should.equal(true)
})
})
})

View File

@@ -0,0 +1,412 @@
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import { Writable } from 'stream'
import mongodb from 'mongodb-legacy'
const { ObjectId } = mongodb
const MODULE_PATH =
'../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger.mjs'
describe('UpdateMerger :', function () {
beforeEach(async function (ctx) {
ctx.projectId = 'project_id_here'
ctx.userId = 'mock-user-id'
ctx.randomUUID = 'random-uuid'
ctx.dumpPath = '/dump'
ctx.docPath = ctx.newDocPath = '/folder/doc.tex'
ctx.filePath = ctx.newFilePath = '/folder/file.png'
ctx.existingDocPath = '/folder/other.tex'
ctx.existingFilePath = '/folder/fig1.pdf'
ctx.linkedFileData = { provider: 'url' }
ctx.existingDocs = [{ path: '/main.tex' }, { path: '/folder/other.tex' }]
ctx.existingFiles = [{ path: '/figure.pdf' }, { path: '/folder/fig1.pdf' }]
ctx.fsPath = `${ctx.dumpPath}/${ctx.projectId}_${ctx.randomUUID}`
ctx.fileContents = `\\documentclass{article}
\\usepackage[utf8]{inputenc}
\\title{42}
\\author{Jane Doe}
\\date{June 2011}`
ctx.docLines = ctx.fileContents.split('\n')
ctx.source = 'dropbox'
ctx.updateRequest = new Writable()
ctx.writeStream = new Writable()
ctx.fsPromises = {
unlink: sinon.stub().resolves(),
readFile: sinon.stub().withArgs(ctx.fsPath).resolves(ctx.fileContents),
mkdir: sinon.stub().resolves(),
}
ctx.fs = {
createWriteStream: sinon.stub().returns(ctx.writeStream),
}
ctx.doc = {
_id: new ObjectId(),
rev: 2,
}
ctx.file = {
_id: new ObjectId(),
rev: 6,
}
ctx.folder = {
_id: new ObjectId(),
}
ctx.EditorController = {
promises: {
deleteEntityWithPath: sinon.stub().resolves(new ObjectId()),
upsertDocWithPath: sinon
.stub()
.resolves({ doc: ctx.doc, folder: ctx.folder }),
upsertFileWithPath: sinon
.stub()
.resolves({ file: ctx.file, folder: ctx.folder }),
},
}
ctx.FileTypeManager = {
promises: {
getType: sinon.stub(),
},
}
ctx.crypto = {
randomUUID: sinon.stub().returns(ctx.randomUUID),
}
ctx.ProjectEntityHandler = {
promises: {
getAllEntities: sinon.stub().resolves({
docs: ctx.existingDocs,
files: ctx.existingFiles,
}),
},
}
ctx.Settings = { path: { dumpFolder: ctx.dumpPath } }
ctx.stream = { pipeline: sinon.stub().resolves() }
vi.doMock('fs/promises', () => ({
default: ctx.fsPromises,
}))
vi.doMock('fs', () => ({
default: ctx.fs,
}))
vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({
default: ctx.EditorController,
}))
vi.doMock('../../../../app/src/Features/Uploads/FileTypeManager', () => ({
default: ctx.FileTypeManager,
}))
vi.doMock(
'../../../../app/src/Features/Project/ProjectEntityHandler',
() => ({
default: ctx.ProjectEntityHandler,
})
)
vi.doMock('@overleaf/settings', () => ({
default: ctx.Settings,
}))
vi.doMock('stream/promises', () => ({
pipeline: ctx.stream.pipeline,
}))
vi.doMock('crypto', () => ({
default: ctx.crypto,
}))
ctx.UpdateMerger = (await import(MODULE_PATH)).default
})
describe('mergeUpdate', function () {
describe('doc updates for a new doc', function () {
beforeEach(async function (ctx) {
ctx.FileTypeManager.promises.getType.resolves({
binary: false,
encoding: 'utf-8',
})
ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
ctx.userId,
ctx.projectId,
ctx.docPath,
ctx.updateRequest,
ctx.source
)
})
it('should look at the file contents', function (ctx) {
expect(ctx.FileTypeManager.promises.getType).to.have.been.called
})
it('should process update as doc', function (ctx) {
expect(
ctx.EditorController.promises.upsertDocWithPath
).to.have.been.calledWith(
ctx.projectId,
ctx.docPath,
ctx.docLines,
ctx.source,
ctx.userId
)
})
it('removes the temp file from disk', function (ctx) {
expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
})
it('returns the entity id and rev', function (ctx) {
expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(ctx.mergeUpdateResult.rev).to.equal(2)
})
})
describe('file updates for a new file ', function () {
beforeEach(async function (ctx) {
ctx.FileTypeManager.promises.getType.resolves({ binary: true })
ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
ctx.userId,
ctx.projectId,
ctx.filePath,
ctx.updateRequest,
ctx.source
)
})
it('should look at the file contents', function (ctx) {
expect(ctx.FileTypeManager.promises.getType).to.have.been.called
})
it('should process update as file', function (ctx) {
expect(
ctx.EditorController.promises.upsertFileWithPath
).to.have.been.calledWith(
ctx.projectId,
ctx.filePath,
ctx.fsPath,
null,
ctx.source,
ctx.userId
)
})
it('removes the temp file from disk', function (ctx) {
expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
})
it('returns the entity id and rev', function (ctx) {
expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(ctx.mergeUpdateResult.rev).to.equal(6)
})
})
describe('doc updates for an existing doc', function () {
beforeEach(async function (ctx) {
ctx.FileTypeManager.promises.getType.resolves({
binary: false,
encoding: 'utf-8',
})
ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
ctx.userId,
ctx.projectId,
ctx.existingDocPath,
ctx.updateRequest,
ctx.source
)
})
it('should look at the file contents', function (ctx) {
expect(ctx.FileTypeManager.promises.getType).to.have.been.called
})
it('should process update as doc', function (ctx) {
expect(
ctx.EditorController.promises.upsertDocWithPath
).to.have.been.calledWith(
ctx.projectId,
ctx.existingDocPath,
ctx.docLines,
ctx.source,
ctx.userId
)
})
it('removes the temp file from disk', function (ctx) {
expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
})
it('returns the entity id and rev', function (ctx) {
expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(ctx.mergeUpdateResult.rev).to.equal(2)
})
})
describe('file updates for an existing file', function () {
beforeEach(async function (ctx) {
ctx.FileTypeManager.promises.getType.resolves({ binary: true })
ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
ctx.userId,
ctx.projectId,
ctx.existingFilePath,
ctx.updateRequest,
ctx.source
)
})
it('should look at the file contents', function (ctx) {
expect(ctx.FileTypeManager.promises.getType).to.have.been.called
})
it('should process update as file', function (ctx) {
expect(
ctx.EditorController.promises.upsertFileWithPath
).to.have.been.calledWith(
ctx.projectId,
ctx.existingFilePath,
ctx.fsPath,
null,
ctx.source,
ctx.userId
)
})
it('removes the temp file from disk', function (ctx) {
expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
})
it('returns the entity id and rev', function (ctx) {
expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(ctx.mergeUpdateResult.rev).to.equal(6)
})
})
})
describe('file updates for an existing doc', function () {
beforeEach(async function (ctx) {
ctx.FileTypeManager.promises.getType.resolves({ binary: true })
ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
ctx.userId,
ctx.projectId,
ctx.existingDocPath,
ctx.updateRequest,
ctx.source
)
})
it('should look at the file contents', function (ctx) {
expect(ctx.FileTypeManager.promises.getType).to.have.been.called
})
it('should process update as file', function (ctx) {
expect(
ctx.EditorController.promises.upsertFileWithPath
).to.have.been.calledWith(
ctx.projectId,
ctx.existingDocPath,
ctx.fsPath,
null,
ctx.source,
ctx.userId
)
})
it('removes the temp file from disk', function (ctx) {
expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
})
it('returns the entity id and rev', function (ctx) {
expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(ctx.mergeUpdateResult.rev).to.equal(6)
})
})
describe('doc updates for an existing file', function () {
beforeEach(async function (ctx) {
ctx.FileTypeManager.promises.getType.resolves({ binary: true })
ctx.mergeUpdateResult = await ctx.UpdateMerger.promises.mergeUpdate(
ctx.userId,
ctx.projectId,
ctx.existingFilePath,
ctx.updateRequest,
ctx.source
)
})
it('should look at the file contents', function (ctx) {
expect(ctx.FileTypeManager.promises.getType).to.have.been.called
})
it('should not delete the existing file', function (ctx) {
expect(ctx.EditorController.promises.deleteEntityWithPath).to.not.have
.been.called
})
it('should process update as file', function (ctx) {
expect(
ctx.EditorController.promises.upsertFileWithPath
).to.have.been.calledWith(
ctx.projectId,
ctx.existingFilePath,
ctx.fsPath,
null,
ctx.source,
ctx.userId
)
})
it('removes the temp file from disk', function (ctx) {
expect(ctx.fsPromises.unlink).to.have.been.calledWith(ctx.fsPath)
})
it('returns the entity id and rev', function (ctx) {
expect(ctx.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(ctx.mergeUpdateResult.rev).to.equal(6)
})
})
describe('deleteUpdate', function () {
beforeEach(async function (ctx) {
ctx.deleteUpdateResult = await ctx.UpdateMerger.promises.deleteUpdate(
ctx.userId,
ctx.projectId,
ctx.docPath,
ctx.source
)
})
afterEach(function (ctx) {
delete ctx.deleteUpdateResult
})
it('should delete the entity in the editor controller', function (ctx) {
expect(
ctx.EditorController.promises.deleteEntityWithPath
).to.have.been.calledWith(
ctx.projectId,
ctx.docPath,
ctx.source,
ctx.userId
)
})
it('returns the entity id', function (ctx) {
expect(ctx.deleteUpdateResult).to.be.instanceOf(ObjectId)
})
})
})

View File

@@ -1,387 +0,0 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const { Writable } = require('stream')
const { ObjectId } = require('mongodb-legacy')
const MODULE_PATH =
'../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger.js'
describe('UpdateMerger :', function () {
beforeEach(function () {
this.projectId = 'project_id_here'
this.userId = 'mock-user-id'
this.randomUUID = 'random-uuid'
this.dumpPath = '/dump'
this.docPath = this.newDocPath = '/folder/doc.tex'
this.filePath = this.newFilePath = '/folder/file.png'
this.existingDocPath = '/folder/other.tex'
this.existingFilePath = '/folder/fig1.pdf'
this.linkedFileData = { provider: 'url' }
this.existingDocs = [{ path: '/main.tex' }, { path: '/folder/other.tex' }]
this.existingFiles = [{ path: '/figure.pdf' }, { path: '/folder/fig1.pdf' }]
this.fsPath = `${this.dumpPath}/${this.projectId}_${this.randomUUID}`
this.fileContents = `\\documentclass{article}
\\usepackage[utf8]{inputenc}
\\title{42}
\\author{Jane Doe}
\\date{June 2011}`
this.docLines = this.fileContents.split('\n')
this.source = 'dropbox'
this.updateRequest = new Writable()
this.writeStream = new Writable()
this.fsPromises = {
unlink: sinon.stub().resolves(),
readFile: sinon.stub().withArgs(this.fsPath).resolves(this.fileContents),
mkdir: sinon.stub().resolves(),
}
this.fs = {
createWriteStream: sinon.stub().returns(this.writeStream),
}
this.doc = {
_id: new ObjectId(),
rev: 2,
}
this.file = {
_id: new ObjectId(),
rev: 6,
}
this.folder = {
_id: new ObjectId(),
}
this.EditorController = {
promises: {
deleteEntityWithPath: sinon.stub().resolves(new ObjectId()),
upsertDocWithPath: sinon
.stub()
.resolves({ doc: this.doc, folder: this.folder }),
upsertFileWithPath: sinon
.stub()
.resolves({ file: this.file, folder: this.folder }),
},
}
this.FileTypeManager = {
promises: {
getType: sinon.stub(),
},
}
this.crypto = {
randomUUID: sinon.stub().returns(this.randomUUID),
}
this.ProjectEntityHandler = {
promises: {
getAllEntities: sinon.stub().resolves({
docs: this.existingDocs,
files: this.existingFiles,
}),
},
}
this.Settings = { path: { dumpFolder: this.dumpPath } }
this.stream = { pipeline: sinon.stub().resolves() }
this.UpdateMerger = SandboxedModule.require(MODULE_PATH, {
requires: {
'fs/promises': this.fsPromises,
fs: this.fs,
'../Editor/EditorController': this.EditorController,
'../Uploads/FileTypeManager': this.FileTypeManager,
'../Project/ProjectEntityHandler': this.ProjectEntityHandler,
'@overleaf/settings': this.Settings,
'stream/promises': this.stream,
crypto: this.crypto,
},
})
})
describe('mergeUpdate', function () {
describe('doc updates for a new doc', function () {
beforeEach(async function () {
this.FileTypeManager.promises.getType.resolves({
binary: false,
encoding: 'utf-8',
})
this.mergeUpdateResult = await this.UpdateMerger.promises.mergeUpdate(
this.userId,
this.projectId,
this.docPath,
this.updateRequest,
this.source
)
})
it('should look at the file contents', function () {
expect(this.FileTypeManager.promises.getType).to.have.been.called
})
it('should process update as doc', function () {
expect(
this.EditorController.promises.upsertDocWithPath
).to.have.been.calledWith(
this.projectId,
this.docPath,
this.docLines,
this.source,
this.userId
)
})
it('removes the temp file from disk', function () {
expect(this.fsPromises.unlink).to.have.been.calledWith(this.fsPath)
})
it('returns the entity id and rev', function () {
expect(this.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(this.mergeUpdateResult.rev).to.equal(2)
})
})
describe('file updates for a new file ', function () {
beforeEach(async function () {
this.FileTypeManager.promises.getType.resolves({ binary: true })
this.mergeUpdateResult = await this.UpdateMerger.promises.mergeUpdate(
this.userId,
this.projectId,
this.filePath,
this.updateRequest,
this.source
)
})
it('should look at the file contents', function () {
expect(this.FileTypeManager.promises.getType).to.have.been.called
})
it('should process update as file', function () {
expect(
this.EditorController.promises.upsertFileWithPath
).to.have.been.calledWith(
this.projectId,
this.filePath,
this.fsPath,
null,
this.source,
this.userId
)
})
it('removes the temp file from disk', function () {
expect(this.fsPromises.unlink).to.have.been.calledWith(this.fsPath)
})
it('returns the entity id and rev', function () {
expect(this.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(this.mergeUpdateResult.rev).to.equal(6)
})
})
describe('doc updates for an existing doc', function () {
beforeEach(async function () {
this.FileTypeManager.promises.getType.resolves({
binary: false,
encoding: 'utf-8',
})
this.mergeUpdateResult = await this.UpdateMerger.promises.mergeUpdate(
this.userId,
this.projectId,
this.existingDocPath,
this.updateRequest,
this.source
)
})
it('should look at the file contents', function () {
expect(this.FileTypeManager.promises.getType).to.have.been.called
})
it('should process update as doc', function () {
expect(
this.EditorController.promises.upsertDocWithPath
).to.have.been.calledWith(
this.projectId,
this.existingDocPath,
this.docLines,
this.source,
this.userId
)
})
it('removes the temp file from disk', function () {
expect(this.fsPromises.unlink).to.have.been.calledWith(this.fsPath)
})
it('returns the entity id and rev', function () {
expect(this.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(this.mergeUpdateResult.rev).to.equal(2)
})
})
describe('file updates for an existing file', function () {
beforeEach(async function () {
this.FileTypeManager.promises.getType.resolves({ binary: true })
this.mergeUpdateResult = await this.UpdateMerger.promises.mergeUpdate(
this.userId,
this.projectId,
this.existingFilePath,
this.updateRequest,
this.source
)
})
it('should look at the file contents', function () {
expect(this.FileTypeManager.promises.getType).to.have.been.called
})
it('should process update as file', function () {
expect(
this.EditorController.promises.upsertFileWithPath
).to.have.been.calledWith(
this.projectId,
this.existingFilePath,
this.fsPath,
null,
this.source,
this.userId
)
})
it('removes the temp file from disk', function () {
expect(this.fsPromises.unlink).to.have.been.calledWith(this.fsPath)
})
it('returns the entity id and rev', function () {
expect(this.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(this.mergeUpdateResult.rev).to.equal(6)
})
})
})
describe('file updates for an existing doc', function () {
beforeEach(async function () {
this.FileTypeManager.promises.getType.resolves({ binary: true })
this.mergeUpdateResult = await this.UpdateMerger.promises.mergeUpdate(
this.userId,
this.projectId,
this.existingDocPath,
this.updateRequest,
this.source
)
})
it('should look at the file contents', function () {
expect(this.FileTypeManager.promises.getType).to.have.been.called
})
it('should process update as file', function () {
expect(
this.EditorController.promises.upsertFileWithPath
).to.have.been.calledWith(
this.projectId,
this.existingDocPath,
this.fsPath,
null,
this.source,
this.userId
)
})
it('removes the temp file from disk', function () {
expect(this.fsPromises.unlink).to.have.been.calledWith(this.fsPath)
})
it('returns the entity id and rev', function () {
expect(this.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(this.mergeUpdateResult.rev).to.equal(6)
})
})
describe('doc updates for an existing file', function () {
beforeEach(async function () {
this.FileTypeManager.promises.getType.resolves({ binary: true })
this.mergeUpdateResult = await this.UpdateMerger.promises.mergeUpdate(
this.userId,
this.projectId,
this.existingFilePath,
this.updateRequest,
this.source
)
})
it('should look at the file contents', function () {
expect(this.FileTypeManager.promises.getType).to.have.been.called
})
it('should not delete the existing file', function () {
expect(this.EditorController.promises.deleteEntityWithPath).to.not.have
.been.called
})
it('should process update as file', function () {
expect(
this.EditorController.promises.upsertFileWithPath
).to.have.been.calledWith(
this.projectId,
this.existingFilePath,
this.fsPath,
null,
this.source,
this.userId
)
})
it('removes the temp file from disk', function () {
expect(this.fsPromises.unlink).to.have.been.calledWith(this.fsPath)
})
it('returns the entity id and rev', function () {
expect(this.mergeUpdateResult.entityId).to.be.instanceOf(ObjectId)
expect(this.mergeUpdateResult.rev).to.equal(6)
})
})
describe('deleteUpdate', function () {
beforeEach(async function () {
this.deleteUpdateResult = await this.UpdateMerger.promises.deleteUpdate(
this.userId,
this.projectId,
this.docPath,
this.source
)
})
afterEach(function () {
delete this.deleteUpdateResult
})
it('should delete the entity in the editor controller', function () {
expect(
this.EditorController.promises.deleteEntityWithPath
).to.have.been.calledWith(
this.projectId,
this.docPath,
this.source,
this.userId
)
})
it('returns the entity id', function () {
expect(this.deleteUpdateResult).to.be.instanceOf(ObjectId)
})
})
})

View File

@@ -1,19 +1,20 @@
const SandboxedModule = require('sandboxed-module')
const { expect } = require('chai')
const sinon = require('sinon')
const { ObjectId } = require('mongodb-legacy')
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'
const { ObjectId } = mongodb
const MODULE_PATH = '../../../../app/src/Features/Tutorial/TutorialHandler'
describe('TutorialHandler', function () {
beforeEach(function () {
this.clock = sinon.useFakeTimers()
beforeEach(async function (ctx) {
ctx.clock = sinon.useFakeTimers()
const THIRTY_DAYS_AGO = Date.now() - 30 * 24 * 60 * 60 * 1000
const TOMORROW = Date.now() + 24 * 60 * 60 * 1000
const YESTERDAY = Date.now() - 24 * 60 * 60 * 1000
this.user = {
ctx.user = {
_id: new ObjectId(),
completedTutorials: {
'legacy-format': new Date(Date.now() - 1000),
@@ -42,28 +43,26 @@ describe('TutorialHandler', function () {
},
}
this.UserUpdater = {
ctx.UserUpdater = {
promises: {
updateUser: sinon.stub().resolves(),
},
}
this.TutorialHandler = SandboxedModule.require(MODULE_PATH, {
requires: {
'../User/UserUpdater': this.UserUpdater,
},
})
vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({
default: ctx.UserUpdater,
}))
ctx.TutorialHandler = (await import(MODULE_PATH)).default
})
afterEach(function () {
this.clock.restore()
afterEach(function (ctx) {
ctx.clock.restore()
})
describe('getInactiveTutorials', function () {
it('returns all recorded tutorials except when they were posponed long ago', function () {
const hiddenTutorials = this.TutorialHandler.getInactiveTutorials(
this.user
)
it('returns all recorded tutorials except when they were posponed long ago', function (ctx) {
const hiddenTutorials = ctx.TutorialHandler.getInactiveTutorials(ctx.user)
expect(hiddenTutorials).to.have.members([
'legacy-format',
'completed',
@@ -73,7 +72,7 @@ describe('TutorialHandler', function () {
expect(hiddenTutorials).to.have.lengthOf(4)
const shownTutorials = Object.keys(this.user.completedTutorials).filter(
const shownTutorials = Object.keys(ctx.user.completedTutorials).filter(
key => !hiddenTutorials.includes(key)
)