mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-27 11:01:56 +02:00
Merge pull request #28273 from overleaf/ac-some-web-esm-migration
[web] Convert some Features files to ES modules (part 1) GitOrigin-RevId: d19b024efad315143e022143e2a2683df8071744
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
const { fetchJson } = require('@overleaf/fetch-utils')
|
||||
const logger = require('@overleaf/logger')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const DeviceHistory = require('./DeviceHistory')
|
||||
const AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
const { expressify } = require('@overleaf/promise-utils')
|
||||
const EmailsHelper = require('../Helpers/EmailHelper')
|
||||
import { fetchJson } from '@overleaf/fetch-utils'
|
||||
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 AuthenticationController from '../Authentication/AuthenticationController.js'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import EmailsHelper from '../Helpers/EmailHelper.js'
|
||||
|
||||
function respondInvalidCaptcha(req, res) {
|
||||
res.status(400).json({
|
||||
@@ -114,7 +114,7 @@ function validateCaptcha(action) {
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
respondInvalidCaptcha,
|
||||
validateCaptcha,
|
||||
canSkipCaptcha: expressify(canSkipCaptcha),
|
||||
@@ -1,11 +1,11 @@
|
||||
const { expressify } = require('@overleaf/promise-utils')
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
const ChatApiHandler = require('./ChatApiHandler')
|
||||
const EditorRealTimeController = require('../Editor/EditorRealTimeController')
|
||||
const SessionManager = require('../Authentication/SessionManager')
|
||||
const UserInfoManager = require('../User/UserInfoManager')
|
||||
const UserInfoController = require('../User/UserInfoController')
|
||||
const ChatManager = require('./ChatManager')
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import Modules from '../../infrastructure/Modules.js'
|
||||
import ChatApiHandler from './ChatApiHandler.js'
|
||||
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'
|
||||
|
||||
async function sendMessage(req, res) {
|
||||
const { project_id: projectId } = req.params
|
||||
@@ -48,7 +48,7 @@ async function getMessages(req, res) {
|
||||
res.json(messages)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
sendMessage: expressify(sendMessage),
|
||||
getMessages: expressify(getMessages),
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import HttpErrorHandler from '../../Features/Errors/HttpErrorHandler.js'
|
||||
import mongodb from 'mongodb-legacy'
|
||||
import CollaboratorsHandler from './CollaboratorsHandler.js'
|
||||
import CollaboratorsGetter from './CollaboratorsGetter.js'
|
||||
import OwnershipTransferHandler from './OwnershipTransferHandler.js'
|
||||
import OwnershipTransferHandler from './OwnershipTransferHandler.mjs'
|
||||
import SessionManager from '../Authentication/SessionManager.js'
|
||||
import EditorRealTimeController from '../Editor/EditorRealTimeController.js'
|
||||
import TagsHandler from '../Tags/TagsHandler.js'
|
||||
|
||||
@@ -12,42 +12,6 @@ const ProjectEditorHandler = require('../Project/ProjectEditorHandler')
|
||||
const Sources = require('../Authorization/Sources')
|
||||
const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
|
||||
|
||||
module.exports = {
|
||||
getMemberIdsWithPrivilegeLevels: callbackify(getMemberIdsWithPrivilegeLevels),
|
||||
getMemberIds: callbackify(getMemberIds),
|
||||
getInvitedMemberIds: callbackify(getInvitedMemberIds),
|
||||
getInvitedMembersWithPrivilegeLevelsFromFields: callbackify(
|
||||
getInvitedMembersWithPrivilegeLevelsFromFields
|
||||
),
|
||||
getMemberIdPrivilegeLevel: callbackify(getMemberIdPrivilegeLevel),
|
||||
getProjectsUserIsMemberOf: callbackify(getProjectsUserIsMemberOf),
|
||||
dangerouslyGetAllProjectsUserIsMemberOf: callbackify(
|
||||
dangerouslyGetAllProjectsUserIsMemberOf
|
||||
),
|
||||
isUserInvitedMemberOfProject: callbackify(isUserInvitedMemberOfProject),
|
||||
getPublicShareTokens: callbackify(getPublicShareTokens),
|
||||
userIsTokenMember: callbackify(userIsTokenMember),
|
||||
getAllInvitedMembers: callbackify(getAllInvitedMembers),
|
||||
promises: {
|
||||
getProjectAccess,
|
||||
getMemberIdsWithPrivilegeLevels,
|
||||
getMemberIds,
|
||||
getInvitedMemberIds,
|
||||
getInvitedMembersWithPrivilegeLevelsFromFields,
|
||||
getMemberIdPrivilegeLevel,
|
||||
getInvitedEditCollaboratorCount,
|
||||
getInvitedPendingEditorCount,
|
||||
getProjectsUserIsMemberOf,
|
||||
dangerouslyGetAllProjectsUserIsMemberOf,
|
||||
isUserInvitedMemberOfProject,
|
||||
isUserInvitedReadWriteMemberOfProject,
|
||||
getPublicShareTokens,
|
||||
userIsTokenMember,
|
||||
userIsReadWriteTokenMember,
|
||||
getAllInvitedMembers,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef ProjectMember
|
||||
* @property {string} id
|
||||
@@ -241,8 +205,6 @@ class ProjectAccess {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.ProjectAccess = ProjectAccess
|
||||
|
||||
async function getProjectAccess(projectId) {
|
||||
const project = await ProjectGetter.promises.getProject(projectId, {
|
||||
owner_ref: 1,
|
||||
@@ -577,3 +539,40 @@ async function _loadMembers(members) {
|
||||
})
|
||||
.filter(r => r != null)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMemberIdsWithPrivilegeLevels: callbackify(getMemberIdsWithPrivilegeLevels),
|
||||
getMemberIds: callbackify(getMemberIds),
|
||||
getInvitedMemberIds: callbackify(getInvitedMemberIds),
|
||||
getInvitedMembersWithPrivilegeLevelsFromFields: callbackify(
|
||||
getInvitedMembersWithPrivilegeLevelsFromFields
|
||||
),
|
||||
getMemberIdPrivilegeLevel: callbackify(getMemberIdPrivilegeLevel),
|
||||
getProjectsUserIsMemberOf: callbackify(getProjectsUserIsMemberOf),
|
||||
dangerouslyGetAllProjectsUserIsMemberOf: callbackify(
|
||||
dangerouslyGetAllProjectsUserIsMemberOf
|
||||
),
|
||||
isUserInvitedMemberOfProject: callbackify(isUserInvitedMemberOfProject),
|
||||
getPublicShareTokens: callbackify(getPublicShareTokens),
|
||||
userIsTokenMember: callbackify(userIsTokenMember),
|
||||
getAllInvitedMembers: callbackify(getAllInvitedMembers),
|
||||
promises: {
|
||||
getProjectAccess,
|
||||
getMemberIdsWithPrivilegeLevels,
|
||||
getMemberIds,
|
||||
getInvitedMemberIds,
|
||||
getInvitedMembersWithPrivilegeLevelsFromFields,
|
||||
getMemberIdPrivilegeLevel,
|
||||
getInvitedEditCollaboratorCount,
|
||||
getInvitedPendingEditorCount,
|
||||
getProjectsUserIsMemberOf,
|
||||
dangerouslyGetAllProjectsUserIsMemberOf,
|
||||
isUserInvitedMemberOfProject,
|
||||
isUserInvitedReadWriteMemberOfProject,
|
||||
getPublicShareTokens,
|
||||
userIsTokenMember,
|
||||
userIsReadWriteTokenMember,
|
||||
getAllInvitedMembers,
|
||||
},
|
||||
ProjectAccess,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
|
||||
import CollaboratorsInviteController from './CollaboratorsInviteController.mjs'
|
||||
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
|
||||
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
|
||||
import CaptchaMiddleware from '../Captcha/CaptchaMiddleware.js'
|
||||
import CaptchaMiddleware from '../Captcha/CaptchaMiddleware.mjs'
|
||||
import AnalyticsRegistrationSourceMiddleware from '../Analytics/AnalyticsRegistrationSourceMiddleware.js'
|
||||
import { Joi, validate } from '../../infrastructure/Validation.js'
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
const logger = require('@overleaf/logger')
|
||||
const { Project } = require('../../models/Project')
|
||||
const ProjectGetter = require('../Project/ProjectGetter')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const CollaboratorsHandler = require('./CollaboratorsHandler')
|
||||
const EmailHandler = require('../Email/EmailHandler')
|
||||
const Errors = require('../Errors/Errors')
|
||||
const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
|
||||
const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
|
||||
const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
|
||||
const AnalyticsManager = require('../Analytics/AnalyticsManager')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const TagsHandler = require('../Tags/TagsHandler')
|
||||
const { promiseMapWithLimit } = require('@overleaf/promise-utils')
|
||||
import logger from '@overleaf/logger'
|
||||
import { Project } from '../../models/Project.js'
|
||||
import ProjectGetter from '../Project/ProjectGetter.js'
|
||||
import UserGetter from '../User/UserGetter.js'
|
||||
import CollaboratorsHandler from './CollaboratorsHandler.js'
|
||||
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 AnalyticsManager from '../Analytics/AnalyticsManager.js'
|
||||
import OError from '@overleaf/o-error'
|
||||
import TagsHandler from '../Tags/TagsHandler.js'
|
||||
import { promiseMapWithLimit } from '@overleaf/promise-utils'
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
promises: {
|
||||
transferOwnership,
|
||||
transferAllProjectsToUser,
|
||||
@@ -1,18 +1,18 @@
|
||||
const { NotFoundError, ResourceGoneError } = require('../Errors/Errors')
|
||||
const {
|
||||
import { NotFoundError, ResourceGoneError } from '../Errors/Errors.js'
|
||||
import {
|
||||
fetchStreamWithResponse,
|
||||
RequestFailedError,
|
||||
} = require('@overleaf/fetch-utils')
|
||||
const Path = require('path')
|
||||
const { pipeline } = require('stream/promises')
|
||||
const logger = require('@overleaf/logger')
|
||||
const ClsiCacheManager = require('./ClsiCacheManager')
|
||||
const CompileController = require('./CompileController')
|
||||
const { expressify } = require('@overleaf/promise-utils')
|
||||
const ClsiCacheHandler = require('./ClsiCacheHandler')
|
||||
const ProjectGetter = require('../Project/ProjectGetter')
|
||||
const { MeteredStream } = require('@overleaf/stream-utils')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
} from '@overleaf/fetch-utils'
|
||||
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 { expressify } from '@overleaf/promise-utils'
|
||||
import ClsiCacheHandler from './ClsiCacheHandler.js'
|
||||
import ProjectGetter from '../Project/ProjectGetter.js'
|
||||
import { MeteredStream } from '@overleaf/stream-utils'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
|
||||
/**
|
||||
* Download a file from a specific build on the clsi-cache.
|
||||
@@ -150,7 +150,7 @@ async function getLatestBuildFromCache(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
downloadFromCache: expressify(downloadFromCache),
|
||||
getLatestBuildFromCache: expressify(getLatestBuildFromCache),
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
const ProjectDeleter = require('../Project/ProjectDeleter')
|
||||
const EditorController = require('./EditorController')
|
||||
const ProjectGetter = require('../Project/ProjectGetter')
|
||||
const AuthorizationManager = require('../Authorization/AuthorizationManager')
|
||||
const ProjectEditorHandler = require('../Project/ProjectEditorHandler')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const CollaboratorsInviteGetter = require('../Collaborators/CollaboratorsInviteGetter')
|
||||
const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
|
||||
const SessionManager = require('../Authentication/SessionManager')
|
||||
const Errors = require('../Errors/Errors')
|
||||
const { expressify } = require('@overleaf/promise-utils')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const { ProjectAccess } = require('../Collaborators/CollaboratorsGetter')
|
||||
import ProjectDeleter from '../Project/ProjectDeleter.js'
|
||||
import EditorController from './EditorController.js'
|
||||
import ProjectGetter from '../Project/ProjectGetter.js'
|
||||
import AuthorizationManager from '../Authorization/AuthorizationManager.js'
|
||||
import ProjectEditorHandler from '../Project/ProjectEditorHandler.js'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import CollaboratorsInviteGetter from '../Collaborators/CollaboratorsInviteGetter.js'
|
||||
import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
|
||||
import SessionManager from '../Authentication/SessionManager.js'
|
||||
import Errors from '../Errors/Errors.js'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import Settings from '@overleaf/settings'
|
||||
import CollaboratorsGetter from '../Collaborators/CollaboratorsGetter.js'
|
||||
|
||||
module.exports = {
|
||||
const ProjectAccess = CollaboratorsGetter.ProjectAccess
|
||||
|
||||
export default {
|
||||
joinProject: expressify(joinProject),
|
||||
addDoc: expressify(addDoc),
|
||||
addFolder: expressify(addFolder),
|
||||
@@ -1,4 +1,4 @@
|
||||
import EditorHttpController from './EditorHttpController.js'
|
||||
import EditorHttpController from './EditorHttpController.mjs'
|
||||
import AuthenticationController from '../Authentication/AuthenticationController.js'
|
||||
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.js'
|
||||
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const { isZodErrorLike, fromZodError } = require('zod-validation-error')
|
||||
const Errors = require('./Errors')
|
||||
const SessionManager = require('../Authentication/SessionManager')
|
||||
const SamlLogHandler = require('../SamlLog/SamlLogHandler')
|
||||
const HttpErrorHandler = require('./HttpErrorHandler')
|
||||
const { plainTextResponse } = require('../../infrastructure/Response')
|
||||
const { expressifyErrorHandler } = require('@overleaf/promise-utils')
|
||||
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 HttpErrorHandler from './HttpErrorHandler.js'
|
||||
import { plainTextResponse } from '../../infrastructure/Response.js'
|
||||
import { expressifyErrorHandler } from '@overleaf/promise-utils'
|
||||
|
||||
function notFound(req, res) {
|
||||
res.status(404)
|
||||
@@ -135,7 +135,7 @@ function handleApiError(err, req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
notFound,
|
||||
forbidden,
|
||||
serverError,
|
||||
@@ -1,4 +1,4 @@
|
||||
const { GlobalMetric } = require('../../models/GlobalMetric')
|
||||
import { GlobalMetric } from '../../models/GlobalMetric.js'
|
||||
/**
|
||||
* A Generic collection used to track metrics shared across the entirety of the application
|
||||
* examples:
|
||||
@@ -31,7 +31,7 @@ async function incrementMetric(key, value = 1) {
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
getMetric,
|
||||
setMetric,
|
||||
incrementMetric,
|
||||
@@ -1,28 +1,30 @@
|
||||
// @ts-check
|
||||
|
||||
const { setTimeout } = require('timers/promises')
|
||||
const { pipeline } = require('stream/promises')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const logger = require('@overleaf/logger')
|
||||
const { expressify } = require('@overleaf/promise-utils')
|
||||
const {
|
||||
import { setTimeout } from 'node:timers/promises'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import OError from '@overleaf/o-error'
|
||||
import logger from '@overleaf/logger'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
|
||||
import {
|
||||
fetchStream,
|
||||
fetchStreamWithResponse,
|
||||
fetchJson,
|
||||
fetchNothing,
|
||||
RequestFailedError,
|
||||
} = require('@overleaf/fetch-utils')
|
||||
const settings = require('@overleaf/settings')
|
||||
const SessionManager = require('../Authentication/SessionManager')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const ProjectGetter = require('../Project/ProjectGetter')
|
||||
const Errors = require('../Errors/Errors')
|
||||
const HistoryManager = require('./HistoryManager')
|
||||
const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
|
||||
const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler')
|
||||
const RestoreManager = require('./RestoreManager')
|
||||
const { prepareZipAttachment } = require('../../infrastructure/Response')
|
||||
const Features = require('../../infrastructure/Features')
|
||||
} from '@overleaf/fetch-utils'
|
||||
|
||||
import settings from '@overleaf/settings'
|
||||
import SessionManager from '../Authentication/SessionManager.js'
|
||||
import UserGetter from '../User/UserGetter.js'
|
||||
import ProjectGetter from '../Project/ProjectGetter.js'
|
||||
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 { prepareZipAttachment } from '../../infrastructure/Response.js'
|
||||
import Features from '../../infrastructure/Features.js'
|
||||
|
||||
// Number of seconds after which the browser should send a request to revalidate
|
||||
// blobs
|
||||
@@ -508,7 +510,7 @@ function isPrematureClose(err) {
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
getBlob: expressify(getBlob),
|
||||
headBlob: expressify(headBlob),
|
||||
proxyToHistoryApi: expressify(proxyToHistoryApi),
|
||||
@@ -6,7 +6,7 @@ import { RateLimiter } from '../../infrastructure/RateLimiter.js'
|
||||
import AuthenticationController from '../Authentication/AuthenticationController.js'
|
||||
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.js'
|
||||
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
|
||||
import HistoryController from './HistoryController.js'
|
||||
import HistoryController from './HistoryController.mjs'
|
||||
|
||||
const rateLimiters = {
|
||||
downloadProjectRevision: new RateLimiter('download-project-revision', {
|
||||
|
||||
@@ -41,7 +41,7 @@ import ReferencesHandler from '../References/ReferencesHandler.mjs'
|
||||
import EditorRealTimeController from '../Editor/EditorRealTimeController.js'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import ProjectOutputFileAgent from './ProjectOutputFileAgent.mjs'
|
||||
import ProjectFileAgent from './ProjectFileAgent.js'
|
||||
import ProjectFileAgent from './ProjectFileAgent.mjs'
|
||||
import UrlAgent from './UrlAgent.mjs'
|
||||
|
||||
let LinkedFilesController
|
||||
|
||||
@@ -10,23 +10,26 @@
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let ProjectFileAgent
|
||||
const AuthorizationManager = require('../Authorization/AuthorizationManager')
|
||||
const ProjectLocator = require('../Project/ProjectLocator')
|
||||
const DocstoreManager = require('../Docstore/DocstoreManager')
|
||||
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||
const _ = require('lodash')
|
||||
const LinkedFilesHandler = require('./LinkedFilesHandler')
|
||||
const {
|
||||
import AuthorizationManager from '../Authorization/AuthorizationManager.js'
|
||||
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 {
|
||||
BadDataError,
|
||||
AccessDeniedError,
|
||||
BadEntityTypeError,
|
||||
SourceFileNotFoundError,
|
||||
} = require('./LinkedFilesErrors')
|
||||
const { promisify } = require('@overleaf/promise-utils')
|
||||
const HistoryManager = require('../History/HistoryManager')
|
||||
} from './LinkedFilesErrors.js'
|
||||
|
||||
module.exports = ProjectFileAgent = {
|
||||
import { promisify } from '@overleaf/promise-utils'
|
||||
import HistoryManager from '../History/HistoryManager.js'
|
||||
|
||||
let ProjectFileAgent
|
||||
|
||||
export default ProjectFileAgent = {
|
||||
createLinkedFile(
|
||||
projectId,
|
||||
linkedFileData,
|
||||
@@ -1,7 +1,7 @@
|
||||
import AuthorizationManager from '../Authorization/AuthorizationManager.js'
|
||||
import CompileManager from '../Compile/CompileManager.js'
|
||||
import ClsiManager from '../Compile/ClsiManager.js'
|
||||
import ProjectFileAgent from './ProjectFileAgent.js'
|
||||
import ProjectFileAgent from './ProjectFileAgent.mjs'
|
||||
import _ from 'lodash'
|
||||
import {
|
||||
CompileFailedError,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PasswordResetController from './PasswordResetController.mjs'
|
||||
import AuthenticationController from '../Authentication/AuthenticationController.js'
|
||||
import CaptchaMiddleware from '../../Features/Captcha/CaptchaMiddleware.js'
|
||||
import CaptchaMiddleware from '../../Features/Captcha/CaptchaMiddleware.mjs'
|
||||
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
|
||||
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.js'
|
||||
import { Joi, validate } from '../../infrastructure/Validation.js'
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const { ObjectId } = require('mongodb-legacy')
|
||||
const { Project } = require('../../models/Project')
|
||||
const { callbackifyAll } = require('@overleaf/promise-utils')
|
||||
import mongodb from 'mongodb-legacy'
|
||||
|
||||
import { Project } from '../../models/Project.js'
|
||||
import { callbackifyAll } from '@overleaf/promise-utils'
|
||||
|
||||
const { ObjectId } = mongodb
|
||||
|
||||
const ProjectCollabratecDetailsHandler = {
|
||||
async initializeCollabratecProject(
|
||||
@@ -94,7 +97,7 @@ const ProjectCollabratecDetailsHandler = {
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
...callbackifyAll(ProjectCollabratecDetailsHandler),
|
||||
promises: ProjectCollabratecDetailsHandler,
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import Features from '../../infrastructure/Features.js'
|
||||
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
|
||||
import Path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import ErrorController from '../Errors/ErrorController.js'
|
||||
import ErrorController from '../Errors/ErrorController.mjs'
|
||||
import SessionManager from '../Authentication/SessionManager.js'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import logger from '@overleaf/logger'
|
||||
|
||||
@@ -5,7 +5,7 @@ import TeamInvitesHandler from './TeamInvitesHandler.js'
|
||||
import SessionManager from '../Authentication/SessionManager.js'
|
||||
import SubscriptionLocator from './SubscriptionLocator.js'
|
||||
import SubscriptionHelper from './SubscriptionHelper.js'
|
||||
import ErrorController from '../Errors/ErrorController.js'
|
||||
import ErrorController from '../Errors/ErrorController.mjs'
|
||||
import EmailHelper from '../Helpers/EmailHelper.js'
|
||||
import UserGetter from '../User/UserGetter.js'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
|
||||
@@ -25,7 +25,7 @@ import RedirectManager from './RedirectManager.js'
|
||||
import translations from './Translations.js'
|
||||
import Views from './Views.js'
|
||||
import Features from './Features.js'
|
||||
import ErrorController from '../Features/Errors/ErrorController.js'
|
||||
import ErrorController from '../Features/Errors/ErrorController.mjs'
|
||||
import HttpErrorHandler from '../Features/Errors/HttpErrorHandler.js'
|
||||
import UserSessionsManager from '../Features/User/UserSessionsManager.js'
|
||||
import AuthenticationController from '../Features/Authentication/AuthenticationController.js'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import AdminController from './Features/ServerAdmin/AdminController.js'
|
||||
import ErrorController from './Features/Errors/ErrorController.js'
|
||||
import ErrorController from './Features/Errors/ErrorController.mjs'
|
||||
import Features from './infrastructure/Features.js'
|
||||
import ProjectController from './Features/Project/ProjectController.js'
|
||||
import ProjectApiController from './Features/Project/ProjectApiController.mjs'
|
||||
@@ -34,7 +34,7 @@ import HistoryRouter from './Features/History/HistoryRouter.mjs'
|
||||
import ExportsController from './Features/Exports/ExportsController.mjs'
|
||||
import PasswordResetRouter from './Features/PasswordReset/PasswordResetRouter.mjs'
|
||||
import StaticPagesRouter from './Features/StaticPages/StaticPagesRouter.mjs'
|
||||
import ChatController from './Features/Chat/ChatController.js'
|
||||
import ChatController from './Features/Chat/ChatController.mjs'
|
||||
import Modules from './infrastructure/Modules.js'
|
||||
import {
|
||||
RateLimiter,
|
||||
@@ -57,7 +57,7 @@ import UserMembershipRouter from './Features/UserMembership/UserMembershipRouter
|
||||
import SystemMessageController from './Features/SystemMessages/SystemMessageController.js'
|
||||
import AnalyticsRegistrationSourceMiddleware from './Features/Analytics/AnalyticsRegistrationSourceMiddleware.js'
|
||||
import AnalyticsUTMTrackingMiddleware from './Features/Analytics/AnalyticsUTMTrackingMiddleware.mjs'
|
||||
import CaptchaMiddleware from './Features/Captcha/CaptchaMiddleware.js'
|
||||
import CaptchaMiddleware from './Features/Captcha/CaptchaMiddleware.mjs'
|
||||
import { Joi, validate } from './infrastructure/Validation.js'
|
||||
import UnsupportedBrowserMiddleware from './infrastructure/UnsupportedBrowserMiddleware.js'
|
||||
import logger from '@overleaf/logger'
|
||||
@@ -65,7 +65,7 @@ import _ from 'lodash'
|
||||
import { plainTextResponse } from './infrastructure/Response.js'
|
||||
import PublicAccessLevels from './Features/Authorization/PublicAccessLevels.js'
|
||||
import SocketDiagnostics from './Features/SocketDiagnostics/SocketDiagnostics.mjs'
|
||||
import ClsiCacheController from './Features/Compile/ClsiCacheController.js'
|
||||
import ClsiCacheController from './Features/Compile/ClsiCacheController.mjs'
|
||||
import AsyncLocalStorage from './infrastructure/AsyncLocalStorage.js'
|
||||
|
||||
const { renderUnsupportedBrowserPage, unsupportedBrowserMiddleware } =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ObjectId } from '../../../app/src/infrastructure/mongodb.js'
|
||||
import minimist from 'minimist'
|
||||
import OwnershipTransferHandler from '../../../app/src/Features/Collaborators/OwnershipTransferHandler.js'
|
||||
import OwnershipTransferHandler from '../../../app/src/Features/Collaborators/OwnershipTransferHandler.mjs'
|
||||
import UserGetter from '../../../app/src/Features/User/UserGetter.js'
|
||||
import EmailHelper from '../../../app/src/Features/Helpers/EmailHelper.js'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import UserGetter from '../../../../app/src/Features/User/UserGetter.js'
|
||||
import UserRegistrationHandler from '../../../../app/src/Features/User/UserRegistrationHandler.js'
|
||||
import ErrorController from '../../../../app/src/Features/Errors/ErrorController.js'
|
||||
import ErrorController from '../../../../app/src/Features/Errors/ErrorController.mjs'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
|
||||
const __dirname = Path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('UserActivateController', function () {
|
||||
)
|
||||
|
||||
vi.doMock(
|
||||
'../../../../../app/src/Features/Errors/ErrorController.js',
|
||||
'../../../../../app/src/Features/Errors/ErrorController.mjs',
|
||||
() => ({
|
||||
default: ctx.ErrorController,
|
||||
})
|
||||
|
||||
152
services/web/test/unit/src/Chat/ChatController.test.mjs
Normal file
152
services/web/test/unit/src/Chat/ChatController.test.mjs
Normal file
@@ -0,0 +1,152 @@
|
||||
import { vi } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
|
||||
const MODULE_PATH = '../../../../app/src/Features/Chat/ChatController.mjs'
|
||||
|
||||
describe('ChatController', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.user_id = 'mock-user-id'
|
||||
ctx.settings = {}
|
||||
ctx.ChatApiHandler = { promises: {} }
|
||||
ctx.ChatManager = { promises: {} }
|
||||
ctx.EditorRealTimeController = { emitToRoom: sinon.stub() }
|
||||
ctx.SessionManager = {
|
||||
getLoggedInUserId: sinon.stub().returns(ctx.user_id),
|
||||
}
|
||||
ctx.UserInfoManager = {
|
||||
promises: {},
|
||||
}
|
||||
ctx.UserInfoController = {}
|
||||
ctx.Modules = {
|
||||
promises: {
|
||||
hooks: {
|
||||
fire: sinon.stub().resolves(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
vi.doMock('@overleaf/settings', () => ({
|
||||
default: ctx.settings,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../../app/src/Features/Chat/ChatApiHandler.js', () => ({
|
||||
default: ctx.ChatApiHandler,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../../app/src/Features/Chat/ChatManager.js', () => ({
|
||||
default: ctx.ChatManager,
|
||||
}))
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Editor/EditorRealTimeController.js',
|
||||
() => ({
|
||||
default: ctx.EditorRealTimeController,
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Authentication/SessionManager.js',
|
||||
() => ({
|
||||
default: ctx.SessionManager,
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock('../../../../app/src/Features/User/UserInfoManager.js', () => ({
|
||||
default: ctx.UserInfoManager,
|
||||
}))
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/User/UserInfoController.js',
|
||||
() => ({
|
||||
default: ctx.UserInfoController,
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock('../../../../app/src/infrastructure/Modules.js', () => ({
|
||||
default: ctx.Modules,
|
||||
}))
|
||||
|
||||
ctx.ChatController = (await import(MODULE_PATH)).default
|
||||
ctx.req = {
|
||||
params: {
|
||||
project_id: ctx.project_id,
|
||||
},
|
||||
}
|
||||
ctx.res = {
|
||||
json: sinon.stub(),
|
||||
send: sinon.stub(),
|
||||
sendStatus: sinon.stub(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendMessage', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.req.body = { content: (ctx.content = 'message-content') }
|
||||
ctx.UserInfoManager.promises.getPersonalInfo = sinon
|
||||
.stub()
|
||||
.resolves((ctx.user = { unformatted: 'user' }))
|
||||
ctx.UserInfoController.formatPersonalInfo = sinon
|
||||
.stub()
|
||||
.returns((ctx.formatted_user = { formatted: 'user' }))
|
||||
ctx.ChatApiHandler.promises.sendGlobalMessage = sinon
|
||||
.stub()
|
||||
.resolves((ctx.message = { mock: 'message', user_id: ctx.user_id }))
|
||||
await ctx.ChatController.sendMessage(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should look up the user', function (ctx) {
|
||||
ctx.UserInfoManager.promises.getPersonalInfo
|
||||
.calledWith(ctx.user_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should format and inject the user into the message', function (ctx) {
|
||||
ctx.UserInfoController.formatPersonalInfo
|
||||
.calledWith(ctx.user)
|
||||
.should.equal(true)
|
||||
ctx.message.user.should.deep.equal(ctx.formatted_user)
|
||||
})
|
||||
|
||||
it('should tell the chat handler about the message', function (ctx) {
|
||||
ctx.ChatApiHandler.promises.sendGlobalMessage
|
||||
.calledWith(ctx.project_id, ctx.user_id, ctx.content)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should tell the editor real time controller about the update with the data from the chat handler', function (ctx) {
|
||||
ctx.EditorRealTimeController.emitToRoom
|
||||
.calledWith(ctx.project_id, 'new-chat-message', ctx.message)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return a 204 status code', function (ctx) {
|
||||
ctx.res.sendStatus.calledWith(204).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMessages', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.req.query = {
|
||||
limit: (ctx.limit = '30'),
|
||||
before: (ctx.before = '12345'),
|
||||
}
|
||||
ctx.ChatManager.promises.injectUserInfoIntoThreads = sinon
|
||||
.stub()
|
||||
.resolves()
|
||||
ctx.ChatApiHandler.promises.getGlobalMessages = sinon
|
||||
.stub()
|
||||
.resolves((ctx.messages = ['mock', 'messages']))
|
||||
await ctx.ChatController.getMessages(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should ask the chat handler about the request', function (ctx) {
|
||||
ctx.ChatApiHandler.promises.getGlobalMessages
|
||||
.calledWith(ctx.project_id, ctx.limit, ctx.before)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the messages', function (ctx) {
|
||||
ctx.res.json.calledWith(ctx.messages).should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,125 +0,0 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const path = require('path')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const MODULE_PATH = path.join(
|
||||
__dirname,
|
||||
'../../../../app/src/Features/Chat/ChatController'
|
||||
)
|
||||
|
||||
describe('ChatController', function () {
|
||||
beforeEach(function () {
|
||||
this.user_id = 'mock-user-id'
|
||||
this.settings = {}
|
||||
this.ChatApiHandler = { promises: {} }
|
||||
this.ChatManager = { promises: {} }
|
||||
this.EditorRealTimeController = { emitToRoom: sinon.stub() }
|
||||
this.SessionManager = {
|
||||
getLoggedInUserId: sinon.stub().returns(this.user_id),
|
||||
}
|
||||
this.UserInfoManager = {
|
||||
promises: {},
|
||||
}
|
||||
this.UserInfoController = {}
|
||||
this.Modules = {
|
||||
promises: {
|
||||
hooks: {
|
||||
fire: sinon.stub().resolves(),
|
||||
},
|
||||
},
|
||||
}
|
||||
this.ChatController = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'@overleaf/settings': this.settings,
|
||||
'./ChatApiHandler': this.ChatApiHandler,
|
||||
'./ChatManager': this.ChatManager,
|
||||
'../Editor/EditorRealTimeController': this.EditorRealTimeController,
|
||||
'../Authentication/SessionManager': this.SessionManager,
|
||||
'../User/UserInfoManager': this.UserInfoManager,
|
||||
'../User/UserInfoController': this.UserInfoController,
|
||||
'../../infrastructure/Modules': this.Modules,
|
||||
},
|
||||
})
|
||||
this.req = {
|
||||
params: {
|
||||
project_id: this.project_id,
|
||||
},
|
||||
}
|
||||
this.res = {
|
||||
json: sinon.stub(),
|
||||
send: sinon.stub(),
|
||||
sendStatus: sinon.stub(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendMessage', function () {
|
||||
beforeEach(async function () {
|
||||
this.req.body = { content: (this.content = 'message-content') }
|
||||
this.UserInfoManager.promises.getPersonalInfo = sinon
|
||||
.stub()
|
||||
.resolves((this.user = { unformatted: 'user' }))
|
||||
this.UserInfoController.formatPersonalInfo = sinon
|
||||
.stub()
|
||||
.returns((this.formatted_user = { formatted: 'user' }))
|
||||
this.ChatApiHandler.promises.sendGlobalMessage = sinon
|
||||
.stub()
|
||||
.resolves((this.message = { mock: 'message', user_id: this.user_id }))
|
||||
await this.ChatController.sendMessage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should look up the user', function () {
|
||||
this.UserInfoManager.promises.getPersonalInfo
|
||||
.calledWith(this.user_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should format and inject the user into the message', function () {
|
||||
this.UserInfoController.formatPersonalInfo
|
||||
.calledWith(this.user)
|
||||
.should.equal(true)
|
||||
this.message.user.should.deep.equal(this.formatted_user)
|
||||
})
|
||||
|
||||
it('should tell the chat handler about the message', function () {
|
||||
this.ChatApiHandler.promises.sendGlobalMessage
|
||||
.calledWith(this.project_id, this.user_id, this.content)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should tell the editor real time controller about the update with the data from the chat handler', function () {
|
||||
this.EditorRealTimeController.emitToRoom
|
||||
.calledWith(this.project_id, 'new-chat-message', this.message)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return a 204 status code', function () {
|
||||
this.res.sendStatus.calledWith(204).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMessages', function () {
|
||||
beforeEach(async function () {
|
||||
this.req.query = {
|
||||
limit: (this.limit = '30'),
|
||||
before: (this.before = '12345'),
|
||||
}
|
||||
this.ChatManager.promises.injectUserInfoIntoThreads = sinon
|
||||
.stub()
|
||||
.resolves()
|
||||
this.ChatApiHandler.promises.getGlobalMessages = sinon
|
||||
.stub()
|
||||
.resolves((this.messages = ['mock', 'messages']))
|
||||
await this.ChatController.getMessages(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should ask the chat handler about the request', function () {
|
||||
this.ChatApiHandler.promises.getGlobalMessages
|
||||
.calledWith(this.project_id, this.limit, this.before)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return the messages', function () {
|
||||
this.res.json.calledWith(this.messages).should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -102,7 +102,7 @@ describe('CollaboratorsController', function () {
|
||||
)
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Collaborators/OwnershipTransferHandler.js',
|
||||
'../../../../app/src/Features/Collaborators/OwnershipTransferHandler.mjs',
|
||||
() => ({
|
||||
default: ctx.OwnershipTransferHandler,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
import { vi, expect } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import PrivilegeLevels from '../../../../app/src/Features/Authorization/PrivilegeLevels.js'
|
||||
import Errors from '../../../../app/src/Features/Errors/Errors.js'
|
||||
import mongodb from 'mongodb-legacy'
|
||||
|
||||
const { ObjectId } = mongodb
|
||||
|
||||
const MODULE_PATH =
|
||||
'../../../../app/src/Features/Collaborators/OwnershipTransferHandler.mjs'
|
||||
|
||||
describe('OwnershipTransferHandler', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.user = { _id: new ObjectId(), email: 'owner@example.com' }
|
||||
ctx.collaborator = {
|
||||
_id: new ObjectId(),
|
||||
email: 'collaborator@example.com',
|
||||
}
|
||||
ctx.readOnlyCollaborator = {
|
||||
_id: new ObjectId(),
|
||||
email: 'readonly@example.com',
|
||||
}
|
||||
ctx.reviewer = {
|
||||
_id: new ObjectId(),
|
||||
email: 'reviewer@example.com',
|
||||
}
|
||||
ctx.project = {
|
||||
_id: new ObjectId(),
|
||||
name: 'project',
|
||||
owner_ref: ctx.user._id,
|
||||
collaberator_refs: [ctx.collaborator._id],
|
||||
readOnly_refs: [ctx.readOnlyCollaborator._id],
|
||||
reviewer_refs: [ctx.reviewer._id],
|
||||
}
|
||||
ctx.ProjectGetter = {
|
||||
promises: {
|
||||
getProject: sinon.stub().resolves(ctx.project),
|
||||
},
|
||||
}
|
||||
ctx.ProjectModel = {
|
||||
find: sinon.stub().resolves([]),
|
||||
updateOne: sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(),
|
||||
}),
|
||||
}
|
||||
ctx.UserGetter = {
|
||||
promises: {
|
||||
getUser: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
ctx.TpdsUpdateSender = {
|
||||
promises: {
|
||||
moveEntity: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
ctx.TpdsProjectFlusher = {
|
||||
promises: {
|
||||
flushProjectToTpds: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
ctx.CollaboratorsHandler = {
|
||||
promises: {
|
||||
removeUserFromProject: sinon.stub().resolves(),
|
||||
addUserIdToProject: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
ctx.EmailHandler = {
|
||||
promises: {
|
||||
sendEmail: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
ctx.ProjectAuditLogHandler = {
|
||||
promises: {
|
||||
addEntry: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
ctx.TagsHandler = {
|
||||
promises: {
|
||||
createTag: sinon.stub().resolves(),
|
||||
addProjectsToTag: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
|
||||
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
|
||||
)
|
||||
|
||||
vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({
|
||||
default: ctx.ProjectGetter,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../../app/src/models/Project.js', () => ({
|
||||
Project: ctx.ProjectModel,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../../app/src/Features/Tags/TagsHandler.js', () => ({
|
||||
default: ctx.TagsHandler,
|
||||
}))
|
||||
|
||||
vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({
|
||||
default: ctx.UserGetter,
|
||||
}))
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher.js',
|
||||
() => ({
|
||||
default: ctx.TpdsProjectFlusher,
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Project/ProjectAuditLogHandler.js',
|
||||
() => ({
|
||||
default: ctx.ProjectAuditLogHandler,
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock('../../../../app/src/Features/Email/EmailHandler.js', () => ({
|
||||
default: ctx.EmailHandler,
|
||||
}))
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Collaborators/CollaboratorsHandler.js',
|
||||
() => ({
|
||||
default: ctx.CollaboratorsHandler,
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Analytics/AnalyticsManager',
|
||||
() => ({
|
||||
default: {
|
||||
recordEventForUserInBackground: (ctx.recordEventForUserInBackground =
|
||||
sinon.stub()),
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
ctx.handler = (await import(MODULE_PATH)).default
|
||||
})
|
||||
|
||||
describe('transferOwnership', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.UserGetter.promises.getUser.withArgs(ctx.user._id).resolves(ctx.user)
|
||||
ctx.UserGetter.promises.getUser
|
||||
.withArgs(ctx.collaborator._id)
|
||||
.resolves(ctx.collaborator)
|
||||
ctx.UserGetter.promises.getUser
|
||||
.withArgs(ctx.readOnlyCollaborator._id)
|
||||
.resolves(ctx.readOnlyCollaborator)
|
||||
ctx.UserGetter.promises.getUser
|
||||
.withArgs(ctx.reviewer._id)
|
||||
.resolves(ctx.reviewer)
|
||||
})
|
||||
|
||||
it("should return a not found error if the project can't be found", async function (ctx) {
|
||||
ctx.ProjectGetter.promises.getProject.resolves(null)
|
||||
await expect(
|
||||
ctx.handler.promises.transferOwnership('abc', ctx.collaborator._id)
|
||||
).to.be.rejectedWith(Errors.ProjectNotFoundError)
|
||||
})
|
||||
|
||||
it("should return a not found error if the user can't be found", async function (ctx) {
|
||||
ctx.UserGetter.promises.getUser
|
||||
.withArgs(ctx.collaborator._id)
|
||||
.resolves(null)
|
||||
await expect(
|
||||
ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id
|
||||
)
|
||||
).to.be.rejectedWith(Errors.UserNotFoundError)
|
||||
})
|
||||
|
||||
it('should return an error if user cannot be removed as collaborator ', async function (ctx) {
|
||||
ctx.CollaboratorsHandler.promises.removeUserFromProject.rejects(
|
||||
new Error('user-cannot-be-removed')
|
||||
)
|
||||
await expect(
|
||||
ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
|
||||
it('should transfer ownership of the project', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id
|
||||
)
|
||||
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: ctx.project._id },
|
||||
sinon.match({ $set: { owner_ref: ctx.collaborator._id } })
|
||||
)
|
||||
})
|
||||
|
||||
it('should transfer ownership of the project to a read-only collaborator', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.readOnlyCollaborator._id
|
||||
)
|
||||
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: ctx.project._id },
|
||||
sinon.match({ $set: { owner_ref: ctx.readOnlyCollaborator._id } })
|
||||
)
|
||||
})
|
||||
|
||||
it('gives old owner read-only permissions if new owner was previously a viewer', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.readOnlyCollaborator._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.readOnlyCollaborator._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_ONLY
|
||||
)
|
||||
})
|
||||
|
||||
it('should do nothing if transferring back to the owner', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.user._id
|
||||
)
|
||||
expect(ctx.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
|
||||
it("should remove the user from the project's collaborators", async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.removeUserFromProject
|
||||
).to.have.been.calledWith(ctx.project._id, ctx.collaborator._id)
|
||||
})
|
||||
|
||||
it('should add the former project owner as a read/write collaborator', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_AND_WRITE
|
||||
)
|
||||
})
|
||||
|
||||
it('should transfer ownership of the project to a reviewer', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.reviewer._id
|
||||
)
|
||||
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: ctx.project._id },
|
||||
sinon.match({ $set: { owner_ref: ctx.reviewer._id } })
|
||||
)
|
||||
})
|
||||
|
||||
it('gives old owner reviewer permissions if new owner was previously a reviewer', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.reviewer._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.reviewer._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.REVIEW
|
||||
)
|
||||
})
|
||||
|
||||
it('should flush the project to tpds', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id
|
||||
)
|
||||
expect(
|
||||
ctx.TpdsProjectFlusher.promises.flushProjectToTpds
|
||||
).to.have.been.calledWith(ctx.project._id)
|
||||
})
|
||||
|
||||
it('should send an email notification', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id
|
||||
)
|
||||
expect(ctx.EmailHandler.promises.sendEmail).to.have.been.calledWith(
|
||||
'ownershipTransferConfirmationPreviousOwner',
|
||||
{
|
||||
to: ctx.user.email,
|
||||
project: ctx.project,
|
||||
newOwner: ctx.collaborator,
|
||||
}
|
||||
)
|
||||
expect(ctx.EmailHandler.promises.sendEmail).to.have.been.calledWith(
|
||||
'ownershipTransferConfirmationNewOwner',
|
||||
{
|
||||
to: ctx.collaborator.email,
|
||||
project: ctx.project,
|
||||
previousOwner: ctx.user,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should not send an email notification with the skipEmails option', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id,
|
||||
{ skipEmails: true }
|
||||
)
|
||||
expect(ctx.EmailHandler.promises.sendEmail).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('should track the change in BigQuery', async function (ctx) {
|
||||
const sessionUserId = new ObjectId()
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id,
|
||||
{ sessionUserId }
|
||||
)
|
||||
expect(ctx.recordEventForUserInBackground).to.have.been.calledWith(
|
||||
ctx.user._id,
|
||||
'project-ownership-transfer',
|
||||
{
|
||||
projectId: ctx.project._id,
|
||||
newOwnerId: ctx.collaborator._id,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should write an entry in the audit log', async function (ctx) {
|
||||
const sessionUserId = new ObjectId()
|
||||
const ipAddress = '1.2.3.4'
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id,
|
||||
{ sessionUserId, ipAddress }
|
||||
)
|
||||
expect(
|
||||
ctx.ProjectAuditLogHandler.promises.addEntry
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
'transfer-ownership',
|
||||
sessionUserId,
|
||||
ipAddress,
|
||||
{
|
||||
previousOwnerId: ctx.user._id,
|
||||
newOwnerId: ctx.collaborator._id,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should decline to transfer ownership to a non-collaborator', async function (ctx) {
|
||||
ctx.project.collaberator_refs = []
|
||||
ctx.project.readOnly_refs = []
|
||||
await expect(
|
||||
ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id
|
||||
)
|
||||
).to.be.rejectedWith(Errors.UserNotCollaboratorError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('transferAllProjectsToUser', function () {
|
||||
const fromUserEmail = 'user.one@example.com'
|
||||
const ipAddress = '1.2.3.4'
|
||||
let fromUserId, toUserId
|
||||
beforeEach(function () {
|
||||
fromUserId = new ObjectId().toString()
|
||||
toUserId = new ObjectId().toString()
|
||||
})
|
||||
|
||||
describe('with missing user', function () {
|
||||
it('should throw an error', async function (ctx) {
|
||||
ctx.UserGetter.promises.getUser.withArgs(fromUserId).resolves(null)
|
||||
ctx.UserGetter.promises.getUser
|
||||
.withArgs(toUserId)
|
||||
.resolves({ _id: new ObjectId(toUserId) })
|
||||
await expect(
|
||||
ctx.handler.promises.transferAllProjectsToUser({
|
||||
toUserId,
|
||||
fromUserId,
|
||||
ipAddress,
|
||||
})
|
||||
).to.be.rejectedWith(/missing source user/)
|
||||
|
||||
ctx.UserGetter.promises.getUser
|
||||
.withArgs(fromUserId)
|
||||
.resolves({ _id: new ObjectId(fromUserId), email: fromUserEmail })
|
||||
ctx.UserGetter.promises.getUser.withArgs(toUserId).resolves(null)
|
||||
await expect(
|
||||
ctx.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
).to.be.rejectedWith(/missing destination user/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the same id', function () {
|
||||
it('should throw an error', async function (ctx) {
|
||||
ctx.UserGetter.promises.getUser
|
||||
.withArgs(fromUserId)
|
||||
.resolves({ _id: new ObjectId(fromUserId), email: fromUserEmail })
|
||||
await expect(
|
||||
ctx.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId: fromUserId,
|
||||
ipAddress,
|
||||
})
|
||||
).to.be.rejectedWith(/rejecting transfer between identical users/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('happy path', function () {
|
||||
let tag, fromUserEmail, projects
|
||||
|
||||
beforeEach(function (ctx) {
|
||||
tag = {
|
||||
_id: new ObjectId(),
|
||||
name: 'some-tag-name',
|
||||
}
|
||||
projects = [
|
||||
{ _id: 'project-1' },
|
||||
{ _id: 'project-2' },
|
||||
{ _id: 'project-3' },
|
||||
]
|
||||
|
||||
ctx.UserGetter.promises.getUser.withArgs(fromUserId).resolves({
|
||||
_id: new ObjectId(fromUserId),
|
||||
email: fromUserEmail,
|
||||
})
|
||||
ctx.UserGetter.promises.getUser.withArgs(toUserId).resolves({
|
||||
_id: new ObjectId(toUserId),
|
||||
})
|
||||
ctx.ProjectModel.find.resolves(projects)
|
||||
ctx.TagsHandler.promises.createTag.resolves({
|
||||
_id: tag._id,
|
||||
name: 'some-tag-name',
|
||||
})
|
||||
ctx.TagsHandler.promises.addProjectsToTag.resolves()
|
||||
})
|
||||
|
||||
it('creates a tag', async function (ctx) {
|
||||
await ctx.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(ctx.TagsHandler.promises.createTag).to.have.been.calledWith(
|
||||
toUserId,
|
||||
`transferred-from-${fromUserEmail}`,
|
||||
'#434AF0',
|
||||
{ truncate: true }
|
||||
)
|
||||
})
|
||||
|
||||
it('returns a projectCount, and tag name', async function (ctx) {
|
||||
const result = await ctx.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(result.projectCount).to.equal(projects.length)
|
||||
expect(result.newTagName).to.equal('some-tag-name')
|
||||
})
|
||||
|
||||
it('gets the user records', async function (ctx) {
|
||||
await ctx.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(ctx.UserGetter.promises.getUser).to.have.been.calledWith(
|
||||
fromUserId
|
||||
)
|
||||
expect(ctx.UserGetter.promises.getUser).to.have.been.calledWith(
|
||||
toUserId
|
||||
)
|
||||
})
|
||||
|
||||
it('gets the list of affected projects', async function (ctx) {
|
||||
await ctx.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(ctx.ProjectModel.find).to.have.been.calledWith({
|
||||
owner_ref: fromUserId,
|
||||
})
|
||||
})
|
||||
|
||||
it('transfers all of the projects', async function (ctx) {
|
||||
await ctx.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
|
||||
expect(ctx.ProjectModel.updateOne.callCount).to.equal(3)
|
||||
expect(ctx.TagsHandler.promises.addProjectsToTag.callCount).to.equal(1)
|
||||
|
||||
for (const project of projects) {
|
||||
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: project._id },
|
||||
sinon.match({ $set: { owner_ref: toUserId } })
|
||||
)
|
||||
}
|
||||
expect(
|
||||
ctx.TagsHandler.promises.addProjectsToTag
|
||||
).to.have.been.calledWith(
|
||||
toUserId,
|
||||
tag._id,
|
||||
projects.map(p => p._id)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,494 +0,0 @@
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const PrivilegeLevels = require('../../../../app/src/Features/Authorization/PrivilegeLevels')
|
||||
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
||||
const { ObjectId } = require('mongodb-legacy')
|
||||
|
||||
const MODULE_PATH =
|
||||
'../../../../app/src/Features/Collaborators/OwnershipTransferHandler'
|
||||
|
||||
describe('OwnershipTransferHandler', function () {
|
||||
beforeEach(function () {
|
||||
this.user = { _id: new ObjectId(), email: 'owner@example.com' }
|
||||
this.collaborator = {
|
||||
_id: new ObjectId(),
|
||||
email: 'collaborator@example.com',
|
||||
}
|
||||
this.readOnlyCollaborator = {
|
||||
_id: new ObjectId(),
|
||||
email: 'readonly@example.com',
|
||||
}
|
||||
this.reviewer = {
|
||||
_id: new ObjectId(),
|
||||
email: 'reviewer@example.com',
|
||||
}
|
||||
this.project = {
|
||||
_id: new ObjectId(),
|
||||
name: 'project',
|
||||
owner_ref: this.user._id,
|
||||
collaberator_refs: [this.collaborator._id],
|
||||
readOnly_refs: [this.readOnlyCollaborator._id],
|
||||
reviewer_refs: [this.reviewer._id],
|
||||
}
|
||||
this.ProjectGetter = {
|
||||
promises: {
|
||||
getProject: sinon.stub().resolves(this.project),
|
||||
},
|
||||
}
|
||||
this.ProjectModel = {
|
||||
find: sinon.stub().resolves([]),
|
||||
updateOne: sinon.stub().returns({
|
||||
exec: sinon.stub().resolves(),
|
||||
}),
|
||||
}
|
||||
this.UserGetter = {
|
||||
promises: {
|
||||
getUser: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.TpdsUpdateSender = {
|
||||
promises: {
|
||||
moveEntity: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.TpdsProjectFlusher = {
|
||||
promises: {
|
||||
flushProjectToTpds: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.CollaboratorsHandler = {
|
||||
promises: {
|
||||
removeUserFromProject: sinon.stub().resolves(),
|
||||
addUserIdToProject: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.EmailHandler = {
|
||||
promises: {
|
||||
sendEmail: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.ProjectAuditLogHandler = {
|
||||
promises: {
|
||||
addEntry: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.TagsHandler = {
|
||||
promises: {
|
||||
createTag: sinon.stub().resolves(),
|
||||
addProjectsToTag: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.handler = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'../Project/ProjectGetter': this.ProjectGetter,
|
||||
'../../models/Project': {
|
||||
Project: this.ProjectModel,
|
||||
},
|
||||
'../Tags/TagsHandler': this.TagsHandler,
|
||||
'../User/UserGetter': this.UserGetter,
|
||||
'../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
|
||||
'../Project/ProjectAuditLogHandler': this.ProjectAuditLogHandler,
|
||||
'../Email/EmailHandler': this.EmailHandler,
|
||||
'./CollaboratorsHandler': this.CollaboratorsHandler,
|
||||
'../Analytics/AnalyticsManager': {
|
||||
recordEventForUserInBackground: (this.recordEventForUserInBackground =
|
||||
sinon.stub()),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('transferOwnership', function () {
|
||||
beforeEach(function () {
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(this.user._id)
|
||||
.resolves(this.user)
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(this.collaborator._id)
|
||||
.resolves(this.collaborator)
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(this.readOnlyCollaborator._id)
|
||||
.resolves(this.readOnlyCollaborator)
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(this.reviewer._id)
|
||||
.resolves(this.reviewer)
|
||||
})
|
||||
|
||||
it("should return a not found error if the project can't be found", async function () {
|
||||
this.ProjectGetter.promises.getProject.resolves(null)
|
||||
await expect(
|
||||
this.handler.promises.transferOwnership('abc', this.collaborator._id)
|
||||
).to.be.rejectedWith(Errors.ProjectNotFoundError)
|
||||
})
|
||||
|
||||
it("should return a not found error if the user can't be found", async function () {
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(this.collaborator._id)
|
||||
.resolves(null)
|
||||
await expect(
|
||||
this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
).to.be.rejectedWith(Errors.UserNotFoundError)
|
||||
})
|
||||
|
||||
it('should return an error if user cannot be removed as collaborator ', async function () {
|
||||
this.CollaboratorsHandler.promises.removeUserFromProject.rejects(
|
||||
new Error('user-cannot-be-removed')
|
||||
)
|
||||
await expect(
|
||||
this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
|
||||
it('should transfer ownership of the project', async function () {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: this.project._id },
|
||||
sinon.match({ $set: { owner_ref: this.collaborator._id } })
|
||||
)
|
||||
})
|
||||
|
||||
it('should transfer ownership of the project to a read-only collaborator', async function () {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.readOnlyCollaborator._id
|
||||
)
|
||||
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: this.project._id },
|
||||
sinon.match({ $set: { owner_ref: this.readOnlyCollaborator._id } })
|
||||
)
|
||||
})
|
||||
|
||||
it('gives old owner read-only permissions if new owner was previously a viewer', async function () {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.readOnlyCollaborator._id
|
||||
)
|
||||
expect(
|
||||
this.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
this.project._id,
|
||||
this.readOnlyCollaborator._id,
|
||||
this.user._id,
|
||||
PrivilegeLevels.READ_ONLY
|
||||
)
|
||||
})
|
||||
|
||||
it('should do nothing if transferring back to the owner', async function () {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.user._id
|
||||
)
|
||||
expect(this.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
|
||||
it("should remove the user from the project's collaborators", async function () {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
expect(
|
||||
this.CollaboratorsHandler.promises.removeUserFromProject
|
||||
).to.have.been.calledWith(this.project._id, this.collaborator._id)
|
||||
})
|
||||
|
||||
it('should add the former project owner as a read/write collaborator', async function () {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
expect(
|
||||
this.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
this.project._id,
|
||||
this.collaborator._id,
|
||||
this.user._id,
|
||||
PrivilegeLevels.READ_AND_WRITE
|
||||
)
|
||||
})
|
||||
|
||||
it('should transfer ownership of the project to a reviewer', async function () {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.reviewer._id
|
||||
)
|
||||
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: this.project._id },
|
||||
sinon.match({ $set: { owner_ref: this.reviewer._id } })
|
||||
)
|
||||
})
|
||||
|
||||
it('gives old owner reviewer permissions if new owner was previously a reviewer', async function () {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.reviewer._id
|
||||
)
|
||||
expect(
|
||||
this.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
this.project._id,
|
||||
this.reviewer._id,
|
||||
this.user._id,
|
||||
PrivilegeLevels.REVIEW
|
||||
)
|
||||
})
|
||||
|
||||
it('should flush the project to tpds', async function () {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
expect(
|
||||
this.TpdsProjectFlusher.promises.flushProjectToTpds
|
||||
).to.have.been.calledWith(this.project._id)
|
||||
})
|
||||
|
||||
it('should send an email notification', async function () {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
expect(this.EmailHandler.promises.sendEmail).to.have.been.calledWith(
|
||||
'ownershipTransferConfirmationPreviousOwner',
|
||||
{
|
||||
to: this.user.email,
|
||||
project: this.project,
|
||||
newOwner: this.collaborator,
|
||||
}
|
||||
)
|
||||
expect(this.EmailHandler.promises.sendEmail).to.have.been.calledWith(
|
||||
'ownershipTransferConfirmationNewOwner',
|
||||
{
|
||||
to: this.collaborator.email,
|
||||
project: this.project,
|
||||
previousOwner: this.user,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should not send an email notification with the skipEmails option', async function () {
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id,
|
||||
{ skipEmails: true }
|
||||
)
|
||||
expect(this.EmailHandler.promises.sendEmail).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('should track the change in BigQuery', async function () {
|
||||
const sessionUserId = new ObjectId()
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id,
|
||||
{ sessionUserId }
|
||||
)
|
||||
expect(this.recordEventForUserInBackground).to.have.been.calledWith(
|
||||
this.user._id,
|
||||
'project-ownership-transfer',
|
||||
{
|
||||
projectId: this.project._id,
|
||||
newOwnerId: this.collaborator._id,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should write an entry in the audit log', async function () {
|
||||
const sessionUserId = new ObjectId()
|
||||
const ipAddress = '1.2.3.4'
|
||||
await this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id,
|
||||
{ sessionUserId, ipAddress }
|
||||
)
|
||||
expect(
|
||||
this.ProjectAuditLogHandler.promises.addEntry
|
||||
).to.have.been.calledWith(
|
||||
this.project._id,
|
||||
'transfer-ownership',
|
||||
sessionUserId,
|
||||
ipAddress,
|
||||
{
|
||||
previousOwnerId: this.user._id,
|
||||
newOwnerId: this.collaborator._id,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should decline to transfer ownership to a non-collaborator', async function () {
|
||||
this.project.collaberator_refs = []
|
||||
this.project.readOnly_refs = []
|
||||
await expect(
|
||||
this.handler.promises.transferOwnership(
|
||||
this.project._id,
|
||||
this.collaborator._id
|
||||
)
|
||||
).to.be.rejectedWith(Errors.UserNotCollaboratorError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('transferAllProjectsToUser', function () {
|
||||
const fromUserEmail = 'user.one@example.com'
|
||||
const ipAddress = '1.2.3.4'
|
||||
let fromUserId, toUserId
|
||||
beforeEach(function () {
|
||||
fromUserId = new ObjectId().toString()
|
||||
toUserId = new ObjectId().toString()
|
||||
})
|
||||
|
||||
describe('with missing user', function () {
|
||||
it('should throw an error', async function () {
|
||||
this.UserGetter.promises.getUser.withArgs(fromUserId).resolves(null)
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(toUserId)
|
||||
.resolves({ _id: new ObjectId(toUserId) })
|
||||
await expect(
|
||||
this.handler.promises.transferAllProjectsToUser({
|
||||
toUserId,
|
||||
fromUserId,
|
||||
ipAddress,
|
||||
})
|
||||
).to.be.rejectedWith(/missing source user/)
|
||||
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(fromUserId)
|
||||
.resolves({ _id: new ObjectId(fromUserId), email: fromUserEmail })
|
||||
this.UserGetter.promises.getUser.withArgs(toUserId).resolves(null)
|
||||
await expect(
|
||||
this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
).to.be.rejectedWith(/missing destination user/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the same id', function () {
|
||||
it('should throw an error', async function () {
|
||||
this.UserGetter.promises.getUser
|
||||
.withArgs(fromUserId)
|
||||
.resolves({ _id: new ObjectId(fromUserId), email: fromUserEmail })
|
||||
await expect(
|
||||
this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId: fromUserId,
|
||||
ipAddress,
|
||||
})
|
||||
).to.be.rejectedWith(/rejecting transfer between identical users/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('happy path', function () {
|
||||
let tag, fromUserEmail, projects
|
||||
|
||||
beforeEach(function () {
|
||||
tag = {
|
||||
_id: new ObjectId(),
|
||||
name: 'some-tag-name',
|
||||
}
|
||||
projects = [
|
||||
{ _id: 'project-1' },
|
||||
{ _id: 'project-2' },
|
||||
{ _id: 'project-3' },
|
||||
]
|
||||
|
||||
this.UserGetter.promises.getUser.withArgs(fromUserId).resolves({
|
||||
_id: new ObjectId(fromUserId),
|
||||
email: fromUserEmail,
|
||||
})
|
||||
this.UserGetter.promises.getUser.withArgs(toUserId).resolves({
|
||||
_id: new ObjectId(toUserId),
|
||||
})
|
||||
this.ProjectModel.find.resolves(projects)
|
||||
this.TagsHandler.promises.createTag.resolves({
|
||||
_id: tag._id,
|
||||
name: 'some-tag-name',
|
||||
})
|
||||
this.TagsHandler.promises.addProjectsToTag.resolves()
|
||||
})
|
||||
|
||||
it('creates a tag', async function () {
|
||||
await this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(this.TagsHandler.promises.createTag).to.have.been.calledWith(
|
||||
toUserId,
|
||||
`transferred-from-${fromUserEmail}`,
|
||||
'#434AF0',
|
||||
{ truncate: true }
|
||||
)
|
||||
})
|
||||
|
||||
it('returns a projectCount, and tag name', async function () {
|
||||
const result = await this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(result.projectCount).to.equal(projects.length)
|
||||
expect(result.newTagName).to.equal('some-tag-name')
|
||||
})
|
||||
|
||||
it('gets the user records', async function () {
|
||||
await this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(this.UserGetter.promises.getUser).to.have.been.calledWith(
|
||||
fromUserId
|
||||
)
|
||||
expect(this.UserGetter.promises.getUser).to.have.been.calledWith(
|
||||
toUserId
|
||||
)
|
||||
})
|
||||
|
||||
it('gets the list of affected projects', async function () {
|
||||
await this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
expect(this.ProjectModel.find).to.have.been.calledWith({
|
||||
owner_ref: fromUserId,
|
||||
})
|
||||
})
|
||||
|
||||
it('transfers all of the projects', async function () {
|
||||
await this.handler.promises.transferAllProjectsToUser({
|
||||
fromUserId,
|
||||
toUserId,
|
||||
ipAddress,
|
||||
})
|
||||
|
||||
expect(this.ProjectModel.updateOne.callCount).to.equal(3)
|
||||
expect(this.TagsHandler.promises.addProjectsToTag.callCount).to.equal(1)
|
||||
|
||||
for (const project of projects) {
|
||||
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: project._id },
|
||||
sinon.match({ $set: { owner_ref: toUserId } })
|
||||
)
|
||||
}
|
||||
expect(
|
||||
this.TagsHandler.promises.addProjectsToTag
|
||||
).to.have.been.calledWith(
|
||||
toUserId,
|
||||
tag._id,
|
||||
projects.map(p => p._id)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
735
services/web/test/unit/src/Editor/EditorHttpController.test.mjs
Normal file
735
services/web/test/unit/src/Editor/EditorHttpController.test.mjs
Normal file
@@ -0,0 +1,735 @@
|
||||
import { vi, expect } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import mongodb from 'mongodb-legacy'
|
||||
import Errors from '../../../../app/src/Features/Errors/Errors.js'
|
||||
import MockRequest from '../helpers/MockRequest.js'
|
||||
import MockResponse from '../helpers/MockResponse.js'
|
||||
|
||||
const { ObjectId } = mongodb
|
||||
|
||||
const MODULE_PATH =
|
||||
'../../../../app/src/Features/Editor/EditorHttpController.mjs'
|
||||
|
||||
describe('EditorHttpController', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.ownerId = new ObjectId()
|
||||
ctx.project = {
|
||||
_id: new ObjectId(),
|
||||
owner_ref: ctx.ownerId,
|
||||
}
|
||||
ctx.user = {
|
||||
_id: new ObjectId(),
|
||||
projects: {},
|
||||
}
|
||||
ctx.members = [
|
||||
{ user: { _id: 'owner', features: {} }, privilegeLevel: 'owner' },
|
||||
{ user: { _id: 'one' }, privilegeLevel: 'readOnly' },
|
||||
]
|
||||
ctx.ownerMember = ctx.members[0]
|
||||
ctx.invites = [{ _id: 'three' }, { _id: 'four' }]
|
||||
ctx.projectView = {
|
||||
_id: ctx.project._id,
|
||||
owner: {
|
||||
_id: 'owner',
|
||||
email: 'owner@example.com',
|
||||
other_property: true,
|
||||
},
|
||||
members: [
|
||||
{ _id: 'owner', privileges: 'owner' },
|
||||
{ _id: 'one', privileges: 'readOnly' },
|
||||
],
|
||||
invites: [{ three: 3 }, { four: 4 }],
|
||||
}
|
||||
ctx.reducedProjectView = {
|
||||
_id: ctx.projectView._id,
|
||||
owner: { _id: ctx.projectView.owner._id },
|
||||
members: [],
|
||||
invites: [],
|
||||
}
|
||||
ctx.doc = { _id: new ObjectId(), name: 'excellent-original-idea.tex' }
|
||||
ctx.file = { _id: new ObjectId() }
|
||||
ctx.folder = { _id: new ObjectId() }
|
||||
ctx.source = 'editor'
|
||||
|
||||
ctx.parentFolderId = 'mock-folder-id'
|
||||
ctx.req = new MockRequest()
|
||||
ctx.res = new MockResponse()
|
||||
ctx.next = sinon.stub()
|
||||
ctx.token = null
|
||||
ctx.docLines = ['hello', 'overleaf']
|
||||
|
||||
ctx.AuthorizationManager = {
|
||||
isRestrictedUser: sinon.stub().returns(false),
|
||||
promises: {
|
||||
getPrivilegeLevelForProjectWithProjectAccess: sinon
|
||||
.stub()
|
||||
.resolves('owner'),
|
||||
},
|
||||
}
|
||||
const members = ctx.members
|
||||
const ownerMember = ctx.ownerMember
|
||||
ctx.CollaboratorsGetter = {
|
||||
ProjectAccess: class {
|
||||
loadOwnerAndInvitedMembers() {
|
||||
return { members, ownerMember }
|
||||
}
|
||||
|
||||
loadOwner() {
|
||||
return ownerMember
|
||||
}
|
||||
|
||||
isUserTokenMember() {
|
||||
return false
|
||||
}
|
||||
|
||||
isUserInvitedMember() {
|
||||
return false
|
||||
}
|
||||
},
|
||||
promises: {
|
||||
isUserInvitedMemberOfProject: sinon.stub().resolves(false),
|
||||
},
|
||||
}
|
||||
ctx.CollaboratorsHandler = {
|
||||
promises: {
|
||||
userIsTokenMember: sinon.stub().resolves(false),
|
||||
},
|
||||
}
|
||||
ctx.invites = [
|
||||
{
|
||||
_id: 'invite_one',
|
||||
email: 'user-one@example.com',
|
||||
privileges: 'readOnly',
|
||||
projectId: ctx.project._id,
|
||||
},
|
||||
{
|
||||
_id: 'invite_two',
|
||||
email: 'user-two@example.com',
|
||||
privileges: 'readOnly',
|
||||
projectId: ctx.project._id,
|
||||
},
|
||||
]
|
||||
ctx.CollaboratorsInviteGetter = {
|
||||
promises: {
|
||||
getAllInvites: sinon.stub().resolves(ctx.invites),
|
||||
},
|
||||
}
|
||||
ctx.EditorController = {
|
||||
promises: {
|
||||
addDoc: sinon.stub().resolves(ctx.doc),
|
||||
addFile: sinon.stub().resolves(ctx.file),
|
||||
addFolder: sinon.stub().resolves(ctx.folder),
|
||||
renameEntity: sinon.stub().resolves(),
|
||||
moveEntity: sinon.stub().resolves(),
|
||||
deleteEntity: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
ctx.ProjectDeleter = {
|
||||
promises: {
|
||||
unmarkAsDeletedByExternalSource: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
ctx.ProjectGetter = {
|
||||
promises: {
|
||||
getProjectWithoutDocLines: sinon.stub().resolves(ctx.project),
|
||||
},
|
||||
}
|
||||
ctx.ProjectEditorHandler = {
|
||||
buildProjectModelView: sinon.stub().returns(ctx.projectView),
|
||||
}
|
||||
ctx.Metrics = { inc: sinon.stub() }
|
||||
ctx.TokenAccessHandler = {
|
||||
getRequestToken: sinon.stub().returns(ctx.token),
|
||||
}
|
||||
ctx.SessionManager = {
|
||||
getLoggedInUserId: sinon.stub().returns(ctx.user._id),
|
||||
}
|
||||
ctx.ProjectEntityUpdateHandler = {
|
||||
promises: {
|
||||
convertDocToFile: sinon.stub().resolves(ctx.file),
|
||||
},
|
||||
}
|
||||
ctx.DocstoreManager = {
|
||||
promises: {
|
||||
getAllDeletedDocs: sinon.stub().resolves([]),
|
||||
},
|
||||
}
|
||||
ctx.HttpErrorHandler = {
|
||||
notFound: sinon.stub(),
|
||||
unprocessableEntity: sinon.stub(),
|
||||
}
|
||||
ctx.SplitTestHandler = {
|
||||
promises: {
|
||||
getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }),
|
||||
},
|
||||
}
|
||||
ctx.UserGetter = { promises: { getUser: sinon.stub().resolves(null, {}) } }
|
||||
|
||||
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
|
||||
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
|
||||
)
|
||||
vi.doMock('../../../../app/src/Features/Project/ProjectDeleter.js', () => ({
|
||||
default: ctx.ProjectDeleter,
|
||||
}))
|
||||
vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({
|
||||
default: ctx.ProjectGetter,
|
||||
}))
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Authorization/AuthorizationManager.js',
|
||||
() => ({
|
||||
default: ctx.AuthorizationManager,
|
||||
})
|
||||
)
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Project/ProjectEditorHandler.js',
|
||||
() => ({
|
||||
default: ctx.ProjectEditorHandler,
|
||||
})
|
||||
)
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Editor/EditorController.js',
|
||||
() => ({
|
||||
default: ctx.EditorController,
|
||||
})
|
||||
)
|
||||
vi.doMock('@overleaf/metrics', () => ({
|
||||
default: ctx.Metrics,
|
||||
}))
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Collaborators/CollaboratorsGetter.js',
|
||||
() => ({
|
||||
default: ctx.CollaboratorsGetter,
|
||||
})
|
||||
)
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Collaborators/CollaboratorsHandler.js',
|
||||
() => ({
|
||||
default: ctx.CollaboratorsHandler,
|
||||
})
|
||||
)
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter.js',
|
||||
() => ({
|
||||
default: ctx.CollaboratorsInviteGetter,
|
||||
})
|
||||
)
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/TokenAccess/TokenAccessHandler.js',
|
||||
() => ({
|
||||
default: ctx.TokenAccessHandler,
|
||||
})
|
||||
)
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Authentication/SessionManager.js',
|
||||
() => ({
|
||||
default: ctx.SessionManager,
|
||||
})
|
||||
)
|
||||
vi.doMock('../../../../app/src/infrastructure/FileWriter.js', () => ({
|
||||
default: ctx.FileWriter,
|
||||
}))
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Project/ProjectEntityUpdateHandler.js',
|
||||
() => ({
|
||||
default: ctx.ProjectEntityUpdateHandler,
|
||||
})
|
||||
)
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Docstore/DocstoreManager.js',
|
||||
() => ({
|
||||
default: ctx.DocstoreManager,
|
||||
})
|
||||
)
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Errors/HttpErrorHandler.js',
|
||||
() => ({
|
||||
default: ctx.HttpErrorHandler,
|
||||
})
|
||||
)
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/SplitTests/SplitTestHandler.js',
|
||||
() => ({
|
||||
default: ctx.SplitTestHandler,
|
||||
})
|
||||
)
|
||||
vi.doMock('../../../../app/src/Features/Compile/CompileManager.js', () => ({
|
||||
default: {},
|
||||
}))
|
||||
vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({
|
||||
default: ctx.UserGetter,
|
||||
}))
|
||||
|
||||
ctx.EditorHttpController = (await import(MODULE_PATH)).default
|
||||
})
|
||||
|
||||
describe('joinProject', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req.params = { Project_id: ctx.project._id }
|
||||
ctx.req.query = { user_id: ctx.user._id }
|
||||
ctx.req.body = { userId: ctx.user._id }
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
sinon
|
||||
.stub(
|
||||
ctx.CollaboratorsGetter.ProjectAccess.prototype,
|
||||
'isUserInvitedMember'
|
||||
)
|
||||
.returns(true)
|
||||
ctx.res.callback = resolve
|
||||
ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should request a full view', function (ctx) {
|
||||
expect(
|
||||
ctx.ProjectEditorHandler.buildProjectModelView
|
||||
).to.have.been.calledWith(
|
||||
ctx.project,
|
||||
ctx.ownerMember,
|
||||
ctx.members,
|
||||
ctx.invites,
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the project and privilege level', function (ctx) {
|
||||
expect(ctx.res.json).to.have.been.calledWith({
|
||||
project: ctx.projectView,
|
||||
privilegeLevel: 'owner',
|
||||
isRestrictedUser: false,
|
||||
isTokenMember: false,
|
||||
isInvitedMember: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not try to unmark the project as deleted', function (ctx) {
|
||||
expect(ctx.ProjectDeleter.promises.unmarkAsDeletedByExternalSource).not
|
||||
.to.have.been.called
|
||||
})
|
||||
|
||||
it('should send an inc metric', function (ctx) {
|
||||
expect(ctx.Metrics.inc).to.have.been.calledWith('editor.join-project')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the project is marked as deleted', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.projectView.deletedByExternalDataSource = true
|
||||
ctx.res.callback = resolve
|
||||
ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should unmark the project as deleted', function (ctx) {
|
||||
expect(
|
||||
ctx.ProjectDeleter.promises.unmarkAsDeletedByExternalSource
|
||||
).to.have.been.calledWith(ctx.project._id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a restricted user', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.ProjectEditorHandler.buildProjectModelView.returns(
|
||||
ctx.reducedProjectView
|
||||
)
|
||||
ctx.AuthorizationManager.isRestrictedUser.returns(true)
|
||||
ctx.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess.resolves(
|
||||
'readOnly'
|
||||
)
|
||||
ctx.res.callback = resolve
|
||||
ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should request a restricted view', function (ctx) {
|
||||
expect(
|
||||
ctx.ProjectEditorHandler.buildProjectModelView
|
||||
).to.have.been.calledWith(ctx.project, ctx.ownerMember, [], [], true)
|
||||
})
|
||||
|
||||
it('should mark the user as restricted, and hide details of owner', function (ctx) {
|
||||
expect(ctx.res.json).to.have.been.calledWith({
|
||||
project: ctx.reducedProjectView,
|
||||
privilegeLevel: 'readOnly',
|
||||
isRestrictedUser: true,
|
||||
isTokenMember: false,
|
||||
isInvitedMember: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when not authorized', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess.resolves(
|
||||
null
|
||||
)
|
||||
ctx.res.callback = resolve
|
||||
ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should send a 403 response', function (ctx) {
|
||||
expect(ctx.res.statusCode).to.equal(403)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an anonymous user', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.token = 'token'
|
||||
ctx.TokenAccessHandler.getRequestToken.returns(ctx.token)
|
||||
ctx.ProjectEditorHandler.buildProjectModelView.returns(
|
||||
ctx.reducedProjectView
|
||||
)
|
||||
ctx.req.body = {
|
||||
userId: 'anonymous-user',
|
||||
anonymousAccessToken: ctx.token,
|
||||
}
|
||||
ctx.res.callback = resolve
|
||||
ctx.AuthorizationManager.isRestrictedUser
|
||||
.withArgs(null, 'readOnly', false, false)
|
||||
.returns(true)
|
||||
ctx.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess
|
||||
.withArgs(null, ctx.project._id, ctx.token)
|
||||
.resolves('readOnly')
|
||||
ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should request a restricted view', function (ctx) {
|
||||
expect(
|
||||
ctx.ProjectEditorHandler.buildProjectModelView
|
||||
).to.have.been.calledWith(ctx.project, ctx.ownerMember, [], [], true)
|
||||
})
|
||||
|
||||
it('should mark the user as restricted', function (ctx) {
|
||||
expect(ctx.res.json).to.have.been.calledWith({
|
||||
project: ctx.reducedProjectView,
|
||||
privilegeLevel: 'readOnly',
|
||||
isRestrictedUser: true,
|
||||
isTokenMember: false,
|
||||
isInvitedMember: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a token access user', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
sinon
|
||||
.stub(
|
||||
ctx.CollaboratorsGetter.ProjectAccess.prototype,
|
||||
'isUserInvitedMember'
|
||||
)
|
||||
.returns(false)
|
||||
sinon
|
||||
.stub(
|
||||
ctx.CollaboratorsGetter.ProjectAccess.prototype,
|
||||
'isUserTokenMember'
|
||||
)
|
||||
.returns(true)
|
||||
ctx.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess.resolves(
|
||||
'readAndWrite'
|
||||
)
|
||||
ctx.res.callback = resolve
|
||||
ctx.EditorHttpController.joinProject(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark the user as being a token-access member', function (ctx) {
|
||||
expect(ctx.res.json).to.have.been.calledWith({
|
||||
project: ctx.projectView,
|
||||
privilegeLevel: 'readAndWrite',
|
||||
isRestrictedUser: false,
|
||||
isTokenMember: true,
|
||||
isInvitedMember: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when project is not found', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.ProjectGetter.promises.getProjectWithoutDocLines.resolves(null)
|
||||
await new Promise(resolve => {
|
||||
ctx.next.callsFake(() => resolve())
|
||||
ctx.EditorHttpController.joinProject(ctx.req, ctx.res, ctx.next)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle return not found error', function (ctx) {
|
||||
expect(ctx.next).to.have.been.calledWith(
|
||||
sinon.match.instanceOf(Errors.NotFoundError)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addDoc', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req.params = { Project_id: ctx.project._id }
|
||||
ctx.req.body = {
|
||||
name: (ctx.docName = 'doc-name'),
|
||||
parent_folder_id: ctx.parentFolderId,
|
||||
}
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.res.callback = resolve
|
||||
ctx.EditorHttpController.addDoc(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call EditorController.addDoc', function (ctx) {
|
||||
expect(ctx.EditorController.promises.addDoc).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.parentFolderId,
|
||||
ctx.docName,
|
||||
[],
|
||||
'editor',
|
||||
ctx.user._id
|
||||
)
|
||||
})
|
||||
|
||||
it('should send the doc back as JSON', function (ctx) {
|
||||
expect(ctx.res.json).to.have.been.calledWith(ctx.doc)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unsuccesfully', function () {
|
||||
it('handle name too short', async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.req.body.name = ''
|
||||
ctx.res.callback = () => {
|
||||
expect(ctx.res.statusCode).to.equal(400)
|
||||
resolve()
|
||||
}
|
||||
ctx.EditorHttpController.addDoc(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('handle too many files', async function (ctx) {
|
||||
ctx.EditorController.promises.addDoc.rejects(
|
||||
new Error('project_has_too_many_files')
|
||||
)
|
||||
await new Promise(resolve => {
|
||||
ctx.res.callback = () => {
|
||||
expect(ctx.res.body).to.equal('"project_has_too_many_files"')
|
||||
expect(ctx.res.status).to.have.been.calledWith(400)
|
||||
resolve()
|
||||
}
|
||||
ctx.EditorHttpController.addDoc(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addFolder', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.folderName = 'folder-name'
|
||||
ctx.req.params = { Project_id: ctx.project._id }
|
||||
ctx.req.body = {
|
||||
name: ctx.folderName,
|
||||
parent_folder_id: ctx.parentFolderId,
|
||||
}
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.res.callback = resolve
|
||||
ctx.EditorHttpController.addFolder(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call EditorController.addFolder', function (ctx) {
|
||||
expect(ctx.EditorController.promises.addFolder).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.parentFolderId,
|
||||
ctx.folderName,
|
||||
'editor'
|
||||
)
|
||||
})
|
||||
|
||||
it('should send the folder back as JSON', function (ctx) {
|
||||
expect(ctx.res.json).to.have.been.calledWith(ctx.folder)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unsuccesfully', function () {
|
||||
it('handle name too short', async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.req.body.name = ''
|
||||
ctx.res.callback = () => {
|
||||
expect(ctx.res.statusCode).to.equal(400)
|
||||
resolve()
|
||||
}
|
||||
ctx.EditorHttpController.addFolder(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('handle too many files', async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.EditorController.promises.addFolder.rejects(
|
||||
new Error('project_has_too_many_files')
|
||||
)
|
||||
ctx.res.callback = () => {
|
||||
expect(ctx.res.body).to.equal('"project_has_too_many_files"')
|
||||
expect(ctx.res.statusCode).to.equal(400)
|
||||
resolve()
|
||||
}
|
||||
ctx.EditorHttpController.addFolder(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('handle invalid element name', async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.EditorController.promises.addFolder.rejects(
|
||||
new Error('invalid element name')
|
||||
)
|
||||
ctx.res.callback = () => {
|
||||
expect(ctx.res.body).to.equal('"invalid_file_name"')
|
||||
expect(ctx.res.statusCode).to.equal(400)
|
||||
resolve()
|
||||
}
|
||||
ctx.EditorHttpController.addFolder(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('renameEntity', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.entityId = 'entity-id-123'
|
||||
ctx.entityType = 'entity-type'
|
||||
ctx.req.params = {
|
||||
Project_id: ctx.project._id,
|
||||
entity_id: ctx.entityId,
|
||||
entity_type: ctx.entityType,
|
||||
}
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.newName = 'new-name'
|
||||
ctx.req.body = { name: ctx.newName, source: ctx.source }
|
||||
ctx.res.callback = resolve
|
||||
ctx.EditorHttpController.renameEntity(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call EditorController.renameEntity', function (ctx) {
|
||||
expect(
|
||||
ctx.EditorController.promises.renameEntity
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.entityId,
|
||||
ctx.entityType,
|
||||
ctx.newName,
|
||||
ctx.user._id,
|
||||
ctx.source
|
||||
)
|
||||
})
|
||||
|
||||
it('should send back a success response', function (ctx) {
|
||||
expect(ctx.res.sendStatus).to.have.been.calledWith(204)
|
||||
})
|
||||
})
|
||||
describe('with long name', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.newName = 'long'.repeat(100)
|
||||
ctx.req.body = { name: ctx.newName, source: ctx.source }
|
||||
ctx.EditorHttpController.renameEntity(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should send back a bad request status code', function (ctx) {
|
||||
expect(ctx.res.statusCode).to.equal(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with 0 length name', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.newName = ''
|
||||
ctx.req.body = { name: ctx.newName, source: ctx.source }
|
||||
ctx.EditorHttpController.renameEntity(ctx.req, ctx.res)
|
||||
})
|
||||
|
||||
it('should send back a bad request status code', function (ctx) {
|
||||
expect(ctx.res.statusCode).to.equal(400)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('moveEntity', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.entityId = 'entity-id-123'
|
||||
ctx.entityType = 'entity-type'
|
||||
ctx.folderId = 'folder-id-123'
|
||||
ctx.req.params = {
|
||||
Project_id: ctx.project._id,
|
||||
entity_id: ctx.entityId,
|
||||
entity_type: ctx.entityType,
|
||||
}
|
||||
ctx.req.body = { folder_id: ctx.folderId, source: ctx.source }
|
||||
ctx.res.callback = resolve
|
||||
ctx.EditorHttpController.moveEntity(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call EditorController.moveEntity', function (ctx) {
|
||||
expect(ctx.EditorController.promises.moveEntity).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.entityId,
|
||||
ctx.folderId,
|
||||
ctx.entityType,
|
||||
ctx.user._id,
|
||||
ctx.source
|
||||
)
|
||||
})
|
||||
|
||||
it('should send back a success response', function (ctx) {
|
||||
expect(ctx.res.statusCode).to.equal(204)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteEntity', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.entityId = 'entity-id-123'
|
||||
ctx.entityType = 'entity-type'
|
||||
ctx.req.params = {
|
||||
Project_id: ctx.project._id,
|
||||
entity_id: ctx.entityId,
|
||||
entity_type: ctx.entityType,
|
||||
}
|
||||
ctx.res.callback = resolve
|
||||
ctx.EditorHttpController.deleteEntity(ctx.req, ctx.res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call EditorController.deleteEntity', function (ctx) {
|
||||
expect(
|
||||
ctx.EditorController.promises.deleteEntity
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.entityId,
|
||||
ctx.entityType,
|
||||
'editor',
|
||||
ctx.user._id
|
||||
)
|
||||
})
|
||||
|
||||
it('should send back a success response', function (ctx) {
|
||||
expect(ctx.res.statusCode).to.equal(204)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,630 +0,0 @@
|
||||
/* eslint-disable mocha/handle-done-callback */
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const { ObjectId } = require('mongodb-legacy')
|
||||
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
||||
const MockRequest = require('../helpers/MockRequest')
|
||||
const MockResponse = require('../helpers/MockResponse')
|
||||
|
||||
const MODULE_PATH = '../../../../app/src/Features/Editor/EditorHttpController'
|
||||
|
||||
describe('EditorHttpController', function () {
|
||||
beforeEach(function () {
|
||||
this.ownerId = new ObjectId()
|
||||
this.project = {
|
||||
_id: new ObjectId(),
|
||||
owner_ref: this.ownerId,
|
||||
}
|
||||
this.user = {
|
||||
_id: new ObjectId(),
|
||||
projects: {},
|
||||
}
|
||||
this.members = [
|
||||
{ user: { _id: 'owner', features: {} }, privilegeLevel: 'owner' },
|
||||
{ user: { _id: 'one' }, privilegeLevel: 'readOnly' },
|
||||
]
|
||||
this.ownerMember = this.members[0]
|
||||
this.invites = [{ _id: 'three' }, { _id: 'four' }]
|
||||
this.projectView = {
|
||||
_id: this.project._id,
|
||||
owner: {
|
||||
_id: 'owner',
|
||||
email: 'owner@example.com',
|
||||
other_property: true,
|
||||
},
|
||||
members: [
|
||||
{ _id: 'owner', privileges: 'owner' },
|
||||
{ _id: 'one', privileges: 'readOnly' },
|
||||
],
|
||||
invites: [{ three: 3 }, { four: 4 }],
|
||||
}
|
||||
this.reducedProjectView = {
|
||||
_id: this.projectView._id,
|
||||
owner: { _id: this.projectView.owner._id },
|
||||
members: [],
|
||||
invites: [],
|
||||
}
|
||||
this.doc = { _id: new ObjectId(), name: 'excellent-original-idea.tex' }
|
||||
this.file = { _id: new ObjectId() }
|
||||
this.folder = { _id: new ObjectId() }
|
||||
this.source = 'editor'
|
||||
|
||||
this.parentFolderId = 'mock-folder-id'
|
||||
this.req = new MockRequest()
|
||||
this.res = new MockResponse()
|
||||
this.next = sinon.stub()
|
||||
this.token = null
|
||||
this.docLines = ['hello', 'overleaf']
|
||||
|
||||
this.AuthorizationManager = {
|
||||
isRestrictedUser: sinon.stub().returns(false),
|
||||
promises: {
|
||||
getPrivilegeLevelForProjectWithProjectAccess: sinon
|
||||
.stub()
|
||||
.resolves('owner'),
|
||||
},
|
||||
}
|
||||
const members = this.members
|
||||
const ownerMember = this.ownerMember
|
||||
this.CollaboratorsGetter = {
|
||||
ProjectAccess: class {
|
||||
loadOwnerAndInvitedMembers() {
|
||||
return { members, ownerMember }
|
||||
}
|
||||
|
||||
loadOwner() {
|
||||
return ownerMember
|
||||
}
|
||||
|
||||
isUserTokenMember() {
|
||||
return false
|
||||
}
|
||||
|
||||
isUserInvitedMember() {
|
||||
return false
|
||||
}
|
||||
},
|
||||
promises: {
|
||||
isUserInvitedMemberOfProject: sinon.stub().resolves(false),
|
||||
},
|
||||
}
|
||||
this.CollaboratorsHandler = {
|
||||
promises: {
|
||||
userIsTokenMember: sinon.stub().resolves(false),
|
||||
},
|
||||
}
|
||||
this.invites = [
|
||||
{
|
||||
_id: 'invite_one',
|
||||
email: 'user-one@example.com',
|
||||
privileges: 'readOnly',
|
||||
projectId: this.project._id,
|
||||
},
|
||||
{
|
||||
_id: 'invite_two',
|
||||
email: 'user-two@example.com',
|
||||
privileges: 'readOnly',
|
||||
projectId: this.project._id,
|
||||
},
|
||||
]
|
||||
this.CollaboratorsInviteGetter = {
|
||||
promises: {
|
||||
getAllInvites: sinon.stub().resolves(this.invites),
|
||||
},
|
||||
}
|
||||
this.EditorController = {
|
||||
promises: {
|
||||
addDoc: sinon.stub().resolves(this.doc),
|
||||
addFile: sinon.stub().resolves(this.file),
|
||||
addFolder: sinon.stub().resolves(this.folder),
|
||||
renameEntity: sinon.stub().resolves(),
|
||||
moveEntity: sinon.stub().resolves(),
|
||||
deleteEntity: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.ProjectDeleter = {
|
||||
promises: {
|
||||
unmarkAsDeletedByExternalSource: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
this.ProjectGetter = {
|
||||
promises: {
|
||||
getProjectWithoutDocLines: sinon.stub().resolves(this.project),
|
||||
},
|
||||
}
|
||||
this.ProjectEditorHandler = {
|
||||
buildProjectModelView: sinon.stub().returns(this.projectView),
|
||||
}
|
||||
this.Metrics = { inc: sinon.stub() }
|
||||
this.TokenAccessHandler = {
|
||||
getRequestToken: sinon.stub().returns(this.token),
|
||||
}
|
||||
this.SessionManager = {
|
||||
getLoggedInUserId: sinon.stub().returns(this.user._id),
|
||||
}
|
||||
this.ProjectEntityUpdateHandler = {
|
||||
promises: {
|
||||
convertDocToFile: sinon.stub().resolves(this.file),
|
||||
},
|
||||
}
|
||||
this.DocstoreManager = {
|
||||
promises: {
|
||||
getAllDeletedDocs: sinon.stub().resolves([]),
|
||||
},
|
||||
}
|
||||
this.HttpErrorHandler = {
|
||||
notFound: sinon.stub(),
|
||||
unprocessableEntity: sinon.stub(),
|
||||
}
|
||||
this.SplitTestHandler = {
|
||||
promises: {
|
||||
getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }),
|
||||
},
|
||||
}
|
||||
this.UserGetter = { promises: { getUser: sinon.stub().resolves(null, {}) } }
|
||||
this.EditorHttpController = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
'../Project/ProjectDeleter': this.ProjectDeleter,
|
||||
'../Project/ProjectGetter': this.ProjectGetter,
|
||||
'../Authorization/AuthorizationManager': this.AuthorizationManager,
|
||||
'../Project/ProjectEditorHandler': this.ProjectEditorHandler,
|
||||
'./EditorController': this.EditorController,
|
||||
'@overleaf/metrics': this.Metrics,
|
||||
'../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
|
||||
'../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
|
||||
'../Collaborators/CollaboratorsInviteGetter':
|
||||
this.CollaboratorsInviteGetter,
|
||||
'../TokenAccess/TokenAccessHandler': this.TokenAccessHandler,
|
||||
'../Authentication/SessionManager': this.SessionManager,
|
||||
'../../infrastructure/FileWriter': this.FileWriter,
|
||||
'../Project/ProjectEntityUpdateHandler':
|
||||
this.ProjectEntityUpdateHandler,
|
||||
'../Docstore/DocstoreManager': this.DocstoreManager,
|
||||
'../Errors/HttpErrorHandler': this.HttpErrorHandler,
|
||||
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
|
||||
'../Compile/CompileManager': {},
|
||||
'../User/UserGetter': this.UserGetter,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('joinProject', function () {
|
||||
beforeEach(function () {
|
||||
this.req.params = { Project_id: this.project._id }
|
||||
this.req.query = { user_id: this.user._id }
|
||||
this.req.body = { userId: this.user._id }
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(function (done) {
|
||||
sinon
|
||||
.stub(
|
||||
this.CollaboratorsGetter.ProjectAccess.prototype,
|
||||
'isUserInvitedMember'
|
||||
)
|
||||
.returns(true)
|
||||
this.res.callback = done
|
||||
this.EditorHttpController.joinProject(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should request a full view', function () {
|
||||
expect(
|
||||
this.ProjectEditorHandler.buildProjectModelView
|
||||
).to.have.been.calledWith(
|
||||
this.project,
|
||||
this.ownerMember,
|
||||
this.members,
|
||||
this.invites,
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the project and privilege level', function () {
|
||||
expect(this.res.json).to.have.been.calledWith({
|
||||
project: this.projectView,
|
||||
privilegeLevel: 'owner',
|
||||
isRestrictedUser: false,
|
||||
isTokenMember: false,
|
||||
isInvitedMember: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not try to unmark the project as deleted', function () {
|
||||
expect(this.ProjectDeleter.promises.unmarkAsDeletedByExternalSource).not
|
||||
.to.have.been.called
|
||||
})
|
||||
|
||||
it('should send an inc metric', function () {
|
||||
expect(this.Metrics.inc).to.have.been.calledWith('editor.join-project')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the project is marked as deleted', function () {
|
||||
beforeEach(function (done) {
|
||||
this.projectView.deletedByExternalDataSource = true
|
||||
this.res.callback = done
|
||||
this.EditorHttpController.joinProject(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should unmark the project as deleted', function () {
|
||||
expect(
|
||||
this.ProjectDeleter.promises.unmarkAsDeletedByExternalSource
|
||||
).to.have.been.calledWith(this.project._id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a restricted user', function () {
|
||||
beforeEach(function (done) {
|
||||
this.ProjectEditorHandler.buildProjectModelView.returns(
|
||||
this.reducedProjectView
|
||||
)
|
||||
this.AuthorizationManager.isRestrictedUser.returns(true)
|
||||
this.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess.resolves(
|
||||
'readOnly'
|
||||
)
|
||||
this.res.callback = done
|
||||
this.EditorHttpController.joinProject(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should request a restricted view', function () {
|
||||
expect(
|
||||
this.ProjectEditorHandler.buildProjectModelView
|
||||
).to.have.been.calledWith(this.project, this.ownerMember, [], [], true)
|
||||
})
|
||||
|
||||
it('should mark the user as restricted, and hide details of owner', function () {
|
||||
expect(this.res.json).to.have.been.calledWith({
|
||||
project: this.reducedProjectView,
|
||||
privilegeLevel: 'readOnly',
|
||||
isRestrictedUser: true,
|
||||
isTokenMember: false,
|
||||
isInvitedMember: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when not authorized', function () {
|
||||
beforeEach(function (done) {
|
||||
this.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess.resolves(
|
||||
null
|
||||
)
|
||||
this.res.callback = done
|
||||
this.EditorHttpController.joinProject(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send a 403 response', function () {
|
||||
expect(this.res.statusCode).to.equal(403)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an anonymous user', function () {
|
||||
beforeEach(function (done) {
|
||||
this.token = 'token'
|
||||
this.TokenAccessHandler.getRequestToken.returns(this.token)
|
||||
this.ProjectEditorHandler.buildProjectModelView.returns(
|
||||
this.reducedProjectView
|
||||
)
|
||||
this.req.body = {
|
||||
userId: 'anonymous-user',
|
||||
anonymousAccessToken: this.token,
|
||||
}
|
||||
this.res.callback = done
|
||||
this.AuthorizationManager.isRestrictedUser
|
||||
.withArgs(null, 'readOnly', false, false)
|
||||
.returns(true)
|
||||
this.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess
|
||||
.withArgs(null, this.project._id, this.token)
|
||||
.resolves('readOnly')
|
||||
this.EditorHttpController.joinProject(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should request a restricted view', function () {
|
||||
expect(
|
||||
this.ProjectEditorHandler.buildProjectModelView
|
||||
).to.have.been.calledWith(this.project, this.ownerMember, [], [], true)
|
||||
})
|
||||
|
||||
it('should mark the user as restricted', function () {
|
||||
expect(this.res.json).to.have.been.calledWith({
|
||||
project: this.reducedProjectView,
|
||||
privilegeLevel: 'readOnly',
|
||||
isRestrictedUser: true,
|
||||
isTokenMember: false,
|
||||
isInvitedMember: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a token access user', function () {
|
||||
beforeEach(function (done) {
|
||||
sinon
|
||||
.stub(
|
||||
this.CollaboratorsGetter.ProjectAccess.prototype,
|
||||
'isUserInvitedMember'
|
||||
)
|
||||
.returns(false)
|
||||
sinon
|
||||
.stub(
|
||||
this.CollaboratorsGetter.ProjectAccess.prototype,
|
||||
'isUserTokenMember'
|
||||
)
|
||||
.returns(true)
|
||||
this.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess.resolves(
|
||||
'readAndWrite'
|
||||
)
|
||||
this.res.callback = done
|
||||
this.EditorHttpController.joinProject(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should mark the user as being a token-access member', function () {
|
||||
expect(this.res.json).to.have.been.calledWith({
|
||||
project: this.projectView,
|
||||
privilegeLevel: 'readAndWrite',
|
||||
isRestrictedUser: false,
|
||||
isTokenMember: true,
|
||||
isInvitedMember: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when project is not found', function () {
|
||||
beforeEach(function (done) {
|
||||
this.ProjectGetter.promises.getProjectWithoutDocLines.resolves(null)
|
||||
this.next.callsFake(() => done())
|
||||
this.EditorHttpController.joinProject(this.req, this.res, this.next)
|
||||
})
|
||||
|
||||
it('should handle return not found error', function () {
|
||||
expect(this.next).to.have.been.calledWith(
|
||||
sinon.match.instanceOf(Errors.NotFoundError)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addDoc', function () {
|
||||
beforeEach(function () {
|
||||
this.req.params = { Project_id: this.project._id }
|
||||
this.req.body = {
|
||||
name: (this.name = 'doc-name'),
|
||||
parent_folder_id: this.parentFolderId,
|
||||
}
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(function (done) {
|
||||
this.res.callback = done
|
||||
this.EditorHttpController.addDoc(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should call EditorController.addDoc', function () {
|
||||
expect(this.EditorController.promises.addDoc).to.have.been.calledWith(
|
||||
this.project._id,
|
||||
this.parentFolderId,
|
||||
this.name,
|
||||
[],
|
||||
'editor',
|
||||
this.user._id
|
||||
)
|
||||
})
|
||||
|
||||
it('should send the doc back as JSON', function () {
|
||||
expect(this.res.json).to.have.been.calledWith(this.doc)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unsuccesfully', function () {
|
||||
it('handle name too short', function (done) {
|
||||
this.req.body.name = ''
|
||||
this.res.callback = () => {
|
||||
expect(this.res.statusCode).to.equal(400)
|
||||
done()
|
||||
}
|
||||
this.EditorHttpController.addDoc(this.req, this.res)
|
||||
})
|
||||
|
||||
it('handle too many files', function (done) {
|
||||
this.EditorController.promises.addDoc.rejects(
|
||||
new Error('project_has_too_many_files')
|
||||
)
|
||||
this.res.callback = () => {
|
||||
expect(this.res.body).to.equal('"project_has_too_many_files"')
|
||||
expect(this.res.status).to.have.been.calledWith(400)
|
||||
done()
|
||||
}
|
||||
this.EditorHttpController.addDoc(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addFolder', function () {
|
||||
beforeEach(function () {
|
||||
this.folderName = 'folder-name'
|
||||
this.req.params = { Project_id: this.project._id }
|
||||
this.req.body = {
|
||||
name: this.folderName,
|
||||
parent_folder_id: this.parentFolderId,
|
||||
}
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(function (done) {
|
||||
this.res.callback = done
|
||||
this.EditorHttpController.addFolder(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should call EditorController.addFolder', function () {
|
||||
expect(
|
||||
this.EditorController.promises.addFolder
|
||||
).to.have.been.calledWith(
|
||||
this.project._id,
|
||||
this.parentFolderId,
|
||||
this.folderName,
|
||||
'editor'
|
||||
)
|
||||
})
|
||||
|
||||
it('should send the folder back as JSON', function () {
|
||||
expect(this.res.json).to.have.been.calledWith(this.folder)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unsuccesfully', function () {
|
||||
it('handle name too short', function (done) {
|
||||
this.req.body.name = ''
|
||||
this.res.callback = () => {
|
||||
expect(this.res.statusCode).to.equal(400)
|
||||
done()
|
||||
}
|
||||
this.EditorHttpController.addFolder(this.req, this.res)
|
||||
})
|
||||
|
||||
it('handle too many files', function (done) {
|
||||
this.EditorController.promises.addFolder.rejects(
|
||||
new Error('project_has_too_many_files')
|
||||
)
|
||||
this.res.callback = () => {
|
||||
expect(this.res.body).to.equal('"project_has_too_many_files"')
|
||||
expect(this.res.statusCode).to.equal(400)
|
||||
done()
|
||||
}
|
||||
this.EditorHttpController.addFolder(this.req, this.res)
|
||||
})
|
||||
|
||||
it('handle invalid element name', function (done) {
|
||||
this.EditorController.promises.addFolder.rejects(
|
||||
new Error('invalid element name')
|
||||
)
|
||||
this.res.callback = () => {
|
||||
expect(this.res.body).to.equal('"invalid_file_name"')
|
||||
expect(this.res.statusCode).to.equal(400)
|
||||
done()
|
||||
}
|
||||
this.EditorHttpController.addFolder(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('renameEntity', function () {
|
||||
beforeEach(function () {
|
||||
this.entityId = 'entity-id-123'
|
||||
this.entityType = 'entity-type'
|
||||
this.req.params = {
|
||||
Project_id: this.project._id,
|
||||
entity_id: this.entityId,
|
||||
entity_type: this.entityType,
|
||||
}
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(function (done) {
|
||||
this.newName = 'new-name'
|
||||
this.req.body = { name: this.newName, source: this.source }
|
||||
this.res.callback = done
|
||||
this.EditorHttpController.renameEntity(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should call EditorController.renameEntity', function () {
|
||||
expect(
|
||||
this.EditorController.promises.renameEntity
|
||||
).to.have.been.calledWith(
|
||||
this.project._id,
|
||||
this.entityId,
|
||||
this.entityType,
|
||||
this.newName,
|
||||
this.user._id,
|
||||
this.source
|
||||
)
|
||||
})
|
||||
|
||||
it('should send back a success response', function () {
|
||||
expect(this.res.sendStatus).to.have.been.calledWith(204)
|
||||
})
|
||||
})
|
||||
describe('with long name', function () {
|
||||
beforeEach(function () {
|
||||
this.newName = 'long'.repeat(100)
|
||||
this.req.body = { name: this.newName, source: this.source }
|
||||
this.EditorHttpController.renameEntity(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send back a bad request status code', function () {
|
||||
expect(this.res.statusCode).to.equal(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with 0 length name', function () {
|
||||
beforeEach(function () {
|
||||
this.newName = ''
|
||||
this.req.body = { name: this.newName, source: this.source }
|
||||
this.EditorHttpController.renameEntity(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send back a bad request status code', function () {
|
||||
expect(this.res.statusCode).to.equal(400)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('moveEntity', function () {
|
||||
beforeEach(function (done) {
|
||||
this.entityId = 'entity-id-123'
|
||||
this.entityType = 'entity-type'
|
||||
this.folderId = 'folder-id-123'
|
||||
this.req.params = {
|
||||
Project_id: this.project._id,
|
||||
entity_id: this.entityId,
|
||||
entity_type: this.entityType,
|
||||
}
|
||||
this.req.body = { folder_id: this.folderId, source: this.source }
|
||||
this.res.callback = done
|
||||
this.EditorHttpController.moveEntity(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should call EditorController.moveEntity', function () {
|
||||
expect(this.EditorController.promises.moveEntity).to.have.been.calledWith(
|
||||
this.project._id,
|
||||
this.entityId,
|
||||
this.folderId,
|
||||
this.entityType,
|
||||
this.user._id,
|
||||
this.source
|
||||
)
|
||||
})
|
||||
|
||||
it('should send back a success response', function () {
|
||||
expect(this.res.statusCode).to.equal(204)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteEntity', function () {
|
||||
beforeEach(function (done) {
|
||||
this.entityId = 'entity-id-123'
|
||||
this.entityType = 'entity-type'
|
||||
this.req.params = {
|
||||
Project_id: this.project._id,
|
||||
entity_id: this.entityId,
|
||||
entity_type: this.entityType,
|
||||
}
|
||||
this.res.callback = done
|
||||
this.EditorHttpController.deleteEntity(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should call EditorController.deleteEntity', function () {
|
||||
expect(
|
||||
this.EditorController.promises.deleteEntity
|
||||
).to.have.been.calledWith(
|
||||
this.project._id,
|
||||
this.entityId,
|
||||
this.entityType,
|
||||
'editor',
|
||||
this.user._id
|
||||
)
|
||||
})
|
||||
|
||||
it('should send back a success response', function () {
|
||||
expect(this.res.statusCode).to.equal(204)
|
||||
})
|
||||
})
|
||||
})
|
||||
297
services/web/test/unit/src/History/HistoryController.test.mjs
Normal file
297
services/web/test/unit/src/History/HistoryController.test.mjs
Normal file
@@ -0,0 +1,297 @@
|
||||
import { vi, expect } from 'vitest'
|
||||
import sinon from 'sinon'
|
||||
import { RequestFailedError } from '@overleaf/fetch-utils'
|
||||
import Errors from '../../../../app/src/Features/Errors/Errors.js'
|
||||
import mongodb from 'mongodb-legacy'
|
||||
|
||||
const { ObjectId } = mongodb
|
||||
|
||||
const modulePath = '../../../../app/src/Features/History/HistoryController.mjs'
|
||||
|
||||
describe('HistoryController', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.callback = sinon.stub()
|
||||
ctx.user_id = 'user-id-123'
|
||||
ctx.project_id = 'mock-project-id'
|
||||
ctx.stream = sinon.stub()
|
||||
ctx.fetchResponse = {
|
||||
headers: {
|
||||
get: sinon.stub(),
|
||||
},
|
||||
}
|
||||
ctx.next = sinon.stub()
|
||||
|
||||
ctx.SessionManager = {
|
||||
getLoggedInUserId: sinon.stub().returns(ctx.user_id),
|
||||
}
|
||||
|
||||
ctx.Stream = {
|
||||
pipeline: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
ctx.HistoryManager = {
|
||||
promises: {
|
||||
injectUserDetails: sinon.stub(),
|
||||
},
|
||||
}
|
||||
|
||||
ctx.ProjectEntityUpdateHandler = {
|
||||
promises: {
|
||||
resyncProjectHistory: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
ctx.fetchJson = sinon.stub()
|
||||
ctx.fetchStream = sinon.stub().resolves(ctx.stream)
|
||||
ctx.fetchStreamWithResponse = sinon
|
||||
.stub()
|
||||
.resolves({ stream: ctx.stream, response: ctx.fetchResponse })
|
||||
ctx.fetchNothing = sinon.stub().resolves()
|
||||
|
||||
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
|
||||
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
|
||||
)
|
||||
|
||||
vi.doMock('stream/promises', () => ctx.Stream)
|
||||
|
||||
vi.doMock('@overleaf/settings', () => ({
|
||||
default: (ctx.settings = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('@overleaf/fetch-utils', () => ({
|
||||
fetchJson: ctx.fetchJson,
|
||||
fetchStream: ctx.fetchStream,
|
||||
fetchStreamWithResponse: ctx.fetchStreamWithResponse,
|
||||
fetchNothing: ctx.fetchNothing,
|
||||
}))
|
||||
|
||||
vi.doMock('@overleaf/Metrics', () => ({
|
||||
default: {},
|
||||
}))
|
||||
|
||||
vi.doMock('../../../../app/src/infrastructure/mongodb.js', () => ({
|
||||
default: { ObjectId },
|
||||
}))
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Authentication/SessionManager.js',
|
||||
() => ({
|
||||
default: ctx.SessionManager,
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock('../../../../app/src/Features/History/HistoryManager.js', () => ({
|
||||
default: ctx.HistoryManager,
|
||||
}))
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Project/ProjectDetailsHandler.js',
|
||||
() => ({
|
||||
default: (ctx.ProjectDetailsHandler = {}),
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Project/ProjectEntityUpdateHandler.js',
|
||||
() => ({
|
||||
default: ctx.ProjectEntityUpdateHandler,
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({
|
||||
default: (ctx.UserGetter = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({
|
||||
default: (ctx.ProjectGetter = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../../app/src/Features/History/RestoreManager.js', () => ({
|
||||
default: (ctx.RestoreManager = {}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../../../app/src/infrastructure/Features.js', () => ({
|
||||
default: (ctx.Features = sinon.stub().withArgs('saas').returns(true)),
|
||||
}))
|
||||
|
||||
ctx.HistoryController = (await import(modulePath)).default
|
||||
ctx.settings.apis = {
|
||||
project_history: {
|
||||
url: 'http://project_history.example.com',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('proxyToHistoryApi', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.req = { url: '/mock/url', method: 'POST', session: sinon.stub() }
|
||||
ctx.res = {
|
||||
set: sinon.stub(),
|
||||
}
|
||||
ctx.contentType = 'application/json'
|
||||
ctx.contentLength = 212
|
||||
ctx.fetchResponse.headers.get
|
||||
.withArgs('Content-Type')
|
||||
.returns(ctx.contentType)
|
||||
ctx.fetchResponse.headers.get
|
||||
.withArgs('Content-Length')
|
||||
.returns(ctx.contentLength)
|
||||
await ctx.HistoryController.proxyToHistoryApi(ctx.req, ctx.res, ctx.next)
|
||||
})
|
||||
|
||||
it('should get the user id', function (ctx) {
|
||||
ctx.SessionManager.getLoggedInUserId.should.have.been.calledWith(
|
||||
ctx.req.session
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the project history api', function (ctx) {
|
||||
ctx.fetchStreamWithResponse.should.have.been.calledWith(
|
||||
`${ctx.settings.apis.project_history.url}${ctx.req.url}`,
|
||||
{
|
||||
method: ctx.req.method,
|
||||
headers: {
|
||||
'X-User-Id': ctx.user_id,
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should pipe the response to the client', function (ctx) {
|
||||
expect(ctx.Stream.pipeline).to.have.been.calledWith(ctx.stream, ctx.res)
|
||||
})
|
||||
|
||||
it('should propagate the appropriate headers', function (ctx) {
|
||||
expect(ctx.res.set).to.have.been.calledWith(
|
||||
'Content-Type',
|
||||
ctx.contentType
|
||||
)
|
||||
expect(ctx.res.set).to.have.been.calledWith(
|
||||
'Content-Length',
|
||||
ctx.contentLength
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('proxyToHistoryApiAndInjectUserDetails', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.req = { url: '/mock/url', method: 'POST' }
|
||||
ctx.res = { json: sinon.stub() }
|
||||
ctx.data = 'mock-data'
|
||||
ctx.dataWithUsers = 'mock-injected-data'
|
||||
ctx.fetchJson.resolves(ctx.data)
|
||||
ctx.HistoryManager.promises.injectUserDetails.resolves(ctx.dataWithUsers)
|
||||
await ctx.HistoryController.proxyToHistoryApiAndInjectUserDetails(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
ctx.next
|
||||
)
|
||||
})
|
||||
|
||||
it('should get the user id', function (ctx) {
|
||||
ctx.SessionManager.getLoggedInUserId.should.have.been.calledWith(
|
||||
ctx.req.session
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the project history api', function (ctx) {
|
||||
ctx.fetchJson.should.have.been.calledWith(
|
||||
`${ctx.settings.apis.project_history.url}${ctx.req.url}`,
|
||||
{
|
||||
method: ctx.req.method,
|
||||
headers: {
|
||||
'X-User-Id': ctx.user_id,
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should inject the user data', function (ctx) {
|
||||
ctx.HistoryManager.promises.injectUserDetails.should.have.been.calledWith(
|
||||
ctx.data
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the data with users to the client', function (ctx) {
|
||||
ctx.res.json.should.have.been.calledWith(ctx.dataWithUsers)
|
||||
})
|
||||
})
|
||||
|
||||
describe('proxyToHistoryApiAndInjectUserDetails (with the history API failing)', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.url = '/mock/url'
|
||||
ctx.req = { url: ctx.url, method: 'POST' }
|
||||
ctx.res = { json: sinon.stub() }
|
||||
ctx.err = new RequestFailedError(ctx.url, {}, { status: 500 })
|
||||
ctx.fetchJson.rejects(ctx.err)
|
||||
await ctx.HistoryController.proxyToHistoryApiAndInjectUserDetails(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
ctx.next
|
||||
)
|
||||
})
|
||||
|
||||
it('should not inject the user data', function (ctx) {
|
||||
ctx.HistoryManager.promises.injectUserDetails.should.not.have.been.called
|
||||
})
|
||||
|
||||
it('should not return the data with users to the client', function (ctx) {
|
||||
ctx.res.json.should.not.have.been.called
|
||||
})
|
||||
|
||||
it('should throw an error', function (ctx) {
|
||||
ctx.next.should.have.been.calledWith(ctx.err)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resyncProjectHistory', function () {
|
||||
describe('for a project without project-history enabled', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.req = { params: { Project_id: ctx.project_id }, body: {} }
|
||||
ctx.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
|
||||
|
||||
ctx.error = new Errors.ProjectHistoryDisabledError()
|
||||
ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory.rejects(
|
||||
ctx.error
|
||||
)
|
||||
|
||||
await ctx.HistoryController.resyncProjectHistory(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
ctx.next
|
||||
)
|
||||
})
|
||||
|
||||
it('response with a 404', function (ctx) {
|
||||
ctx.res.sendStatus.should.have.been.calledWith(404)
|
||||
})
|
||||
})
|
||||
|
||||
describe('for a project with project-history enabled', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.req = { params: { Project_id: ctx.project_id }, body: {} }
|
||||
ctx.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
|
||||
|
||||
await ctx.HistoryController.resyncProjectHistory(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
ctx.next
|
||||
)
|
||||
})
|
||||
|
||||
it('sets an extended response timeout', function (ctx) {
|
||||
ctx.res.setTimeout.should.have.been.calledWith(6 * 60 * 1000)
|
||||
})
|
||||
|
||||
it('resyncs the project', function (ctx) {
|
||||
ctx.ProjectEntityUpdateHandler.promises.resyncProjectHistory.should.have.been.calledWith(
|
||||
ctx.project_id
|
||||
)
|
||||
})
|
||||
|
||||
it('responds with a 204', function (ctx) {
|
||||
ctx.res.sendStatus.should.have.been.calledWith(204)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,265 +0,0 @@
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const { RequestFailedError } = require('@overleaf/fetch-utils')
|
||||
|
||||
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
||||
|
||||
const modulePath = '../../../../app/src/Features/History/HistoryController'
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const { ObjectId } = require('mongodb-legacy')
|
||||
|
||||
describe('HistoryController', function () {
|
||||
beforeEach(function () {
|
||||
this.callback = sinon.stub()
|
||||
this.user_id = 'user-id-123'
|
||||
this.project_id = 'mock-project-id'
|
||||
this.stream = sinon.stub()
|
||||
this.fetchResponse = {
|
||||
headers: {
|
||||
get: sinon.stub(),
|
||||
},
|
||||
}
|
||||
this.next = sinon.stub()
|
||||
|
||||
this.SessionManager = {
|
||||
getLoggedInUserId: sinon.stub().returns(this.user_id),
|
||||
}
|
||||
|
||||
this.Stream = {
|
||||
pipeline: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
this.HistoryManager = {
|
||||
promises: {
|
||||
injectUserDetails: sinon.stub(),
|
||||
},
|
||||
}
|
||||
|
||||
this.ProjectEntityUpdateHandler = {
|
||||
promises: {
|
||||
resyncProjectHistory: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
this.fetchJson = sinon.stub()
|
||||
this.fetchStream = sinon.stub().resolves(this.stream)
|
||||
this.fetchStreamWithResponse = sinon
|
||||
.stub()
|
||||
.resolves({ stream: this.stream, response: this.fetchResponse })
|
||||
this.fetchNothing = sinon.stub().resolves()
|
||||
|
||||
this.HistoryController = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'stream/promises': this.Stream,
|
||||
'@overleaf/settings': (this.settings = {}),
|
||||
'@overleaf/fetch-utils': {
|
||||
fetchJson: this.fetchJson,
|
||||
fetchStream: this.fetchStream,
|
||||
fetchStreamWithResponse: this.fetchStreamWithResponse,
|
||||
fetchNothing: this.fetchNothing,
|
||||
},
|
||||
'@overleaf/Metrics': {},
|
||||
'../../infrastructure/mongodb': { ObjectId },
|
||||
'../Authentication/SessionManager': this.SessionManager,
|
||||
'./HistoryManager': this.HistoryManager,
|
||||
'../Project/ProjectDetailsHandler': (this.ProjectDetailsHandler = {}),
|
||||
'../Project/ProjectEntityUpdateHandler':
|
||||
this.ProjectEntityUpdateHandler,
|
||||
'../User/UserGetter': (this.UserGetter = {}),
|
||||
'../Project/ProjectGetter': (this.ProjectGetter = {}),
|
||||
'./RestoreManager': (this.RestoreManager = {}),
|
||||
'../../infrastructure/Features': (this.Features = sinon
|
||||
.stub()
|
||||
.withArgs('saas')
|
||||
.returns(true)),
|
||||
},
|
||||
})
|
||||
this.settings.apis = {
|
||||
project_history: {
|
||||
url: 'http://project_history.example.com',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('proxyToHistoryApi', function () {
|
||||
beforeEach(async function () {
|
||||
this.req = { url: '/mock/url', method: 'POST', session: sinon.stub() }
|
||||
this.res = {
|
||||
set: sinon.stub(),
|
||||
}
|
||||
this.contentType = 'application/json'
|
||||
this.contentLength = 212
|
||||
this.fetchResponse.headers.get
|
||||
.withArgs('Content-Type')
|
||||
.returns(this.contentType)
|
||||
this.fetchResponse.headers.get
|
||||
.withArgs('Content-Length')
|
||||
.returns(this.contentLength)
|
||||
await this.HistoryController.proxyToHistoryApi(
|
||||
this.req,
|
||||
this.res,
|
||||
this.next
|
||||
)
|
||||
})
|
||||
|
||||
it('should get the user id', function () {
|
||||
this.SessionManager.getLoggedInUserId.should.have.been.calledWith(
|
||||
this.req.session
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the project history api', function () {
|
||||
this.fetchStreamWithResponse.should.have.been.calledWith(
|
||||
`${this.settings.apis.project_history.url}${this.req.url}`,
|
||||
{
|
||||
method: this.req.method,
|
||||
headers: {
|
||||
'X-User-Id': this.user_id,
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should pipe the response to the client', function () {
|
||||
expect(this.Stream.pipeline).to.have.been.calledWith(
|
||||
this.stream,
|
||||
this.res
|
||||
)
|
||||
})
|
||||
|
||||
it('should propagate the appropriate headers', function () {
|
||||
expect(this.res.set).to.have.been.calledWith(
|
||||
'Content-Type',
|
||||
this.contentType
|
||||
)
|
||||
expect(this.res.set).to.have.been.calledWith(
|
||||
'Content-Length',
|
||||
this.contentLength
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('proxyToHistoryApiAndInjectUserDetails', function () {
|
||||
beforeEach(async function () {
|
||||
this.req = { url: '/mock/url', method: 'POST' }
|
||||
this.res = { json: sinon.stub() }
|
||||
this.data = 'mock-data'
|
||||
this.dataWithUsers = 'mock-injected-data'
|
||||
this.fetchJson.resolves(this.data)
|
||||
this.HistoryManager.promises.injectUserDetails.resolves(
|
||||
this.dataWithUsers
|
||||
)
|
||||
await this.HistoryController.proxyToHistoryApiAndInjectUserDetails(
|
||||
this.req,
|
||||
this.res,
|
||||
this.next
|
||||
)
|
||||
})
|
||||
|
||||
it('should get the user id', function () {
|
||||
this.SessionManager.getLoggedInUserId.should.have.been.calledWith(
|
||||
this.req.session
|
||||
)
|
||||
})
|
||||
|
||||
it('should call the project history api', function () {
|
||||
this.fetchJson.should.have.been.calledWith(
|
||||
`${this.settings.apis.project_history.url}${this.req.url}`,
|
||||
{
|
||||
method: this.req.method,
|
||||
headers: {
|
||||
'X-User-Id': this.user_id,
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should inject the user data', function () {
|
||||
this.HistoryManager.promises.injectUserDetails.should.have.been.calledWith(
|
||||
this.data
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the data with users to the client', function () {
|
||||
this.res.json.should.have.been.calledWith(this.dataWithUsers)
|
||||
})
|
||||
})
|
||||
|
||||
describe('proxyToHistoryApiAndInjectUserDetails (with the history API failing)', function () {
|
||||
beforeEach(async function () {
|
||||
this.url = '/mock/url'
|
||||
this.req = { url: this.url, method: 'POST' }
|
||||
this.res = { json: sinon.stub() }
|
||||
this.err = new RequestFailedError(this.url, {}, { status: 500 })
|
||||
this.fetchJson.rejects(this.err)
|
||||
await this.HistoryController.proxyToHistoryApiAndInjectUserDetails(
|
||||
this.req,
|
||||
this.res,
|
||||
this.next
|
||||
)
|
||||
})
|
||||
|
||||
it('should not inject the user data', function () {
|
||||
this.HistoryManager.promises.injectUserDetails.should.not.have.been.called
|
||||
})
|
||||
|
||||
it('should not return the data with users to the client', function () {
|
||||
this.res.json.should.not.have.been.called
|
||||
})
|
||||
|
||||
it('should throw an error', function () {
|
||||
this.next.should.have.been.calledWith(this.err)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resyncProjectHistory', function () {
|
||||
describe('for a project without project-history enabled', function () {
|
||||
beforeEach(async function () {
|
||||
this.req = { params: { Project_id: this.project_id }, body: {} }
|
||||
this.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
|
||||
|
||||
this.error = new Errors.ProjectHistoryDisabledError()
|
||||
this.ProjectEntityUpdateHandler.promises.resyncProjectHistory.rejects(
|
||||
this.error
|
||||
)
|
||||
|
||||
await this.HistoryController.resyncProjectHistory(
|
||||
this.req,
|
||||
this.res,
|
||||
this.next
|
||||
)
|
||||
})
|
||||
|
||||
it('response with a 404', function () {
|
||||
this.res.sendStatus.should.have.been.calledWith(404)
|
||||
})
|
||||
})
|
||||
|
||||
describe('for a project with project-history enabled', function () {
|
||||
beforeEach(async function () {
|
||||
this.req = { params: { Project_id: this.project_id }, body: {} }
|
||||
this.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
|
||||
|
||||
await this.HistoryController.resyncProjectHistory(
|
||||
this.req,
|
||||
this.res,
|
||||
this.next
|
||||
)
|
||||
})
|
||||
|
||||
it('sets an extended response timeout', function () {
|
||||
this.res.setTimeout.should.have.been.calledWith(6 * 60 * 1000)
|
||||
})
|
||||
|
||||
it('resyncs the project', function () {
|
||||
this.ProjectEntityUpdateHandler.promises.resyncProjectHistory.should.have.been.calledWith(
|
||||
this.project_id
|
||||
)
|
||||
})
|
||||
|
||||
it('responds with a 204', function () {
|
||||
this.res.sendStatus.should.have.been.calledWith(204)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,425 @@
|
||||
import { vi, expect } from 'vitest'
|
||||
import mongodb from 'mongodb-legacy'
|
||||
import sinon from 'sinon'
|
||||
|
||||
const { ObjectId } = mongodb
|
||||
|
||||
const MODULE_PATH =
|
||||
'../../../../app/src/Features/Project/ProjectCollabratecDetailsHandler.mjs'
|
||||
|
||||
describe('ProjectCollabratecDetailsHandler', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.projectId = new ObjectId('5bea8747c7bba6012fcaceb3')
|
||||
ctx.userId = new ObjectId('5be316a9c7f6aa03802ea8fb')
|
||||
ctx.userId2 = new ObjectId('5c1794b3f0e89b1d1c577eca')
|
||||
ctx.ProjectModel = {}
|
||||
|
||||
vi.doMock('mongodb-legacy', () => ({
|
||||
default: { ObjectId },
|
||||
}))
|
||||
|
||||
vi.doMock('../../../../app/src/models/Project.js', () => ({
|
||||
Project: ctx.ProjectModel,
|
||||
}))
|
||||
|
||||
ctx.ProjectCollabratecDetailsHandler = (await import(MODULE_PATH)).default
|
||||
})
|
||||
|
||||
describe('initializeCollabratecProject', function () {
|
||||
describe('when update succeeds', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
await ctx.ProjectCollabratecDetailsHandler.promises.initializeCollabratecProject(
|
||||
ctx.projectId,
|
||||
ctx.userId,
|
||||
'collabratec-document-id',
|
||||
'collabratec-private-group-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should update project model', function (ctx) {
|
||||
const update = {
|
||||
$set: {
|
||||
collabratecUsers: [
|
||||
{
|
||||
user_id: ctx.userId,
|
||||
collabratec_document_id: 'collabratec-document-id',
|
||||
collabratec_privategroup_id: 'collabratec-private-group-id',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: ctx.projectId },
|
||||
update
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when update has error', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects() })
|
||||
})
|
||||
|
||||
it('should be rejected', async function (ctx) {
|
||||
await expect(
|
||||
ctx.ProjectCollabratecDetailsHandler.promises.initializeCollabratecProject(
|
||||
ctx.projectId,
|
||||
ctx.userId,
|
||||
'collabratec-document-id',
|
||||
'collabratec-private-group-id'
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid args', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
ctx.resultPromise =
|
||||
ctx.ProjectCollabratecDetailsHandler.promises.initializeCollabratecProject(
|
||||
'bad-project-id',
|
||||
'bad-user-id',
|
||||
'collabratec-document-id',
|
||||
'collabratec-private-group-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function (ctx) {
|
||||
await expect(ctx.resultPromise).to.be.rejected
|
||||
expect(ctx.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isLinkedCollabratecUserProject', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.ProjectModel.findOne = sinon.stub().resolves()
|
||||
})
|
||||
|
||||
describe('when find succeeds', function () {
|
||||
describe('when user project found', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.ProjectModel.findOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves('project') })
|
||||
ctx.result =
|
||||
await ctx.ProjectCollabratecDetailsHandler.promises.isLinkedCollabratecUserProject(
|
||||
ctx.projectId,
|
||||
ctx.userId
|
||||
)
|
||||
})
|
||||
|
||||
it('should call find with project and user id', function (ctx) {
|
||||
expect(ctx.ProjectModel.findOne).to.have.been.calledWithMatch({
|
||||
_id: new ObjectId(ctx.projectId),
|
||||
collabratecUsers: {
|
||||
$elemMatch: {
|
||||
user_id: new ObjectId(ctx.userId),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should return true', function (ctx) {
|
||||
expect(ctx.result).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when user project is not found', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.ProjectModel.findOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves(null) })
|
||||
ctx.result =
|
||||
await ctx.ProjectCollabratecDetailsHandler.promises.isLinkedCollabratecUserProject(
|
||||
ctx.projectId,
|
||||
ctx.userId
|
||||
)
|
||||
})
|
||||
|
||||
it('should return false', function (ctx) {
|
||||
expect(ctx.result).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when find has error', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.ProjectModel.findOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects() })
|
||||
})
|
||||
|
||||
it('should be rejected', async function (ctx) {
|
||||
await expect(
|
||||
ctx.ProjectCollabratecDetailsHandler.promises.isLinkedCollabratecUserProject(
|
||||
ctx.projectId,
|
||||
ctx.userId
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid args', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.ProjectModel.findOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
ctx.resultPromise =
|
||||
ctx.ProjectCollabratecDetailsHandler.promises.isLinkedCollabratecUserProject(
|
||||
'bad-project-id',
|
||||
'bad-user-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function (ctx) {
|
||||
await expect(ctx.resultPromise).to.be.rejected
|
||||
expect(ctx.ProjectModel.findOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('linkCollabratecUserProject', function () {
|
||||
describe('when update succeeds', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
await ctx.ProjectCollabratecDetailsHandler.promises.linkCollabratecUserProject(
|
||||
ctx.projectId,
|
||||
ctx.userId,
|
||||
'collabratec-document-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should update project model', function (ctx) {
|
||||
const query = {
|
||||
_id: ctx.projectId,
|
||||
collabratecUsers: {
|
||||
$not: {
|
||||
$elemMatch: {
|
||||
collabratec_document_id: 'collabratec-document-id',
|
||||
user_id: ctx.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const update = {
|
||||
$push: {
|
||||
collabratecUsers: {
|
||||
collabratec_document_id: 'collabratec-document-id',
|
||||
user_id: ctx.userId,
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
query,
|
||||
update
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when update has error', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects() })
|
||||
})
|
||||
|
||||
it('should be rejected', async function (ctx) {
|
||||
await expect(
|
||||
ctx.ProjectCollabratecDetailsHandler.promises.linkCollabratecUserProject(
|
||||
ctx.projectId,
|
||||
ctx.userId,
|
||||
'collabratec-document-id'
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid args', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
ctx.resultPromise =
|
||||
ctx.ProjectCollabratecDetailsHandler.promises.linkCollabratecUserProject(
|
||||
'bad-project-id',
|
||||
'bad-user-id',
|
||||
'collabratec-document-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function (ctx) {
|
||||
await expect(ctx.resultPromise).to.be.rejected
|
||||
expect(ctx.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setCollabratecUsers', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.collabratecUsers = [
|
||||
{
|
||||
user_id: ctx.userId,
|
||||
collabratec_document_id: 'collabratec-document-id-1',
|
||||
collabratec_privategroup_id: 'collabratec-private-group-id-1',
|
||||
},
|
||||
{
|
||||
user_id: ctx.userId2,
|
||||
collabratec_document_id: 'collabratec-document-id-2',
|
||||
collabratec_privategroup_id: 'collabratec-private-group-id-2',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
describe('when update succeeds', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
await ctx.ProjectCollabratecDetailsHandler.promises.setCollabratecUsers(
|
||||
ctx.projectId,
|
||||
ctx.collabratecUsers
|
||||
)
|
||||
})
|
||||
|
||||
it('should update project model', function (ctx) {
|
||||
const update = {
|
||||
$set: {
|
||||
collabratecUsers: ctx.collabratecUsers,
|
||||
},
|
||||
}
|
||||
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: ctx.projectId },
|
||||
update
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when update has error', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects() })
|
||||
})
|
||||
|
||||
it('should be rejected', async function (ctx) {
|
||||
await expect(
|
||||
ctx.ProjectCollabratecDetailsHandler.promises.setCollabratecUsers(
|
||||
ctx.projectId,
|
||||
ctx.collabratecUsers
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid project_id', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
ctx.resultPromise =
|
||||
ctx.ProjectCollabratecDetailsHandler.promises.setCollabratecUsers(
|
||||
'bad-project-id',
|
||||
ctx.collabratecUsers
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function (ctx) {
|
||||
await expect(ctx.resultPromise).to.be.rejected
|
||||
expect(ctx.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid user_id', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.collabratecUsers[1].user_id = 'bad-user-id'
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
ctx.resultPromise =
|
||||
ctx.ProjectCollabratecDetailsHandler.promises.setCollabratecUsers(
|
||||
ctx.projectId,
|
||||
ctx.collabratecUsers
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function (ctx) {
|
||||
await expect(ctx.resultPromise).to.be.rejected
|
||||
expect(ctx.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('unlinkCollabratecUserProject', function () {
|
||||
describe('when update succeeds', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
await ctx.ProjectCollabratecDetailsHandler.promises.unlinkCollabratecUserProject(
|
||||
ctx.projectId,
|
||||
ctx.userId
|
||||
)
|
||||
})
|
||||
|
||||
it('should update project model', function (ctx) {
|
||||
const query = { _id: ctx.projectId }
|
||||
const update = {
|
||||
$pull: {
|
||||
collabratecUsers: {
|
||||
user_id: ctx.userId,
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
query,
|
||||
update
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when update has error', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects() })
|
||||
})
|
||||
|
||||
it('should be rejected', async function (ctx) {
|
||||
await expect(
|
||||
ctx.ProjectCollabratecDetailsHandler.promises.unlinkCollabratecUserProject(
|
||||
ctx.projectId,
|
||||
ctx.userId
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid args', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
ctx.resultPromise =
|
||||
ctx.ProjectCollabratecDetailsHandler.promises.unlinkCollabratecUserProject(
|
||||
'bad-project-id',
|
||||
'bad-user-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function (ctx) {
|
||||
await expect(ctx.resultPromise).to.be.rejected
|
||||
expect(ctx.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,426 +0,0 @@
|
||||
const { ObjectId } = require('mongodb-legacy')
|
||||
const Path = require('path')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const modulePath = Path.join(
|
||||
__dirname,
|
||||
'../../../../app/src/Features/Project/ProjectCollabratecDetailsHandler'
|
||||
)
|
||||
|
||||
describe('ProjectCollabratecDetailsHandler', function () {
|
||||
beforeEach(function () {
|
||||
this.projectId = new ObjectId('5bea8747c7bba6012fcaceb3')
|
||||
this.userId = new ObjectId('5be316a9c7f6aa03802ea8fb')
|
||||
this.userId2 = new ObjectId('5c1794b3f0e89b1d1c577eca')
|
||||
this.ProjectModel = {}
|
||||
this.ProjectCollabratecDetailsHandler = SandboxedModule.require(
|
||||
modulePath,
|
||||
{
|
||||
requires: {
|
||||
'mongodb-legacy': { ObjectId },
|
||||
'../../models/Project': { Project: this.ProjectModel },
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('initializeCollabratecProject', function () {
|
||||
describe('when update succeeds', function () {
|
||||
beforeEach(async function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
await this.ProjectCollabratecDetailsHandler.promises.initializeCollabratecProject(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
'collabratec-document-id',
|
||||
'collabratec-private-group-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should update project model', function () {
|
||||
const update = {
|
||||
$set: {
|
||||
collabratecUsers: [
|
||||
{
|
||||
user_id: this.userId,
|
||||
collabratec_document_id: 'collabratec-document-id',
|
||||
collabratec_privategroup_id: 'collabratec-private-group-id',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: this.projectId },
|
||||
update
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when update has error', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects() })
|
||||
})
|
||||
|
||||
it('should be rejected', async function () {
|
||||
await expect(
|
||||
this.ProjectCollabratecDetailsHandler.promises.initializeCollabratecProject(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
'collabratec-document-id',
|
||||
'collabratec-private-group-id'
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid args', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
this.resultPromise =
|
||||
this.ProjectCollabratecDetailsHandler.promises.initializeCollabratecProject(
|
||||
'bad-project-id',
|
||||
'bad-user-id',
|
||||
'collabratec-document-id',
|
||||
'collabratec-private-group-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function () {
|
||||
await expect(this.resultPromise).to.be.rejected
|
||||
expect(this.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isLinkedCollabratecUserProject', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectModel.findOne = sinon.stub().resolves()
|
||||
})
|
||||
|
||||
describe('when find succeeds', function () {
|
||||
describe('when user project found', function () {
|
||||
beforeEach(async function () {
|
||||
this.ProjectModel.findOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves('project') })
|
||||
this.result =
|
||||
await this.ProjectCollabratecDetailsHandler.promises.isLinkedCollabratecUserProject(
|
||||
this.projectId,
|
||||
this.userId
|
||||
)
|
||||
})
|
||||
|
||||
it('should call find with project and user id', function () {
|
||||
expect(this.ProjectModel.findOne).to.have.been.calledWithMatch({
|
||||
_id: new ObjectId(this.projectId),
|
||||
collabratecUsers: {
|
||||
$elemMatch: {
|
||||
user_id: new ObjectId(this.userId),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should return true', function () {
|
||||
expect(this.result).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when user project is not found', function () {
|
||||
beforeEach(async function () {
|
||||
this.ProjectModel.findOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves(null) })
|
||||
this.result =
|
||||
await this.ProjectCollabratecDetailsHandler.promises.isLinkedCollabratecUserProject(
|
||||
this.projectId,
|
||||
this.userId
|
||||
)
|
||||
})
|
||||
|
||||
it('should return false', function () {
|
||||
expect(this.result).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when find has error', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectModel.findOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects() })
|
||||
})
|
||||
|
||||
it('should be rejected', async function () {
|
||||
await expect(
|
||||
this.ProjectCollabratecDetailsHandler.promises.isLinkedCollabratecUserProject(
|
||||
this.projectId,
|
||||
this.userId
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid args', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectModel.findOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
this.resultPromise =
|
||||
this.ProjectCollabratecDetailsHandler.promises.isLinkedCollabratecUserProject(
|
||||
'bad-project-id',
|
||||
'bad-user-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function () {
|
||||
await expect(this.resultPromise).to.be.rejected
|
||||
expect(this.ProjectModel.findOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('linkCollabratecUserProject', function () {
|
||||
describe('when update succeeds', function () {
|
||||
beforeEach(async function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
await this.ProjectCollabratecDetailsHandler.promises.linkCollabratecUserProject(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
'collabratec-document-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should update project model', function () {
|
||||
const query = {
|
||||
_id: this.projectId,
|
||||
collabratecUsers: {
|
||||
$not: {
|
||||
$elemMatch: {
|
||||
collabratec_document_id: 'collabratec-document-id',
|
||||
user_id: this.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const update = {
|
||||
$push: {
|
||||
collabratecUsers: {
|
||||
collabratec_document_id: 'collabratec-document-id',
|
||||
user_id: this.userId,
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
query,
|
||||
update
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when update has error', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects() })
|
||||
})
|
||||
|
||||
it('should be rejected', async function () {
|
||||
await expect(
|
||||
this.ProjectCollabratecDetailsHandler.promises.linkCollabratecUserProject(
|
||||
this.projectId,
|
||||
this.userId,
|
||||
'collabratec-document-id'
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid args', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
this.resultPromise =
|
||||
this.ProjectCollabratecDetailsHandler.promises.linkCollabratecUserProject(
|
||||
'bad-project-id',
|
||||
'bad-user-id',
|
||||
'collabratec-document-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function () {
|
||||
await expect(this.resultPromise).to.be.rejected
|
||||
expect(this.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setCollabratecUsers', function () {
|
||||
beforeEach(function () {
|
||||
this.collabratecUsers = [
|
||||
{
|
||||
user_id: this.userId,
|
||||
collabratec_document_id: 'collabratec-document-id-1',
|
||||
collabratec_privategroup_id: 'collabratec-private-group-id-1',
|
||||
},
|
||||
{
|
||||
user_id: this.userId2,
|
||||
collabratec_document_id: 'collabratec-document-id-2',
|
||||
collabratec_privategroup_id: 'collabratec-private-group-id-2',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
describe('when update succeeds', function () {
|
||||
beforeEach(async function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
await this.ProjectCollabratecDetailsHandler.promises.setCollabratecUsers(
|
||||
this.projectId,
|
||||
this.collabratecUsers
|
||||
)
|
||||
})
|
||||
|
||||
it('should update project model', function () {
|
||||
const update = {
|
||||
$set: {
|
||||
collabratecUsers: this.collabratecUsers,
|
||||
},
|
||||
}
|
||||
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: this.projectId },
|
||||
update
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when update has error', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects() })
|
||||
})
|
||||
|
||||
it('should be rejected', async function () {
|
||||
await expect(
|
||||
this.ProjectCollabratecDetailsHandler.promises.setCollabratecUsers(
|
||||
this.projectId,
|
||||
this.collabratecUsers
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid project_id', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
this.resultPromise =
|
||||
this.ProjectCollabratecDetailsHandler.promises.setCollabratecUsers(
|
||||
'bad-project-id',
|
||||
this.collabratecUsers
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function () {
|
||||
await expect(this.resultPromise).to.be.rejected
|
||||
expect(this.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid user_id', function () {
|
||||
beforeEach(function () {
|
||||
this.collabratecUsers[1].user_id = 'bad-user-id'
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
this.resultPromise =
|
||||
this.ProjectCollabratecDetailsHandler.promises.setCollabratecUsers(
|
||||
this.projectId,
|
||||
this.collabratecUsers
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function () {
|
||||
await expect(this.resultPromise).to.be.rejected
|
||||
expect(this.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('unlinkCollabratecUserProject', function () {
|
||||
describe('when update succeeds', function () {
|
||||
beforeEach(async function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
await this.ProjectCollabratecDetailsHandler.promises.unlinkCollabratecUserProject(
|
||||
this.projectId,
|
||||
this.userId
|
||||
)
|
||||
})
|
||||
|
||||
it('should update project model', function () {
|
||||
const query = { _id: this.projectId }
|
||||
const update = {
|
||||
$pull: {
|
||||
collabratecUsers: {
|
||||
user_id: this.userId,
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(this.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
query,
|
||||
update
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when update has error', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().rejects() })
|
||||
})
|
||||
|
||||
it('should be rejected', async function () {
|
||||
await expect(
|
||||
this.ProjectCollabratecDetailsHandler.promises.unlinkCollabratecUserProject(
|
||||
this.projectId,
|
||||
this.userId
|
||||
)
|
||||
).to.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('with invalid args', function () {
|
||||
beforeEach(function () {
|
||||
this.ProjectModel.updateOne = sinon
|
||||
.stub()
|
||||
.returns({ exec: sinon.stub().resolves() })
|
||||
this.resultPromise =
|
||||
this.ProjectCollabratecDetailsHandler.promises.unlinkCollabratecUserProject(
|
||||
'bad-project-id',
|
||||
'bad-user-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('should be rejected without updating', async function () {
|
||||
await expect(this.resultPromise).to.be.rejected
|
||||
expect(this.ProjectModel.updateOne).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user