From e3fdcdd6011131564c6f004de4eef8358bd7e3e2 Mon Sep 17 00:00:00 2001 From: Kristina <7614497+khjrtbrg@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:09:28 +0100 Subject: [PATCH] Merge pull request #23131 from overleaf/kh-teardown-link-sharing-split-tests [web] tear down link sharing split tests GitOrigin-RevId: 449e9f368405aea1500035269428e7ae0c37d8fb --- .../Collaborators/CollaboratorsController.mjs | 24 +- .../CollaboratorsInviteController.mjs | 25 +- .../CollaboratorsInviteHandler.mjs | 7 - .../src/Features/Project/ProjectController.js | 61 +- .../TokenAccess/TokenAccessController.mjs | 165 +-- .../web/app/views/project/editor/_meta.pug | 2 - .../web/frontend/extracted-translations.json | 5 - .../components/editor-navigation-toolbar.tsx | 29 +- .../collaborators-limit-upgrade.tsx | 116 +- .../editor-over-limit-modal.tsx | 1 - .../share-project-modal.tsx | 1 - .../view-only-access-modal.tsx | 1 - .../components/require-accept-screen.tsx | 123 +- .../components/token-access-root.tsx | 17 +- .../js/shared/hooks/use-viewer-permissions.ts | 6 +- services/web/frontend/js/utils/meta.ts | 2 - services/web/locales/en.json | 4 - .../test/acceptance/src/TokenAccessTests.mjs | 1167 +++-------------- .../token-access/token-access-page.spec.tsx | 8 +- .../CollaboratorsControllerTests.mjs | 126 +- .../CollaboratorsInviteControllerTests.mjs | 408 ++---- .../CollaboratorsInviteHandlerTests.mjs | 74 +- .../src/Project/ProjectControllerTests.js | 128 +- .../TokenAccessControllerTests.mjs | 570 +++----- 24 files changed, 806 insertions(+), 2264 deletions(-) diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsController.mjs b/services/web/app/src/Features/Collaborators/CollaboratorsController.mjs index 118e5f003a..102e9cca28 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsController.mjs +++ b/services/web/app/src/Features/Collaborators/CollaboratorsController.mjs @@ -13,8 +13,6 @@ import { expressify } from '@overleaf/promise-utils' import { hasAdminAccess } from '../Helpers/AdminAuthorizationHelper.js' import TokenAccessHandler from '../TokenAccess/TokenAccessHandler.js' import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js' -import ProjectGetter from '../Project/ProjectGetter.js' -import SplitTestHandler from '../SplitTests/SplitTestHandler.js' import LimitationsManager from '../Subscription/LimitationsManager.js' import PrivilegeLevels from '../Authorization/PrivilegeLevels.js' @@ -83,24 +81,14 @@ async function setCollaboratorInfo(req, res, next) { const { privilegeLevel } = req.body if (privilegeLevel !== PrivilegeLevels.READ_ONLY) { - const project = await ProjectGetter.promises.getProject(projectId, { - owner_ref: 1, - }) - const linkSharingChanges = - await SplitTestHandler.promises.getAssignmentForUser( - project.owner_ref, - 'link-sharing-warning' - ) const allowed = await LimitationsManager.promises.canAddXEditCollaborators(projectId, 1) - if (linkSharingChanges?.variant === 'active') { - if (!allowed) { - return HttpErrorHandler.forbidden( - req, - res, - 'edit collaborator limit reached' - ) - } + if (!allowed) { + return HttpErrorHandler.forbidden( + req, + res, + 'edit collaborator limit reached' + ) } } await CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs index f0be9e7063..722b1bdd4e 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs +++ b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs @@ -15,7 +15,6 @@ import { expressify } from '@overleaf/promise-utils' import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js' import Errors from '../Errors/Errors.js' import AuthenticationController from '../Authentication/AuthenticationController.js' -import SplitTestHandler from '../SplitTests/SplitTestHandler.js' import PrivilegeLevels from '../Authorization/PrivilegeLevels.js' // This rate limiter allows a different number of requests depending on the @@ -98,28 +97,12 @@ async function inviteToProject(req, res) { logger.debug({ projectId, email, sendingUserId }, 'inviting to project') - const project = await ProjectGetter.promises.getProject(projectId, { - owner_ref: 1, - }) - const linkSharingChanges = - await SplitTestHandler.promises.getAssignmentForUser( - project.owner_ref, - 'link-sharing-warning' - ) - let allowed = false - if (linkSharingChanges?.variant === 'active') { - // if link-sharing-warning is active, can always invite read-only collaborators - if (privileges === PrivilegeLevels.READ_ONLY) { - allowed = true - } else { - allowed = await LimitationsManager.promises.canAddXEditCollaborators( - projectId, - 1 - ) - } + // can always invite read-only collaborators + if (privileges === PrivilegeLevels.READ_ONLY) { + allowed = true } else { - allowed = await LimitationsManager.promises.canAddXCollaborators( + allowed = await LimitationsManager.promises.canAddXEditCollaborators( projectId, 1 ) diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.mjs b/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.mjs index 61ac058788..26c22bf03f 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.mjs +++ b/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.mjs @@ -9,7 +9,6 @@ import UserGetter from '../User/UserGetter.js' import ProjectGetter from '../Project/ProjectGetter.js' import NotificationsBuilder from '../Notifications/NotificationsBuilder.js' import PrivilegeLevels from '../Authorization/PrivilegeLevels.js' -import SplitTestHandler from '../SplitTests/SplitTestHandler.js' import LimitationsManager from '../Subscription/LimitationsManager.js' import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js' import _ from 'lodash' @@ -148,14 +147,8 @@ const CollaboratorsInviteHandler = { const project = await ProjectGetter.promises.getProject(projectId, { owner_ref: 1, }) - const linkSharingEnforcement = - await SplitTestHandler.promises.getAssignmentForUser( - project.owner_ref, - 'link-sharing-enforcement' - ) const pendingEditor = invite.privileges === PrivilegeLevels.READ_AND_WRITE && - linkSharingEnforcement?.variant === 'active' && !(await LimitationsManager.promises.canAcceptEditCollaboratorInvite( project._id )) diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index ad6b19abc8..16d5a88873 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -485,52 +485,32 @@ const _ProjectController = { anonRequestToken ) - const [ - linkSharingChanges, - linkSharingEnforcement, - reviewerRoleAssignment, - ] = await Promise.all([ - SplitTestHandler.promises.getAssignmentForUser( - project.owner_ref, - 'link-sharing-warning' - ), - SplitTestHandler.promises.getAssignmentForUser( - project.owner_ref, - 'link-sharing-enforcement' - ), - SplitTestHandler.promises.getAssignmentForUser( + const reviewerRoleAssignment = + await SplitTestHandler.promises.getAssignmentForUser( project.owner_ref, 'reviewer-role' - ), - ]) + ) - if (linkSharingChanges?.variant === 'active') { - if (linkSharingEnforcement?.variant === 'active') { - await Modules.promises.hooks.fire( - 'enforceCollaboratorLimit', + await Modules.promises.hooks.fire('enforceCollaboratorLimit', projectId) + if (isTokenMember) { + // Check explicitly that the user is in read write token refs, while this could be inferred + // from the privilege level, the privilege level of token members might later be restricted + const isReadWriteTokenMember = + await CollaboratorsGetter.promises.userIsReadWriteTokenMember( + userId, projectId ) - } - if (isTokenMember) { - // Check explicitly that the user is in read write token refs, while this could be inferred - // from the privilege level, the privilege level of token members might later be restricted - const isReadWriteTokenMember = - await CollaboratorsGetter.promises.userIsReadWriteTokenMember( + if (isReadWriteTokenMember) { + // Check for an edge case where a user is both in read write token access refs but also + // an invited read write member. Ensure they are not redirected to the sharing updates page + // We could also delete the token access ref if the user is already a member of the project + const isInvitedReadWriteMember = + await CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject( userId, projectId ) - if (isReadWriteTokenMember) { - // Check for an edge case where a user is both in read write token access refs but also - // an invited read write member. Ensure they are not redirected to the sharing updates page - // We could also delete the token access ref if the user is already a member of the project - const isInvitedReadWriteMember = - await CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject( - userId, - projectId - ) - if (!isInvitedReadWriteMember) { - return res.redirect(`/project/${projectId}/sharing-updates`) - } + if (!isInvitedReadWriteMember) { + return res.redirect(`/project/${projectId}/sharing-updates`) } } } @@ -589,9 +569,6 @@ const _ProjectController = { const exceedAtLimit = planLimit > -1 && namedEditors >= planLimit const projectOpenedSegmentation = { projectId: project._id, - // temporary link sharing segmentation: - linkSharingWarning: linkSharingChanges?.variant, - linkSharingEnforcement: linkSharingEnforcement?.variant, namedEditors, pendingEditors, tokenEditors: project.tokenAccessReadAndWrite_refs?.length || 0, @@ -833,8 +810,6 @@ const _ProjectController = { useOpenTelemetry: Settings.useOpenTelemetryClient, hasTrackChangesFeature: Features.hasFeature('track-changes'), projectTags, - linkSharingWarning: linkSharingChanges?.variant === 'active', - linkSharingEnforcement: linkSharingEnforcement?.variant === 'active', usedLatex: // only use the usedLatex value if the split test is enabled splitTestAssignments['default-visual-for-beginners']?.variant === diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessController.mjs b/services/web/app/src/Features/TokenAccess/TokenAccessController.mjs index fa94b0673e..c6722d3d2b 100644 --- a/services/web/app/src/Features/TokenAccess/TokenAccessController.mjs +++ b/services/web/app/src/Features/TokenAccess/TokenAccessController.mjs @@ -8,7 +8,6 @@ import { expressify } from '@overleaf/promise-utils' import AuthorizationManager from '../Authorization/AuthorizationManager.js' import PrivilegeLevels from '../Authorization/PrivilegeLevels.js' import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js' -import SplitTestHandler from '../SplitTests/SplitTestHandler.js' import CollaboratorsInviteHandler from '../Collaborators/CollaboratorsInviteHandler.mjs' import CollaboratorsHandler from '../Collaborators/CollaboratorsHandler.js' import EditorRealTimeController from '../Editor/EditorRealTimeController.js' @@ -317,108 +316,60 @@ async function grantTokenAccessReadAndWrite(req, res, next) { return next(new Errors.NotFoundError()) } - const linkSharingChanges = - await SplitTestHandler.promises.getAssignmentForUser( - project.owner_ref, - 'link-sharing-warning' - ) - - if (linkSharingChanges?.variant === 'active') { - if (!confirmedByUser) { - return res.json({ - requireAccept: { - linkSharingChanges: true, - projectName: project.name, - }, - }) - } - - const linkSharingEnforcement = - await SplitTestHandler.promises.getAssignmentForUser( - project.owner_ref, - 'link-sharing-enforcement' - ) - const pendingEditor = - linkSharingEnforcement?.variant === 'active' && - !(await LimitationsManager.promises.canAcceptEditCollaboratorInvite( - project._id - )) - await ProjectAuditLogHandler.promises.addEntry( - project._id, - 'accept-via-link-sharing', - userId, - req.ip, - { - privileges: pendingEditor ? 'readOnly' : 'readAndWrite', - ...(pendingEditor && { pendingEditor: true }), - } - ) - AnalyticsManager.recordEventForUserInBackground( - userId, - 'project-joined', - { - mode: pendingEditor ? 'read-only' : 'read-write', - projectId: project._id.toString(), - ...(pendingEditor && { pendingEditor: true }), - } - ) - await CollaboratorsHandler.promises.addUserIdToProject( - project._id, - undefined, - userId, - pendingEditor - ? PrivilegeLevels.READ_ONLY - : PrivilegeLevels.READ_AND_WRITE, - { pendingEditor } - ) - - // remove pending invite and notification - const userEmails = - await UserGetter.promises.getUserConfirmedEmails(userId) - await CollaboratorsInviteHandler.promises.revokeInviteForUser( - project._id, - userEmails - ) - // Should be a noop if the user is already a member, - // and would redirect transparently into the project. - EditorRealTimeController.emitToRoom( - project._id, - 'project:membership:changed', - { members: true, invites: true } - ) - + if (!confirmedByUser) { return res.json({ - redirect: `/project/${project._id}`, - }) - } else { - if (!confirmedByUser) { - return res.json({ - requireAccept: { - projectName: project.name, - }, - }) - } - - if (!project.tokenAccessReadAndWrite_refs.some(id => id.equals(userId))) { - await ProjectAuditLogHandler.promises.addEntry( - project._id, - 'join-via-token', - userId, - req.ip, - { privileges: 'readAndWrite' } - ) - } - - await TokenAccessHandler.promises.addReadAndWriteUserToProject( - userId, - project._id - ) - - return res.json({ - redirect: `/project/${project._id}`, - tokenAccessGranted: tokenType, + requireAccept: { + projectName: project.name, + }, }) } + + const pendingEditor = + !(await LimitationsManager.promises.canAcceptEditCollaboratorInvite( + project._id + )) + await ProjectAuditLogHandler.promises.addEntry( + project._id, + 'accept-via-link-sharing', + userId, + req.ip, + { + privileges: pendingEditor ? 'readOnly' : 'readAndWrite', + ...(pendingEditor && { pendingEditor: true }), + } + ) + AnalyticsManager.recordEventForUserInBackground(userId, 'project-joined', { + mode: pendingEditor ? 'read-only' : 'read-write', + projectId: project._id.toString(), + ...(pendingEditor && { pendingEditor: true }), + }) + await CollaboratorsHandler.promises.addUserIdToProject( + project._id, + undefined, + userId, + pendingEditor + ? PrivilegeLevels.READ_ONLY + : PrivilegeLevels.READ_AND_WRITE, + { pendingEditor } + ) + + // remove pending invite and notification + const userEmails = await UserGetter.promises.getUserConfirmedEmails(userId) + await CollaboratorsInviteHandler.promises.revokeInviteForUser( + project._id, + userEmails + ) + // Should be a noop if the user is already a member, + // and would redirect transparently into the project. + EditorRealTimeController.emitToRoom( + project._id, + 'project:membership:changed', + { members: true, invites: true } + ) + + return res.json({ + redirect: `/project/${project._id}`, + }) } catch (err) { return next( OError.tag( @@ -516,14 +467,6 @@ async function ensureUserCanUseSharingUpdatesConsentPage(req, res, next) { if (!project) { throw new Errors.NotFoundError() } - const linkSharingChanges = - await SplitTestHandler.promises.getAssignmentForUser( - project.owner_ref, - 'link-sharing-warning' - ) - if (linkSharingChanges?.variant !== 'active') { - return AsyncFormHelper.redirect(req, res, `/project/${projectId}`) - } const isReadWriteTokenMember = await CollaboratorsGetter.promises.userIsReadWriteTokenMember( userId, @@ -567,13 +510,7 @@ async function moveReadWriteToCollaborators(req, res, next) { userId, projectId ) - const linkSharingEnforcement = - await SplitTestHandler.promises.getAssignmentForUser( - project.owner_ref, - 'link-sharing-enforcement' - ) const pendingEditor = - linkSharingEnforcement?.variant === 'active' && !(await LimitationsManager.promises.canAcceptEditCollaboratorInvite( project._id )) diff --git a/services/web/app/views/project/editor/_meta.pug b/services/web/app/views/project/editor/_meta.pug index 784bdaecf5..9572c3ea91 100644 --- a/services/web/app/views/project/editor/_meta.pug +++ b/services/web/app/views/project/editor/_meta.pug @@ -35,8 +35,6 @@ meta(name="ol-showTemplatesServerPro", data-type="boolean" content=showTemplates meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChangesFeature) meta(name="ol-inactiveTutorials", data-type="json" content=user.inactiveTutorials) meta(name="ol-projectTags" data-type="json" content=projectTags) -meta(name="ol-linkSharingWarning" data-type="boolean" content=linkSharingWarning) -meta(name="ol-linkSharingEnforcement" data-type="boolean" content=linkSharingEnforcement) meta(name="ol-usedLatex" data-type="string" content=usedLatex) meta(name="ol-ro-mirror-on-client-no-local-storage" data-type="boolean" content=roMirrorOnClientNoLocalStorage) meta(name="ol-isSaas" data-type="boolean" content=isSaas) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 6c72050227..94d4aaae98 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -42,7 +42,6 @@ "accept_selected_changes": "", "accept_terms_and_conditions": "", "accepted_invite": "", - "accepting_invite_as": "", "access_denied": "", "access_edit_your_projects": "", "access_levels_changed": "", @@ -795,7 +794,6 @@ "invite_resend_limit_hit": "", "invited_to_group": "", "invited_to_group_have_individual_subcription": "", - "invited_to_join": "", "inviting": "", "ip_address": "", "is_email_affiliated": "", @@ -1830,7 +1828,6 @@ "upgrade_my_plan": "", "upgrade_now": "", "upgrade_summary": "", - "upgrade_to_add_more_editors": "", "upgrade_to_add_more_editors_and_access_collaboration_features": "", "upgrade_to_get_feature": "", "upgrade_to_track_changes": "", @@ -1964,8 +1961,6 @@ "you_can_manage_your_reference_manager_integrations_from_your_account_settings_page": "", "you_can_now_enable_sso": "", "you_can_now_log_in_sso": "", - "you_can_only_add_n_people_to_edit_a_project": "", - "you_can_only_add_n_people_to_edit_a_project_plural": "", "you_can_request_a_maximum_of_limit_fixes_per_day": "", "you_can_select_or_invite": "", "you_can_select_or_invite_plural": "", diff --git a/services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx b/services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx index 3e4cc19e5f..fa2b8b2523 100644 --- a/services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx +++ b/services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx @@ -3,11 +3,9 @@ import { useOnlineUsersContext } from '@/features/ide-react/context/online-users import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' import * as eventTracking from '@/infrastructure/event-tracking' import EditorNavigationToolbarRoot from '@/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root' -import NewShareProjectModal from '@/features/share-project-modal/components/restricted-link-sharing/share-project-modal' -import ShareProjectModal from '@/features/share-project-modal/components/share-project-modal' +import ShareProjectModal from '@/features/share-project-modal/components/restricted-link-sharing/share-project-modal' import EditorOverLimitModal from '@/features/share-project-modal/components/restricted-link-sharing/editor-over-limit-modal' import ViewOnlyAccessModal from '@/features/share-project-modal/components/restricted-link-sharing/view-only-access-modal' -import getMeta from '@/utils/meta' function EditorNavigationToolbar() { const [showShareModal, setShowShareModal] = useState(false) @@ -23,8 +21,6 @@ function EditorNavigationToolbar() { setShowShareModal(false) }, []) - const showNewShareModal = getMeta('ol-linkSharingWarning') - return ( <> - {showNewShareModal ? ( - <> - - - - - ) : ( - - )} + + + ) } diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/collaborators-limit-upgrade.tsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/collaborators-limit-upgrade.tsx index 4d85c377c9..6808f5a771 100644 --- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/collaborators-limit-upgrade.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/collaborators-limit-upgrade.tsx @@ -1,95 +1,53 @@ import { useTranslation } from 'react-i18next' import Notification from '@/shared/components/notification' import { upgradePlan } from '@/main/account-upgrade' -import { useProjectContext } from '@/shared/context/project-context' import { useUserContext } from '@/shared/context/user-context' import StartFreeTrialButton from '@/shared/components/start-free-trial-button' -import getMeta from '@/utils/meta' import OLButton from '@/features/ui/components/ol/ol-button' export default function CollaboratorsLimitUpgrade() { const { t } = useTranslation() - const { features } = useProjectContext() const user = useUserContext() - const linkSharingEnforcement = getMeta('ol-linkSharingEnforcement') return (
- {linkSharingEnforcement ? ( -
) } diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/editor-over-limit-modal.tsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/editor-over-limit-modal.tsx index ccd973e74f..2764a57d8e 100644 --- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/editor-over-limit-modal.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/editor-over-limit-modal.tsx @@ -16,7 +16,6 @@ const EditorOverLimitModal = () => { setShow(false) } - // split test: link-sharing-warning // show the over-limit warning if user // is editor on a project over // collaborator limit (once every 24 hours) diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal.tsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal.tsx index 3aab5b4950..ca2756fa14 100644 --- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal.tsx @@ -66,7 +66,6 @@ const ShareProjectModal = React.memo(function ShareProjectModal({ const { splitTestVariants } = useSplitTestContext() - // split test: link-sharing-warning // show the new share modal if project owner // is over collaborator limit or has pending editors (once every 24 hours) useEffect(() => { diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-only-access-modal.tsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-only-access-modal.tsx index 5bb1a33040..ab2ea023bb 100644 --- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-only-access-modal.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-only-access-modal.tsx @@ -17,7 +17,6 @@ const ViewOnlyAccessModal = () => { setShow(false) } - // split test: link-sharing-enforcement // show the view-only access modal if // is editor on a project over // collaborator limit (once every week) diff --git a/services/web/frontend/js/features/token-access/components/require-accept-screen.tsx b/services/web/frontend/js/features/token-access/components/require-accept-screen.tsx index 005de55c77..72b0e8a132 100644 --- a/services/web/frontend/js/features/token-access/components/require-accept-screen.tsx +++ b/services/web/frontend/js/features/token-access/components/require-accept-screen.tsx @@ -4,7 +4,6 @@ import getMeta from '@/utils/meta' export type RequireAcceptData = { projectName?: string - linkSharingChanges: boolean } export const RequireAcceptScreen: FC<{ @@ -14,94 +13,50 @@ export const RequireAcceptScreen: FC<{ const { t } = useTranslation() const user = getMeta('ol-user') - if (requireAcceptData.linkSharingChanges) { - return ( -
-
-
-
-
-
-

- {t('youre_joining')} -
- - - {requireAcceptData.projectName || 'This project'} - - - {user && ( - <> -
- {t('as_email', { email: user.email })} - - )} -

-
-
- -
-
-

- {t( - 'your_name_and_email_address_will_be_visible_to_the_project_owner_and_other_editors' - )} -

-
-
- -
-
- -
+ return ( +
+
+
+
+
+
+

+ {t('youre_joining')} +
+ + + {requireAcceptData.projectName || 'This project'} + + + {user && ( + <> +
+ {t('as_email', { email: user.email })} + + )} +

-
-
-
- ) - } - return ( -
-
-
-
-
-
-

- {t('invited_to_join')} -
- {requireAcceptData.projectName || 'This project'} -

+
+
+

+ {t( + 'your_name_and_email_address_will_be_visible_to_the_project_owner_and_other_editors' + )} +

+
- {user && ( -
-
-

- {t('accepting_invite_as')} {user.email} -

-
-
- )} - -
-
- -
+
+
+
diff --git a/services/web/frontend/js/features/token-access/components/token-access-root.tsx b/services/web/frontend/js/features/token-access/components/token-access-root.tsx index 1e8865c90f..b38b3fdab4 100644 --- a/services/web/frontend/js/features/token-access/components/token-access-root.tsx +++ b/services/web/frontend/js/features/token-access/components/token-access-root.tsx @@ -99,13 +99,9 @@ function TokenAccessRoot() { // We don't want the full-size div and back link(?) on // the new page, but we do this so the original page - // doesn't change. When tearing down we can clean up - // the DOM in the main return - if ( - mode === 'requireAccept' && - requireAcceptData && - requireAcceptData.linkSharingChanges - ) { + // doesn't change. + // TODO: clean up the DOM in the main return + if (mode === 'requireAccept' && requireAcceptData) { return ( )} - - {mode === 'requireAccept' && requireAcceptData && ( - - )}
) } diff --git a/services/web/frontend/js/shared/hooks/use-viewer-permissions.ts b/services/web/frontend/js/shared/hooks/use-viewer-permissions.ts index aca2fede22..5f6e7f1f72 100644 --- a/services/web/frontend/js/shared/hooks/use-viewer-permissions.ts +++ b/services/web/frontend/js/shared/hooks/use-viewer-permissions.ts @@ -1,12 +1,8 @@ import { useEditorContext } from '../context/editor-context' -import getMeta from '@/utils/meta' function useViewerPermissions() { const { permissionsLevel } = useEditorContext() - - const hasViewerPermissions = - getMeta('ol-linkSharingWarning') && permissionsLevel === 'readOnly' - return hasViewerPermissions + return permissionsLevel === 'readOnly' } export default useViewerPermissions diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts index 177d596cfd..b8c00d4adc 100644 --- a/services/web/frontend/js/utils/meta.ts +++ b/services/web/frontend/js/utils/meta.ts @@ -136,8 +136,6 @@ export interface Meta { 'ol-learnedWords': string[] 'ol-legacyEditorThemes': string[] 'ol-licenseQuantity': number | undefined - 'ol-linkSharingEnforcement': boolean - 'ol-linkSharingWarning': boolean 'ol-loadingText': string 'ol-managedGroupSubscriptions': ManagedGroupSubscription[] 'ol-managedInstitutions': ManagedInstitution[] diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 41b773d7e4..532fb398d4 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1048,7 +1048,6 @@ "invited_to_group_login_benefits": "As part of this group, you’ll have access to __appName__ premium features such as additional collaborators, greater maximum compile time, and real-time track changes.", "invited_to_group_register": "To accept __inviterName__’s invitation you’ll need to create an account.", "invited_to_group_register_benefits": "__appName__ is a collaborative online LaTeX editor, with thousands of ready-to-use templates and an array of LaTeX learning resources to help you get started.", - "invited_to_join": "You have been invited to join", "inviting": "Inviting", "ip_address": "IP Address", "is_email_affiliated": "Is your email affiliated with an institution? ", @@ -2364,7 +2363,6 @@ "upgrade_my_plan": "Upgrade my plan", "upgrade_now": "Upgrade now", "upgrade_summary": "Upgrade summary", - "upgrade_to_add_more_editors": "Upgrade to add more editors to your project", "upgrade_to_add_more_editors_and_access_collaboration_features": "Upgrade to add more editors and access collaboration features like track changes and full project history.", "upgrade_to_get_feature": "Upgrade to get __feature__, plus:", "upgrade_to_track_changes": "Upgrade to track changes", @@ -2519,8 +2517,6 @@ "you_can_manage_your_reference_manager_integrations_from_your_account_settings_page": "You can manage your reference manager integrations from your <0>account settings page.", "you_can_now_enable_sso": "You can now enable SSO on your Group settings page.", "you_can_now_log_in_sso": "You can now log in through your institution and if eligible you will receive <0>__appName__ Professional features.", - "you_can_only_add_n_people_to_edit_a_project": "You can only add __count__ person to edit a project with you on your current plan. Upgrade to add more.", - "you_can_only_add_n_people_to_edit_a_project_plural": "You can only add __count__ people to edit a project with you on your current plan. Upgrade to add more.", "you_can_opt_in_and_out_of_the_program_at_any_time_on_this_page": "You can <0>opt in and out of the program at any time on this page", "you_can_request_a_maximum_of_limit_fixes_per_day": "You can request a maximum of __limit__ fixes per day. Please try again tomorrow.", "you_can_select_or_invite": "You can select or invite __count__ editor on your current plan, or upgrade to get more.", diff --git a/services/web/test/acceptance/src/TokenAccessTests.mjs b/services/web/test/acceptance/src/TokenAccessTests.mjs index 96d10c6414..41bcc0c3af 100644 --- a/services/web/test/acceptance/src/TokenAccessTests.mjs +++ b/services/web/test/acceptance/src/TokenAccessTests.mjs @@ -5,8 +5,6 @@ import request from './helpers/request.js' import settings from '@overleaf/settings' import { db } from '../../../app/src/infrastructure/mongodb.js' import expectErrorResponse from './helpers/expectErrorResponse.mjs' -import SplitTestHandler from '../../../app/src/Features/SplitTests/SplitTestHandler.js' -import sinon from 'sinon' const tryEditorAccess = (user, projectId, test, callback) => async.series( @@ -115,23 +113,6 @@ const tryReadOnlyTokenAccept = ( ) } -const tryReadAndWriteTokenAccept = ( - user, - token, - testPageLoad, - testFormPost, - callback -) => { - _doTryTokenAccept( - `/${token}`, - user, - token, - testPageLoad, - testFormPost, - callback - ) -} - const _doTryTokenAccept = ( url, user, @@ -831,701 +812,191 @@ describe('TokenAccess', function () { }) }) - describe('read-and-write token', function () { + describe('anonymous read-and-write token, disabled (feature is deprecated)', function () { beforeEach(function (done) { - this.projectName = `token-rw-test${Math.random()}` - this.owner.createProject(this.projectName, (err, projectId) => { - if (err != null) { - return done(err) - } - this.projectId = projectId - this.owner.makeTokenBased(this.projectId, err => { + this.owner.createProject( + `token-anon-rw-test${Math.random()}`, + (err, projectId) => { if (err != null) { return done(err) } - this.owner.getProject(this.projectId, (err, project) => { + this.projectId = projectId + this.owner.makeTokenBased(this.projectId, err => { if (err != null) { return done(err) } - this.tokens = project.tokens - done() + this.owner.getProject(this.projectId, (err, project) => { + if (err != null) { + return done(err) + } + this.tokens = project.tokens + done() + }) }) - }) - }) + } + ) }) - it('should allow the user to access project via read-and-write token url', function (done) { + it('should not allow the user to access read-and-write token', function (done) { async.series( [ - // deny access before the token is used cb => tryEditorAccess( - this.other1, + this.anon, this.projectId, expectErrorResponse.restricted.html, cb ), - // try token cb => tryReadAndWriteTokenAccess( - this.other1, + this.anon, this.tokens.readAndWrite, (response, body) => { expect(response.statusCode).to.equal(200) }, (response, body) => { expect(response.statusCode).to.equal(200) - expect(body.requireAccept.projectName).to.equal( - this.projectName - ) + expect(body).to.deep.equal({ + redirect: '/restricted', + anonWriteAccessDenied: true, + }) }, cb ), - // deny access before the token is accepted + cb => + tryAnonContentAccess( + this.anon, + this.projectId, + this.tokens.readAndWrite, + (response, body) => { + expect(response.statusCode).to.equal(403) + expect(body).to.equal('Forbidden') + }, + cb + ), + cb => + this.anon.login((err, response, body) => { + expect(err).to.not.exist + expect(response.statusCode).to.equal(200) + expect(body.redir).to.equal(`/${this.tokens.readAndWrite}`) + cb() + }), + ], + done + ) + }) + + it('should deny access to access tokens', function (done) { + tryFetchProjectTokens(this.anon, this.projectId, (error, response) => { + expect(error).to.equal(null) + expect(response.statusCode).to.equal(403) + done() + }) + }) + + it('should require login if project does not exist', function (done) { + async.series( + [ + // delete project + cb => { + this.owner.deleteProject(this.projectId, cb) + }, + cb => + tryReadAndWriteTokenAccess( + this.anon, + this.tokens.readAndWrite, + (response, body) => { + expect(response.statusCode).to.equal(200) + }, + (response, body) => { + expect(response.statusCode).to.equal(200) + expect(body).to.deep.equal({ + redirect: '/restricted', + anonWriteAccessDenied: true, + }) + }, + cb + ), + cb => + this.anon.login((err, response, body) => { + expect(err).to.not.exist + expect(response.statusCode).to.equal(200) + expect(body.redir).to.equal(`/${this.tokens.readAndWrite}`) + cb() + }), + ], + done + ) + }) + + it('should save URL hash in redirect', function (done) { + const urlFragment = '#123456' + const tokenWithUrlFragment = `${this.tokens.readAndWrite}${urlFragment}` + + async.series( + [ cb => tryEditorAccess( - this.other1, + this.anon, this.projectId, expectErrorResponse.restricted.html, cb ), - // accept token cb => - tryReadAndWriteTokenAccept( - this.other1, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - (response, body) => { - expect(response.statusCode).to.equal(200) - expect(body.redirect).to.equal(`/project/${this.projectId}`) - expect(body.tokenAccessGranted).to.equal('readAndWrite') - }, - cb - ), - cb => - tryEditorAccess( - this.other1, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - cb - ), - cb => - tryContentAccess( - this.other1, - this.projectId, - (response, body) => { - expect(body.privilegeLevel).to.equal('readAndWrite') - expect(body.isRestrictedUser).to.equal(false) - expect(body.isTokenMember).to.equal(true) - expect(body.isInvitedMember).to.equal(false) - expect(body.project.owner).to.have.all.keys( - '_id', - 'email', - 'first_name', - 'last_name', - 'privileges', - 'signUpDate' - ) - }, - cb - ), - ], - done - ) - }) - - it('fetching access tokens returns an empty object', function (done) { - async.series( - [ - cb => - tryReadAndWriteTokenAccept( - this.other1, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - (response, body) => { - expect(response.statusCode).to.equal(200) - expect(body.redirect).to.equal(`/project/${this.projectId}`) - expect(body.tokenAccessGranted).to.equal('readAndWrite') - }, - cb - ), - cb => { - tryFetchProjectTokens( - this.other1, - this.projectId, - (error, response, body) => { - expect(error).to.equal(null) - expect(response.statusCode).to.equal(200) - expect(body).to.deep.equal({}) - cb() - } - ) - }, - ], - done - ) - }) - - describe('upgrading from a read-only token', function () { - beforeEach(function (done) { - this.owner.createProject( - `token-rw-upgrade-test${Math.random()}`, - (err, projectId) => { - if (err != null) { - return done(err) - } - this.projectId = projectId - this.owner.makeTokenBased(this.projectId, err => { - if (err != null) { - return done(err) - } - this.owner.getProject(this.projectId, (err, project) => { - if (err != null) { - return done(err) + this.anon.request.get( + tokenWithUrlFragment, + (err, response, body) => { + if (err) { + return cb(err) } - this.tokens = project.tokens - done() - }) - }) - } - ) - }) + expect(response.statusCode).to.equal(200) - it('should allow user to access project via read-only, then upgrade to read-write', function (done) { - async.series( - [ - // deny access before the token is used - cb => - tryEditorAccess( - this.other1, - this.projectId, - expectErrorResponse.restricted.html, - cb - ), - cb => { - // use read-only token - tryReadOnlyTokenAccept( - this.other1, - this.tokens.readOnly, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - (response, body) => { - expect(response.statusCode).to.equal(200) - expect(body.redirect).to.equal(`/project/${this.projectId}`) - expect(body.tokenAccessGranted).to.equal('readOnly') - }, - cb - ) - }, - cb => { - tryEditorAccess( - this.other1, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - cb - ) - }, - cb => { - // allow content access read-only - tryContentAccess( - this.other1, - this.projectId, - (response, body) => { - expect(body.privilegeLevel).to.equal('readOnly') - expect(body.isRestrictedUser).to.equal(true) - expect(body.isTokenMember).to.equal(true) - expect(body.isInvitedMember).to.equal(false) - expect(body.project.owner).to.have.keys('_id') - expect(body.project.owner).to.not.have.any.keys( - 'email', - 'first_name', - 'last_name' - ) - }, - cb - ) - }, - // - // Then switch to read-write token - // - cb => - tryReadAndWriteTokenAccept( - this.other1, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - (response, body) => { - expect(response.statusCode).to.equal(200) - expect(body.redirect).to.equal(`/project/${this.projectId}`) - expect(body.tokenAccessGranted).to.equal('readAndWrite') - }, - cb - ), - cb => - tryEditorAccess( - this.other1, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - cb - ), - cb => - tryContentAccess( - this.other1, - this.projectId, - (response, body) => { - expect(body.privilegeLevel).to.equal('readAndWrite') - expect(body.isRestrictedUser).to.equal(false) - expect(body.isTokenMember).to.equal(true) - expect(body.isInvitedMember).to.equal(false) - expect(body.project.owner).to.have.all.keys( - '_id', - 'email', - 'first_name', - 'last_name', - 'privileges', - 'signUpDate' - ) - }, - cb - ), - ], - done - ) - }) - }) - - describe('made private again', function () { - beforeEach(function (done) { - this.owner.makePrivate(this.projectId, () => setTimeout(done, 1000)) - }) - - it('should deny access to project', function (done) { - async.series( - [ - cb => { - tryEditorAccess( - this.other1, - this.projectId, - (response, body) => {}, - cb - ) - }, - cb => { - tryReadAndWriteTokenAccess( - this.other1, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - (response, body) => { - expect(response.statusCode).to.equal(404) - }, - cb - ) - }, - cb => { - tryEditorAccess( - this.other1, - this.projectId, - expectErrorResponse.restricted.html, - cb - ) - }, - cb => { - tryContentAccess( - this.other1, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(403) - expect(body).to.equal('Forbidden') - }, - cb - ) - }, - ], - done - ) - }) - - it('should deny access to access tokens', function (done) { - tryFetchProjectTokens( - this.other1, - this.projectId, - (error, response) => { - expect(error).to.equal(null) - expect(response.statusCode).to.equal(403) - done() - } - ) - }) + this.anon.request.post( + `${this.tokens.readAndWrite}/grant`, + { + json: { + token: this.tokens.readAndWrite, + tokenHashPrefix: urlFragment, + }, + }, + (err, response, body) => { + if (err) { + return cb(err) + } + expect(response.statusCode).to.equal(200) + expect(body).to.deep.equal({ + redirect: '/restricted', + anonWriteAccessDenied: true, + }) + cb() + } + ) + } + ), + cb => + tryAnonContentAccess( + this.anon, + this.projectId, + this.tokens.readAndWrite, + (response, body) => { + expect(response.statusCode).to.equal(403) + expect(body).to.equal('Forbidden') + }, + cb + ), + cb => + this.anon.login((err, response, body) => { + expect(err).to.not.exist + expect(response.statusCode).to.equal(200) + expect(body.redir).to.equal(`/${tokenWithUrlFragment}`) + cb() + }), + ], + done + ) }) }) - if (!settings.allowAnonymousReadAndWriteSharing) { - describe('anonymous read-and-write token, disabled', function () { - beforeEach(function (done) { - this.owner.createProject( - `token-anon-rw-test${Math.random()}`, - (err, projectId) => { - if (err != null) { - return done(err) - } - this.projectId = projectId - this.owner.makeTokenBased(this.projectId, err => { - if (err != null) { - return done(err) - } - this.owner.getProject(this.projectId, (err, project) => { - if (err != null) { - return done(err) - } - this.tokens = project.tokens - done() - }) - }) - } - ) - }) - - it('should not allow the user to access read-and-write token', function (done) { - async.series( - [ - cb => - tryEditorAccess( - this.anon, - this.projectId, - expectErrorResponse.restricted.html, - cb - ), - cb => - tryReadAndWriteTokenAccess( - this.anon, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - (response, body) => { - expect(response.statusCode).to.equal(200) - expect(body).to.deep.equal({ - redirect: '/restricted', - anonWriteAccessDenied: true, - }) - }, - cb - ), - cb => - tryAnonContentAccess( - this.anon, - this.projectId, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(403) - expect(body).to.equal('Forbidden') - }, - cb - ), - cb => - this.anon.login((err, response, body) => { - expect(err).to.not.exist - expect(response.statusCode).to.equal(200) - expect(body.redir).to.equal(`/${this.tokens.readAndWrite}`) - cb() - }), - ], - done - ) - }) - - it('should deny access to access tokens', function (done) { - tryFetchProjectTokens(this.anon, this.projectId, (error, response) => { - expect(error).to.equal(null) - expect(response.statusCode).to.equal(403) - done() - }) - }) - - it('should require login if project does not exist', function (done) { - async.series( - [ - // delete project - cb => { - this.owner.deleteProject(this.projectId, cb) - }, - cb => - tryReadAndWriteTokenAccess( - this.anon, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - (response, body) => { - expect(response.statusCode).to.equal(200) - expect(body).to.deep.equal({ - redirect: '/restricted', - anonWriteAccessDenied: true, - }) - }, - cb - ), - cb => - this.anon.login((err, response, body) => { - expect(err).to.not.exist - expect(response.statusCode).to.equal(200) - expect(body.redir).to.equal(`/${this.tokens.readAndWrite}`) - cb() - }), - ], - done - ) - }) - - it('should save URL hash in redirect', function (done) { - const urlFragment = '#123456' - const tokenWithUrlFragment = `${this.tokens.readAndWrite}${urlFragment}` - - async.series( - [ - cb => - tryEditorAccess( - this.anon, - this.projectId, - expectErrorResponse.restricted.html, - cb - ), - cb => - this.anon.request.get( - tokenWithUrlFragment, - (err, response, body) => { - if (err) { - return cb(err) - } - expect(response.statusCode).to.equal(200) - - this.anon.request.post( - `${this.tokens.readAndWrite}/grant`, - { - json: { - token: this.tokens.readAndWrite, - tokenHashPrefix: urlFragment, - }, - }, - (err, response, body) => { - if (err) { - return cb(err) - } - expect(response.statusCode).to.equal(200) - expect(body).to.deep.equal({ - redirect: '/restricted', - anonWriteAccessDenied: true, - }) - cb() - } - ) - } - ), - cb => - tryAnonContentAccess( - this.anon, - this.projectId, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(403) - expect(body).to.equal('Forbidden') - }, - cb - ), - cb => - this.anon.login((err, response, body) => { - expect(err).to.not.exist - expect(response.statusCode).to.equal(200) - expect(body.redir).to.equal(`/${tokenWithUrlFragment}`) - cb() - }), - ], - done - ) - }) - }) - } else { - describe('anonymous read-and-write token, enabled', function () { - beforeEach(function (done) { - this.owner.createProject( - `token-anon-rw-test${Math.random()}`, - (err, projectId) => { - if (err != null) { - return done(err) - } - this.projectId = projectId - this.owner.makeTokenBased(this.projectId, err => { - if (err != null) { - return done(err) - } - this.owner.getProject(this.projectId, (err, project) => { - if (err != null) { - return done(err) - } - this.tokens = project.tokens - done() - }) - }) - } - ) - }) - - it('should allow the user to access project via read-and-write token url', function (done) { - async.series( - [ - cb => - tryEditorAccess( - this.anon, - this.projectId, - expectErrorResponse.restricted.html, - cb - ), - cb => - tryReadAndWriteTokenAccess( - this.anon, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - (response, body) => { - expect(response.statusCode).to.equal(200) - expect(body.redirect).to.equal(`/project/${this.projectId}`) - expect(body.grantAnonymousAccess).to.equal('readAndWrite') - }, - cb - ), - cb => - tryEditorAccess( - this.anon, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - cb - ), - cb => - tryAnonContentAccess( - this.anon, - this.projectId, - this.tokens.readAndWrite, - (response, body) => { - expect(body.privilegeLevel).to.equal('readAndWrite') - }, - cb - ), - ], - done - ) - }) - - describe('made private again', function () { - beforeEach(function (done) { - this.owner.makePrivate(this.projectId, () => setTimeout(done, 1000)) - }) - - it('should not allow the user to access read-and-write token', function (done) { - async.series( - [ - cb => - tryEditorAccess( - this.anon, - this.projectId, - expectErrorResponse.restricted.html, - cb - ), - cb => - tryReadAndWriteTokenAccess( - this.anon, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - (response, body) => { - expect(response.statusCode).to.equal(404) - }, - cb - ), - cb => - tryEditorAccess( - this.anon, - this.projectId, - expectErrorResponse.restricted.html, - cb - ), - cb => - tryAnonContentAccess( - this.anon, - this.projectId, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(403) - expect(body).to.equal('Forbidden') - }, - cb - ), - ], - done - ) - }) - - it('should deny access to access tokens', function (done) { - tryFetchProjectTokens( - this.anon, - this.projectId, - (error, response) => { - expect(error).to.equal(null) - expect(response.statusCode).to.equal(403) - done() - } - ) - }) - }) - - it('should 404 if project does not exist', function (done) { - async.series( - [ - // delete project - cb => { - this.owner.deleteProject(this.projectId, cb) - }, - cb => - tryReadAndWriteTokenAccess( - this.anon, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - (response, body) => { - expect(response.statusCode).to.equal(200) - expect(body).to.deep.equal({ - v1Import: { - status: 'mustLogin', - }, - }) - }, - cb - ), - ], - done - ) - }) - }) - } - describe('private overleaf project', function () { beforeEach(function (done) { this.owner.createProject('overleaf-import', (err, projectId) => { @@ -1826,19 +1297,7 @@ describe('TokenAccess', function () { }) }) - describe('link sharing changes', function () { - beforeEach(function () { - this.getAssignmentForUser = sinon.stub( - SplitTestHandler.promises, - 'getAssignmentForUser' - ) - this.getAssignmentForUser.resolves({ variant: 'default' }) - }) - - afterEach(function () { - this.getAssignmentForUser.restore() - }) - + describe('sharing updates consent page for read-and-write token deprecation', function () { describe('not a member of the project', function () { beforeEach(function (done) { this.projectName = `token-link-sharing-changes${Math.random()}` @@ -1895,216 +1354,6 @@ describe('TokenAccess', function () { }) }) - describe('read and write token member of project', function () { - beforeEach(function (done) { - this.projectName = `token-link-sharing-changes${Math.random()}` - this.owner.createProject(this.projectName, (err, projectId) => { - if (err != null) { - return done(err) - } - this.projectId = projectId - this.owner.makeTokenBased(this.projectId, err => { - if (err != null) { - return done(err) - } - this.owner.getProject(this.projectId, (err, project) => { - if (err != null) { - return done(err) - } - this.tokens = project.tokens - // must do token accept before split test enabled - // otherwise would be automatically added to named collaborators - tryReadAndWriteTokenAccept( - this.other1, - this.tokens.readAndWrite, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - (response, body) => { - expect(response.statusCode).to.equal(200) - expect(body.redirect).to.equal(`/project/${this.projectId}`) - expect(body.tokenAccessGranted).to.equal('readAndWrite') - }, - done - ) - }) - }) - }) - }) - - describe('link sharing changes test not active', function () { - it('should redirect to project, same permissions as before', function (done) { - async.series( - [ - cb => { - trySharingUpdatesPage( - this.other1, - this.projectId, - expectRedirectToProject, - cb - ) - }, - cb => { - trySharingUpdatesJoin( - this.other1, - this.projectId, - expectRedirectToProject, - cb - ) - }, - cb => { - trySharingUpdatesView( - this.other1, - this.projectId, - expectRedirectToProject, - cb - ) - }, - cb => { - tryContentAccess( - this.other1, - this.projectId, - (response, body) => { - expect(body.privilegeLevel).to.equal('readAndWrite') - expect(body.isRestrictedUser).to.equal(false) - expect(body.isTokenMember).to.equal(true) - expect(body.isInvitedMember).to.equal(false) - expect(body.project.owner).to.have.all.keys( - '_id', - 'email', - 'first_name', - 'last_name', - 'privileges', - 'signUpDate' - ) - }, - cb - ) - }, - cb => { - tryEditorAccess( - this.other1, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - cb - ) - }, - ], - done - ) - }) - }) - - describe('link sharing changes test is active', function () { - beforeEach(function () { - this.getAssignmentForUser.resolves({ variant: 'active' }) - }) - it('should show sharing updates page', function (done) { - trySharingUpdatesPage( - this.other1, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - done - ) - }) - - it('should allow join to named collaborator', function (done) { - async.series( - [ - cb => { - trySharingUpdatesJoin( - this.other1, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(204) - }, - cb - ) - }, - cb => { - tryContentAccess( - this.other1, - this.projectId, - (response, body) => { - expect(body.privilegeLevel).to.equal('readAndWrite') - expect(body.isRestrictedUser).to.equal(false) - expect(body.isTokenMember).to.equal(false) - expect(body.isInvitedMember).to.equal(true) // now collaborator - expect(body.project.owner).to.have.all.keys( - '_id', - 'email', - 'first_name', - 'last_name', - 'privileges', - 'signUpDate' - ) - }, - cb - ) - }, - cb => { - tryEditorAccess( - this.other1, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - cb - ) - }, - ], - done - ) - }) - - it('should allow move to anonymous viewer', function (done) { - async.series( - [ - cb => { - trySharingUpdatesView( - this.other1, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(204) - }, - cb - ) - }, - cb => { - tryContentAccess( - this.other1, - this.projectId, - (response, body) => { - expect(body.privilegeLevel).to.equal('readOnly') - expect(body.isRestrictedUser).to.equal(true) - expect(body.isTokenMember).to.equal(true) - expect(body.isInvitedMember).to.equal(false) - expect(body.project.owner).to.have.keys('_id') - }, - cb - ) - }, - cb => { - tryEditorAccess( - this.other1, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - cb - ) - }, - ], - done - ) - }) - }) - }) - describe('read-only token member of project', function () { beforeEach(function (done) { this.projectName = `token-link-sharing-changes${Math.random()}` @@ -2140,72 +1389,66 @@ describe('TokenAccess', function () { }) }) - describe('link sharing changes test is active', function () { - beforeEach(function () { - this.getAssignmentForUser.resolves({ variant: 'active' }) - }) - - it('should redirect to project, same view permissions as before', function (done) { - async.series( - [ - cb => { - trySharingUpdatesPage( - this.other1, - this.projectId, - expectRedirectToProject, - cb - ) - }, - cb => { - trySharingUpdatesJoin( - this.other1, - this.projectId, - expectRedirectToProject, - cb - ) - }, - cb => { - trySharingUpdatesView( - this.other1, - this.projectId, - expectRedirectToProject, - cb - ) - }, - cb => { - // allow content access read-only - tryContentAccess( - this.other1, - this.projectId, - (response, body) => { - expect(body.privilegeLevel).to.equal('readOnly') - expect(body.isRestrictedUser).to.equal(true) - expect(body.isTokenMember).to.equal(true) - expect(body.isInvitedMember).to.equal(false) - expect(body.project.owner).to.have.keys('_id') - expect(body.project.owner).to.not.have.any.keys( - 'email', - 'first_name', - 'last_name' - ) - }, - cb - ) - }, - cb => { - tryEditorAccess( - this.other1, - this.projectId, - (response, body) => { - expect(response.statusCode).to.equal(200) - }, - cb - ) - }, - ], - done - ) - }) + it('should redirect to project', function (done) { + async.series( + [ + cb => { + trySharingUpdatesPage( + this.other1, + this.projectId, + expectRedirectToProject, + cb + ) + }, + cb => { + trySharingUpdatesJoin( + this.other1, + this.projectId, + expectRedirectToProject, + cb + ) + }, + cb => { + trySharingUpdatesView( + this.other1, + this.projectId, + expectRedirectToProject, + cb + ) + }, + cb => { + // allow content access read-only + tryContentAccess( + this.other1, + this.projectId, + (response, body) => { + expect(body.privilegeLevel).to.equal('readOnly') + expect(body.isRestrictedUser).to.equal(true) + expect(body.isTokenMember).to.equal(true) + expect(body.isInvitedMember).to.equal(false) + expect(body.project.owner).to.have.keys('_id') + expect(body.project.owner).to.not.have.any.keys( + 'email', + 'first_name', + 'last_name' + ) + }, + cb + ) + }, + cb => { + tryEditorAccess( + this.other1, + this.projectId, + (response, body) => { + expect(response.statusCode).to.equal(200) + }, + cb + ) + }, + ], + done + ) }) }) }) diff --git a/services/web/test/frontend/components/token-access/token-access-page.spec.tsx b/services/web/test/frontend/components/token-access/token-access-page.spec.tsx index 96def25aa8..107d71acf4 100644 --- a/services/web/test/frontend/components/token-access/token-access-page.spec.tsx +++ b/services/web/test/frontend/components/token-access/token-access-page.spec.tsx @@ -29,13 +29,11 @@ describe('', function () { expect(interception.request.body.confirmedByUser).to.be.false }) - cy.get('h1').should( + cy.get('.link-sharing-invite-header').should( 'have.text', - ['You have been invited to join', 'Test Project'].join('') + ['You’re joining', 'Test Project', 'as test@example.com'].join('') ) - cy.contains('You are accepting this invite as test@example.com') - cy.intercept( { method: 'post', url, times: 1 }, { @@ -47,7 +45,7 @@ describe('', function () { cy.stub(location, 'replace').as('replaceLocation') - cy.findByRole('button', { name: 'Join Project' }).click() + cy.findByRole('button', { name: 'OK, join project' }).click() cy.wait('@confirmedGrantRequest').then(interception => { expect(interception.request.body.confirmedByUser).to.be.true diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.mjs index 944f9af7b8..e93563ab95 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.mjs @@ -304,89 +304,77 @@ describe('CollaboratorsController', function () { ) }) - describe('when link-sharing-warning test active', function () { + describe('when setting privilege level to readAndWrite', function () { beforeEach(function () { - this.SplitTestHandler.promises.getAssignmentForUser.resolves({ - variant: 'active', + this.req.body = { privilegeLevel: 'readAndWrite' } + }) + + describe('when owner can add new edit collaborators', function () { + beforeEach(function () { + this.LimitationsManager.promises.canAddXEditCollaborators.resolves( + true + ) + }) + + it('should set privilege level after checking collaborators can be added', function (done) { + this.res.sendStatus = status => { + expect(status).to.equal(204) + expect( + this.LimitationsManager.promises.canAddXEditCollaborators + ).to.have.been.calledWith(this.projectId, 1) + done() + } + this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) }) }) - describe('when setting privilege level to readAndWrite', function () { + describe('when owner cannot add edit collaborators', function () { beforeEach(function () { - this.req.body = { privilegeLevel: 'readAndWrite' } + this.LimitationsManager.promises.canAddXEditCollaborators.resolves( + false + ) }) - describe('when owner can add new edit collaborators', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAddXEditCollaborators.resolves( - true - ) - }) - - it('should set privilege level after checking collaborators can be added', function (done) { - this.res.sendStatus = status => { - expect(status).to.equal(204) - expect( - this.LimitationsManager.promises.canAddXEditCollaborators - ).to.have.been.calledWith(this.projectId, 1) - done() - } - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) - }) - }) - - describe('when owner cannot add edit collaborators', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAddXEditCollaborators.resolves( - false - ) - }) - - it('should return a 403 if trying to set a new edit collaborator', function (done) { - this.HttpErrorHandler.forbidden = sinon.spy((req, res) => { - expect(req).to.equal(this.req) - expect(res).to.equal(this.res) - expect( - this.LimitationsManager.promises.canAddXEditCollaborators - ).to.have.been.calledWith(this.projectId, 1) - expect( - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel - ).to.not.have.been.called - done() - }) - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) + it('should return a 403 if trying to set a new edit collaborator', function (done) { + this.HttpErrorHandler.forbidden = sinon.spy((req, res) => { + expect(req).to.equal(this.req) + expect(res).to.equal(this.res) + expect( + this.LimitationsManager.promises.canAddXEditCollaborators + ).to.have.been.calledWith(this.projectId, 1) + expect( + this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel + ).to.not.have.been.called + done() }) + this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) }) }) + }) - describe('when setting privilege level to readOnly', function () { + describe('when setting privilege level to readOnly', function () { + beforeEach(function () { + this.req.body = { privilegeLevel: 'readOnly' } + }) + + describe('when owner cannot add edit collaborators', function () { beforeEach(function () { - this.req.body = { privilegeLevel: 'readOnly' } + this.LimitationsManager.promises.canAddXEditCollaborators.resolves( + false + ) }) - describe('when owner cannot add edit collaborators', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAddXEditCollaborators.resolves( - false - ) - }) - - it('should always allow setting a collaborator to viewer even if user cant add edit collaborators', function (done) { - this.res.sendStatus = status => { - expect(status).to.equal(204) - expect(this.LimitationsManager.promises.canAddXEditCollaborators) - .to.not.have.been.called - expect( - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel - ).to.have.been.calledWith( - this.projectId, - this.user._id, - 'readOnly' - ) - done() - } - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) - }) + it('should always allow setting a collaborator to viewer even if user cant add edit collaborators', function (done) { + this.res.sendStatus = status => { + expect(status).to.equal(204) + expect(this.LimitationsManager.promises.canAddXEditCollaborators).to + .not.have.been.called + expect( + this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel + ).to.have.been.calledWith(this.projectId, this.user._id, 'readOnly') + done() + } + this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) }) }) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.mjs index b806d5c773..1a6b0a5b63 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.mjs @@ -230,228 +230,18 @@ describe('CollaboratorsInviteController', function () { }) }) - describe('when in link-sharing-warning test', function (done) { - beforeEach(function () { - this.SplitTestHandler.promises.getAssignmentForUser.resolves({ - variant: 'active', - }) - }) - - describe('when all goes well', function (done) { - beforeEach(async function () { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - - await this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res - ) - }) - - it('should produce json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ - invite: this.inviteReducedData, - }) - }) - - it('should have called canAddXEditCollaborators', function () { - this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( - 1 - ) - this.LimitationsManager.promises.canAddXEditCollaborators - .calledWith(this.projectId) - .should.equal(true) - }) - - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( - 1 - ) - - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) - .should.equal(true) - }) - - it('should have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( - 1 - ) - this.CollaboratorsInviteHandler.promises.inviteToProject - .calledWith( - this.projectId, - this.currentUser, - this.targetEmail, - this.privileges - ) - .should.equal(true) - }) - - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') - .should.equal(true) - }) - - it('adds a project audit log entry', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, - 'send-invite', - this.currentUser._id, - this.req.ip, - { - inviteId: this.invite._id, - privileges: this.privileges, - } - ) - }) - }) - - describe('when the user is not allowed to add more edit collaborators', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAddXEditCollaborators.resolves( - false - ) - }) - - describe('readAndWrite collaborator', function () { - beforeEach(function (done) { - this.privileges = 'readAndWrite' - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.res.callback = () => done() - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) - }) - - it('should produce json response without an invite', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ - invite: null, - }) - }) - - it('should not have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( - 0 - ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.currentUser, this.targetEmail) - .should.equal(false) - }) - - it('should not have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( - 0 - ) - }) - }) - - describe('readOnly collaborator (always allowed)', function () { - beforeEach(function (done) { - this.req.body = { - email: this.targetEmail, - privileges: (this.privileges = 'readOnly'), - } - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.res.callback = () => done() - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) - }) - - it('should produce json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ - invite: this.inviteReducedData, - }) - }) - - it('should not have called canAddXEditCollaborators', function () { - this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( - 0 - ) - }) - - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( - 1 - ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) - .should.equal(true) - }) - - it('should have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( - 1 - ) - this.CollaboratorsInviteHandler.promises.inviteToProject - .calledWith( - this.projectId, - this.currentUser, - this.targetEmail, - this.privileges - ) - .should.equal(true) - }) - - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') - .should.equal(true) - }) - - it('adds a project audit log entry', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, - 'send-invite', - this.currentUser._id, - this.req.ip, - { - inviteId: this.invite._id, - privileges: this.privileges, - } - ) - }) - }) - }) - }) - describe('when all goes well', function (done) { - beforeEach(function (done) { + beforeEach(async function () { this.CollaboratorsInviteController._checkShouldInviteEmail = sinon .stub() .resolves(true) this.CollaboratorsInviteController._checkRateLimit = sinon .stub() .resolves(true) - this.res.callback = () => done() - this.CollaboratorsInviteController.inviteToProject( + + await this.CollaboratorsInviteController.inviteToProject( this.req, - this.res, - this.next + this.res ) }) @@ -462,11 +252,11 @@ describe('CollaboratorsInviteController', function () { }) }) - it('should have called canAddXCollaborators', function () { - this.LimitationsManager.promises.canAddXCollaborators.callCount.should.equal( + it('should have called canAddXEditCollaborators', function () { + this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( 1 ) - this.LimitationsManager.promises.canAddXCollaborators + this.LimitationsManager.promises.canAddXEditCollaborators .calledWith(this.projectId) .should.equal(true) }) @@ -475,6 +265,7 @@ describe('CollaboratorsInviteController', function () { this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 1 ) + this.CollaboratorsInviteController._checkShouldInviteEmail .calledWith(this.targetEmail) .should.equal(true) @@ -515,81 +306,128 @@ describe('CollaboratorsInviteController', function () { }) }) - describe('when the user is not allowed to add more collaborators', function () { - beforeEach(function (done) { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.LimitationsManager.promises.canAddXCollaborators.resolves(false) - this.res.callback = () => done() - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next + describe('when the user is not allowed to add more edit collaborators', function () { + beforeEach(function () { + this.LimitationsManager.promises.canAddXEditCollaborators.resolves( + false ) }) - it('should produce json response without an invite', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ invite: null }) + describe('readAndWrite collaborator', function () { + beforeEach(function (done) { + this.privileges = 'readAndWrite' + this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .resolves(true) + this.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + this.res.callback = () => done() + this.CollaboratorsInviteController.inviteToProject( + this.req, + this.res, + this.next + ) + }) + + it('should produce json response without an invite', function () { + this.res.json.callCount.should.equal(1) + expect(this.res.json.firstCall.args[0]).to.deep.equal({ + invite: null, + }) + }) + + it('should not have called _checkShouldInviteEmail', function () { + this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + 0 + ) + this.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(this.currentUser, this.targetEmail) + .should.equal(false) + }) + + it('should not have called inviteToProject', function () { + this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + 0 + ) + }) }) - it('should not have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( - 0 - ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.currentUser, this.targetEmail) - .should.equal(false) - }) + describe('readOnly collaborator (always allowed)', function () { + beforeEach(function (done) { + this.req.body = { + email: this.targetEmail, + privileges: (this.privileges = 'readOnly'), + } + this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .resolves(true) + this.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + this.res.callback = () => done() + this.CollaboratorsInviteController.inviteToProject( + this.req, + this.res, + this.next + ) + }) - it('should not have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( - 0 - ) - }) - }) + it('should produce json response', function () { + this.res.json.callCount.should.equal(1) + expect(this.res.json.firstCall.args[0]).to.deep.equal({ + invite: this.inviteReducedData, + }) + }) - describe('when canAddXCollaborators produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.LimitationsManager.promises.canAddXCollaborators.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) - }) + it('should not have called canAddXEditCollaborators', function () { + this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( + 0 + ) + }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) - }) + it('should have called _checkShouldInviteEmail', function () { + this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + 1 + ) + this.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(this.targetEmail) + .should.equal(true) + }) - it('should not have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( - 0 - ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.currentUser, this.targetEmail) - .should.equal(false) - }) + it('should have called inviteToProject', function () { + this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + 1 + ) + this.CollaboratorsInviteHandler.promises.inviteToProject + .calledWith( + this.projectId, + this.currentUser, + this.targetEmail, + this.privileges + ) + .should.equal(true) + }) - it('should not have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( - 0 - ) + it('should have called emitToRoom', function () { + this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + this.EditorRealTimeController.emitToRoom + .calledWith(this.projectId, 'project:membership:changed') + .should.equal(true) + }) + + it('adds a project audit log entry', function () { + this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + this.projectId, + 'send-invite', + this.currentUser._id, + this.req.ip, + { + inviteId: this.invite._id, + privileges: this.privileges, + } + ) + }) }) }) @@ -617,11 +455,11 @@ describe('CollaboratorsInviteController', function () { expect(this.next).to.have.been.calledWith(sinon.match.instanceOf(Error)) }) - it('should have called canAddXCollaborators', function () { - this.LimitationsManager.promises.canAddXCollaborators.callCount.should.equal( + it('should have called canAddXEditCollaborators', function () { + this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( 1 ) - this.LimitationsManager.promises.canAddXCollaborators + this.LimitationsManager.promises.canAddXEditCollaborators .calledWith(this.projectId) .should.equal(true) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.mjs index e52d2a0a17..1a20d28699 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.mjs @@ -492,6 +492,9 @@ describe('CollaboratorsInviteHandler', function () { this.CollaboratorsHandler.promises.addUserIdToProject.resolves() this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification = sinon.stub().resolves() + this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + true + ) this.ProjectInvite.deleteOne.returns({ exec: sinon.stub().resolves() }) this.call = async () => { await this.CollaboratorsInviteHandler.promises.acceptInvite( @@ -503,11 +506,8 @@ describe('CollaboratorsInviteHandler', function () { }) describe('when all goes well', function () { - it('should have called CollaboratorsHandler.addUserIdToProject', async function () { + it('should add readAndWrite invitees to the project as normal', async function () { await this.call() - this.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( - 1 - ) this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( this.projectId, this.sendingUserId, @@ -546,55 +546,29 @@ describe('CollaboratorsInviteHandler', function () { }) }) - describe('when link-sharing-enforcement is active', function () { + describe('when the project has no more edit collaborator slots', function () { beforeEach(function () { - this.SplitTestHandler.promises.getAssignmentForUser.resolves({ - variant: 'active', - }) + this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + false + ) }) - describe('when the project has no more edit collaborator slots', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( - false - ) - }) - - it('should add readAndWrite invitees to the project as readOnly (pendingEditor) users', async function () { - await this.call() - this.ProjectAuditLogHandler.promises.addEntry.should.have.been.calledWith( - this.projectId, - 'editor-moved-to-pending', - null, - null, - { userId: this.userId.toString() } - ) - this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( - this.projectId, - this.sendingUserId, - this.userId, - 'readOnly', - { pendingEditor: true } - ) - }) - }) - - describe('when the project has available edit collaborator slots', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( - true - ) - }) - - it('should add readAndWrite invitees to the project as normal', async function () { - await this.call() - this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( - this.projectId, - this.sendingUserId, - this.userId, - this.fakeInvite.privileges - ) - }) + it('should add readAndWrite invitees to the project as readOnly (pendingEditor) users', async function () { + await this.call() + this.ProjectAuditLogHandler.promises.addEntry.should.have.been.calledWith( + this.projectId, + 'editor-moved-to-pending', + null, + null, + { userId: this.userId.toString() } + ) + this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( + this.projectId, + this.sendingUserId, + this.userId, + 'readOnly', + { pendingEditor: true } + ) }) }) diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js index 9591947eac..ef85b4c0ed 100644 --- a/services/web/test/unit/src/Project/ProjectControllerTests.js +++ b/services/web/test/unit/src/Project/ProjectControllerTests.js @@ -1031,108 +1031,56 @@ describe('ProjectController', function () { }) }) - describe('link sharing changes active', function () { + describe('when user is a read write token member (and not already a named editor)', function () { beforeEach(function () { - this.SplitTestHandler.promises.getAssignmentForUser.callsFake( - async (userId, test) => { - if (test === 'link-sharing-warning') { - return { variant: 'active' } - } - } + this.CollaboratorsGetter.promises.userIsTokenMember.resolves(true) + this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( + true + ) + this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( + false ) }) - describe('when user is a read write token member (and not already a named editor)', function () { - beforeEach(function () { - this.CollaboratorsGetter.promises.userIsTokenMember.resolves(true) - this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( - true - ) - this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( - false - ) - }) - - it('should redirect to the sharing-updates page', function (done) { - this.res.redirect = url => { - expect(url).to.equal(`/project/${this.project_id}/sharing-updates`) - done() - } - this.ProjectController.loadEditor(this.req, this.res) - }) - }) - - describe('when user is a read write token member but also a named editor', function () { - beforeEach(function () { - this.CollaboratorsGetter.promises.userIsTokenMember.resolves(true) - this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( - true - ) - this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( - true - ) - }) - - it('should not redirect to the sharing-updates page, and should load the editor', function (done) { - this.res.render = (pageName, opts) => { - done() - } - this.ProjectController.loadEditor(this.req, this.res) - }) + it('should redirect to the sharing-updates page', function (done) { + this.res.redirect = url => { + expect(url).to.equal(`/project/${this.project_id}/sharing-updates`) + done() + } + this.ProjectController.loadEditor(this.req, this.res) }) }) - describe('link sharing enforcement', function () { - describe('when not active (default)', function () { - beforeEach(function () { - this.SplitTestHandler.promises.getAssignmentForUser.callsFake( - async (userId, test) => { - if (test === 'link-sharing-warning') { - return { variant: 'active' } - } else if (test === 'link-sharing-enforcement') { - return { variant: 'default' } - } - } - ) - }) - - it('should not call the collaborator limit enforcement check', function (done) { - this.res.render = (pageName, opts) => { - this.Modules.promises.hooks.fire.should.not.have.been.calledWith( - 'enforceCollaboratorLimit' - ) - done() - } - this.ProjectController.loadEditor(this.req, this.res) - }) + describe('when user is a read write token member but also a named editor', function () { + beforeEach(function () { + this.CollaboratorsGetter.promises.userIsTokenMember.resolves(true) + this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( + true + ) + this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( + true + ) }) - describe('when active', function () { - beforeEach(function () { - this.SplitTestHandler.promises.getAssignmentForUser.callsFake( - async (userId, test) => { - if (test === 'link-sharing-warning') { - return { variant: 'active' } - } else if (test === 'link-sharing-enforcement') { - return { variant: 'active' } - } - } - ) - }) - - it('should call the collaborator limit enforcement check', function (done) { - this.res.render = (pageName, opts) => { - this.Modules.promises.hooks.fire.should.have.been.calledWith( - 'enforceCollaboratorLimit', - this.project_id - ) - done() - } - this.ProjectController.loadEditor(this.req, this.res) - }) + it('should not redirect to the sharing-updates page, and should load the editor', function (done) { + this.res.render = (pageName, opts) => { + done() + } + this.ProjectController.loadEditor(this.req, this.res) }) }) + it('should call the collaborator limit enforcement check', function (done) { + this.res.render = (pageName, opts) => { + this.Modules.promises.hooks.fire.should.have.been.calledWith( + 'enforceCollaboratorLimit', + this.project_id + ) + done() + } + this.ProjectController.loadEditor(this.req, this.res) + }) + describe('chatEnabled flag', function () { it('should be set to false when the feature is disabled', function (done) { this.Features.hasFeature = sinon.stub().withArgs('chat').returns(false) diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.mjs b/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.mjs index 8cc6726df4..f8b43a2446 100644 --- a/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.mjs +++ b/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.mjs @@ -192,10 +192,22 @@ describe('TokenAccessController', function () { }) describe('grantTokenAccessReadAndWrite', function () { - describe('normal case', function () { + beforeEach(function () { + this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + true + ) + }) + + describe('normal case (edit slot available)', function () { beforeEach(function (done) { + this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + true + ) this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' } + this.req.body = { + confirmedByUser: true, + tokenHashPrefix: '#prefix', + } this.res.callback = done this.TokenAccessController.grantTokenAccessReadAndWrite( this.req, @@ -204,10 +216,15 @@ describe('TokenAccessController', function () { ) }) - it('grants read and write access', function () { + it('adds the user as a read and write invited member', function () { expect( - this.TokenAccessHandler.promises.addReadAndWriteUserToProject - ).to.have.been.calledWith(this.user._id, this.project._id) + this.CollaboratorsHandler.promises.addUserIdToProject + ).to.have.been.calledWith( + this.project._id, + undefined, + this.user._id, + PrivilegeLevels.READ_AND_WRITE + ) }) it('writes a project audit log', function () { @@ -215,13 +232,32 @@ describe('TokenAccessController', function () { this.ProjectAuditLogHandler.promises.addEntry ).to.have.been.calledWith( this.project._id, - 'join-via-token', + 'accept-via-link-sharing', this.user._id, this.req.ip, { privileges: 'readAndWrite' } ) }) + it('records a project-joined event for the user', function () { + expect( + this.AnalyticsManager.recordEventForUserInBackground + ).to.have.been.calledWith(this.user._id, 'project-joined', { + mode: 'read-write', + projectId: this.project._id.toString(), + }) + }) + + it('emits a project membership changed event', function () { + expect( + this.EditorRealTimeController.emitToRoom + ).to.have.been.calledWith( + this.project._id, + 'project:membership:changed', + { members: true, invites: true } + ) + }) + it('checks token hash', function () { expect( this.TokenAccessHandler.checkTokenHashPrefix @@ -235,262 +271,78 @@ describe('TokenAccessController', function () { }) }) - describe('when project owner in link-sharing-warning split test', function () { - beforeEach(function () { - this.SplitTestHandler.promises.getAssignmentForUser.callsFake( - async (userId, test) => { - if (test === 'link-sharing-warning') { - return { variant: 'active' } - } - } + describe('when there are no edit collaborator slots available', function () { + beforeEach(function (done) { + this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + false ) - }) - - it('tells the ui to show the link-sharing-warning variant', async function () { this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - await this.TokenAccessController.grantTokenAccessReadAndWrite( + this.req.body = { + confirmedByUser: true, + tokenHashPrefix: '#prefix', + } + this.res.callback = done + this.TokenAccessController.grantTokenAccessReadAndWrite( this.req, - { - json: content => { - expect(content).to.deep.equal({ - requireAccept: { - linkSharingChanges: true, - projectName: this.project.name, - }, - }) - }, - } + this.res, + done ) }) - describe('normal case', function () { - beforeEach(function (done) { - this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) - }) + it('adds the user as a read only invited member instead (pendingEditor)', function () { + expect( + this.CollaboratorsHandler.promises.addUserIdToProject + ).to.have.been.calledWith( + this.project._id, + undefined, + this.user._id, + PrivilegeLevels.READ_ONLY, + { pendingEditor: true } + ) + }) - it('adds the user as a read and write invited member', function () { - expect( - this.CollaboratorsHandler.promises.addUserIdToProject - ).to.have.been.calledWith( - this.project._id, - undefined, - this.user._id, - PrivilegeLevels.READ_AND_WRITE - ) - }) + it('writes a project audit log', function () { + expect( + this.ProjectAuditLogHandler.promises.addEntry + ).to.have.been.calledWith( + this.project._id, + 'accept-via-link-sharing', + this.user._id, + this.req.ip, + { privileges: 'readOnly', pendingEditor: true } + ) + }) - it('writes a project audit log', function () { - expect( - this.ProjectAuditLogHandler.promises.addEntry - ).to.have.been.calledWith( - this.project._id, - 'accept-via-link-sharing', - this.user._id, - this.req.ip, - { privileges: 'readAndWrite' } - ) - }) - - it('records a project-joined event for the user', function () { - expect( - this.AnalyticsManager.recordEventForUserInBackground - ).to.have.been.calledWith(this.user._id, 'project-joined', { - mode: 'read-write', - projectId: this.project._id.toString(), - }) - }) - - it('emits a project membership changed event', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( - this.project._id, - 'project:membership:changed', - { members: true, invites: true } - ) - }) - - it('checks token hash', function () { - expect( - this.TokenAccessHandler.checkTokenHashPrefix - ).to.have.been.calledWith( - this.token, - '#prefix', - 'readAndWrite', - this.user._id, - { projectId: this.project._id, action: 'continue' } - ) + it('records a project-joined event for the user', function () { + expect( + this.AnalyticsManager.recordEventForUserInBackground + ).to.have.been.calledWith(this.user._id, 'project-joined', { + mode: 'read-only', + projectId: this.project._id.toString(), + pendingEditor: true, }) }) - describe('when the project owner is in the link-sharing-enforcement split test', function () { - beforeEach(function () { - this.SplitTestHandler.promises.getAssignmentForUser.callsFake( - async (userId, test) => { - if (test === 'link-sharing-warning') { - return { variant: 'active' } - } else if (test === 'link-sharing-enforcement') { - return { variant: 'active' } - } - } - ) - }) + it('emits a project membership changed event', function () { + expect( + this.EditorRealTimeController.emitToRoom + ).to.have.been.calledWith( + this.project._id, + 'project:membership:changed', + { members: true, invites: true } + ) + }) - describe('normal case (edit slot available)', function () { - beforeEach(function (done) { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( - true - ) - this.req.params = { token: this.token } - this.req.body = { - confirmedByUser: true, - tokenHashPrefix: '#prefix', - } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) - }) - - it('adds the user as a read and write invited member', function () { - expect( - this.CollaboratorsHandler.promises.addUserIdToProject - ).to.have.been.calledWith( - this.project._id, - undefined, - this.user._id, - PrivilegeLevels.READ_AND_WRITE - ) - }) - - it('writes a project audit log', function () { - expect( - this.ProjectAuditLogHandler.promises.addEntry - ).to.have.been.calledWith( - this.project._id, - 'accept-via-link-sharing', - this.user._id, - this.req.ip, - { privileges: 'readAndWrite' } - ) - }) - - it('records a project-joined event for the user', function () { - expect( - this.AnalyticsManager.recordEventForUserInBackground - ).to.have.been.calledWith(this.user._id, 'project-joined', { - mode: 'read-write', - projectId: this.project._id.toString(), - }) - }) - - it('emits a project membership changed event', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( - this.project._id, - 'project:membership:changed', - { members: true, invites: true } - ) - }) - - it('checks token hash', function () { - expect( - this.TokenAccessHandler.checkTokenHashPrefix - ).to.have.been.calledWith( - this.token, - '#prefix', - 'readAndWrite', - this.user._id, - { projectId: this.project._id, action: 'continue' } - ) - }) - }) - - describe('when there are no edit collaborator slots available', function () { - beforeEach(function (done) { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( - false - ) - this.req.params = { token: this.token } - this.req.body = { - confirmedByUser: true, - tokenHashPrefix: '#prefix', - } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) - }) - - it('adds the user as a read only invited member instead (pendingEditor)', function () { - expect( - this.CollaboratorsHandler.promises.addUserIdToProject - ).to.have.been.calledWith( - this.project._id, - undefined, - this.user._id, - PrivilegeLevels.READ_ONLY, - { pendingEditor: true } - ) - }) - - it('writes a project audit log', function () { - expect( - this.ProjectAuditLogHandler.promises.addEntry - ).to.have.been.calledWith( - this.project._id, - 'accept-via-link-sharing', - this.user._id, - this.req.ip, - { privileges: 'readOnly', pendingEditor: true } - ) - }) - - it('records a project-joined event for the user', function () { - expect( - this.AnalyticsManager.recordEventForUserInBackground - ).to.have.been.calledWith(this.user._id, 'project-joined', { - mode: 'read-only', - projectId: this.project._id.toString(), - pendingEditor: true, - }) - }) - - it('emits a project membership changed event', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( - this.project._id, - 'project:membership:changed', - { members: true, invites: true } - ) - }) - - it('checks token hash', function () { - expect( - this.TokenAccessHandler.checkTokenHashPrefix - ).to.have.been.calledWith( - this.token, - '#prefix', - 'readAndWrite', - this.user._id, - { projectId: this.project._id, action: 'continue' } - ) - }) - }) + it('checks token hash', function () { + expect( + this.TokenAccessHandler.checkTokenHashPrefix + ).to.have.been.calledWith( + this.token, + '#prefix', + 'readAndWrite', + this.user._id, + { projectId: this.project._id, action: 'continue' } + ) }) }) @@ -507,9 +359,16 @@ describe('TokenAccessController', function () { ) }) - it("doesn't write a project audit log", function () { - expect(this.ProjectAuditLogHandler.promises.addEntry).to.not.have.been - .called + it('writes a project audit log', function () { + expect( + this.ProjectAuditLogHandler.promises.addEntry + ).to.have.been.calledWith( + this.project._id, + 'accept-via-link-sharing', + this.user._id, + this.req.ip, + { privileges: 'readAndWrite' } + ) }) it('checks token hash', function () { @@ -537,10 +396,15 @@ describe('TokenAccessController', function () { ) }) - it('grants read and write access', function () { + it('adds the user as a read and write invited member', function () { expect( - this.TokenAccessHandler.promises.addReadAndWriteUserToProject - ).to.have.been.calledWith(this.user._id, this.project._id) + this.CollaboratorsHandler.promises.addUserIdToProject + ).to.have.been.calledWith( + this.project._id, + undefined, + this.user._id, + PrivilegeLevels.READ_AND_WRITE + ) }) it('checks the hash prefix', function () { @@ -820,8 +684,13 @@ describe('TokenAccessController', function () { .resolves(projectFromInternalStaff) this.res.callback = () => { expect( - this.TokenAccessHandler.promises.addReadAndWriteUserToProject - ).to.have.been.calledWith(admin._id, projectFromInternalStaff._id) + this.CollaboratorsHandler.promises.addUserIdToProject + ).to.have.been.calledWith( + projectFromInternalStaff._id, + undefined, + admin._id, + PrivilegeLevels.READ_AND_WRITE + ) } this.TokenAccessController.grantTokenAccessReadAndWrite( this.req, @@ -1151,138 +1020,77 @@ describe('TokenAccessController', function () { this.req.params = { Project_id: this.project._id } }) - describe('read only invited viewer gaining edit access via link sharing', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + describe('when there are collaborator slots available', function () { + beforeEach(function () { + this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( true ) - this.res.callback = done - this.TokenAccessController.moveReadWriteToCollaborators( - this.req, - this.res, - done - ) }) - it('sets the privilege level to read and write for the invited viewer', function () { - expect( - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel - ).to.have.been.calledWith( - this.project._id, - this.user._id, - PrivilegeLevels.READ_AND_WRITE - ) - expect(this.res.sendStatus).to.have.been.calledWith(204) - }) - }) - describe('previously joined token access user moving to named collaborator', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( - false - ) - this.res.callback = done - this.TokenAccessController.moveReadWriteToCollaborators( - this.req, - this.res, - done - ) - }) - - it('sets the privilege level to read and write for the invited viewer', function () { - expect( - this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject - ).to.have.been.calledWith(this.user._id, this.project._id) - expect( - this.CollaboratorsHandler.promises.addUserIdToProject - ).to.have.been.calledWith( - this.project._id, - undefined, - this.user._id, - PrivilegeLevels.READ_AND_WRITE - ) - expect(this.res.sendStatus).to.have.been.calledWith(204) - }) - }) - - describe('when link-sharing-enforcement test is active', function () { - beforeEach(function () { - this.SplitTestHandler.promises.getAssignmentForUser.resolves({ - variant: 'active', - }) - }) - - describe('when there are collaborator slots available', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( - true - ) - }) - - describe('previously joined token access user moving to named collaborator', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( - false - ) - this.res.callback = done - this.TokenAccessController.moveReadWriteToCollaborators( - this.req, - this.res, - done - ) - }) - - it('sets the privilege level to read and write for the invited viewer', function () { - expect( - this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject - ).to.have.been.calledWith(this.user._id, this.project._id) - expect( - this.CollaboratorsHandler.promises.addUserIdToProject - ).to.have.been.calledWith( - this.project._id, - undefined, - this.user._id, - PrivilegeLevels.READ_AND_WRITE - ) - expect(this.res.sendStatus).to.have.been.calledWith(204) - }) - }) - }) - - describe('when there are no edit collaborator slots available', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + describe('previously joined token access user moving to named collaborator', function () { + beforeEach(function (done) { + this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( false ) + this.res.callback = done + this.TokenAccessController.moveReadWriteToCollaborators( + this.req, + this.res, + done + ) }) - describe('previously joined token access user moving to named collaborator', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( - false - ) - this.res.callback = done - this.TokenAccessController.moveReadWriteToCollaborators( - this.req, - this.res, - done - ) - }) + it('sets the privilege level to read and write for the invited viewer', function () { + expect( + this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject + ).to.have.been.calledWith(this.user._id, this.project._id) + expect( + this.CollaboratorsHandler.promises.addUserIdToProject + ).to.have.been.calledWith( + this.project._id, + undefined, + this.user._id, + PrivilegeLevels.READ_AND_WRITE + ) + expect(this.res.sendStatus).to.have.been.calledWith(204) + }) + }) + }) - it('sets the privilege level to read only for the invited viewer (pendingEditor)', function () { - expect( - this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject - ).to.have.been.calledWith(this.user._id, this.project._id) - expect( - this.CollaboratorsHandler.promises.addUserIdToProject - ).to.have.been.calledWith( - this.project._id, - undefined, - this.user._id, - PrivilegeLevels.READ_ONLY, - { pendingEditor: true } - ) - expect(this.res.sendStatus).to.have.been.calledWith(204) - }) + describe('when there are no edit collaborator slots available', function () { + beforeEach(function () { + this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + false + ) + }) + + describe('previously joined token access user moving to named collaborator', function () { + beforeEach(function (done) { + this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + false + ) + this.res.callback = done + this.TokenAccessController.moveReadWriteToCollaborators( + this.req, + this.res, + done + ) + }) + + it('sets the privilege level to read only for the invited viewer (pendingEditor)', function () { + expect( + this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject + ).to.have.been.calledWith(this.user._id, this.project._id) + expect( + this.CollaboratorsHandler.promises.addUserIdToProject + ).to.have.been.calledWith( + this.project._id, + undefined, + this.user._id, + PrivilegeLevels.READ_ONLY, + { pendingEditor: true } + ) + expect(this.res.sendStatus).to.have.been.calledWith(204) }) }) })