Explorar o código

Merge pull request #23131 from overleaf/kh-teardown-link-sharing-split-tests

[web] tear down link sharing split tests

GitOrigin-RevId: 449e9f368405aea1500035269428e7ae0c37d8fb
Kristina hai 1 ano
pai
achega
e3fdcdd601
Modificáronse 24 ficheiros con 808 adicións e 2038 borrados
  1. 6 18
      services/web/app/src/Features/Collaborators/CollaboratorsController.mjs
  2. 4 21
      services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs
  3. 0 7
      services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.mjs
  4. 18 43
      services/web/app/src/Features/Project/ProjectController.js
  5. 48 111
      services/web/app/src/Features/TokenAccess/TokenAccessController.mjs
  6. 0 2
      services/web/app/views/project/editor/_meta.pug
  7. 0 5
      services/web/frontend/extracted-translations.json
  8. 8 21
      services/web/frontend/js/features/ide-react/components/editor-navigation-toolbar.tsx
  9. 37 79
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/collaborators-limit-upgrade.tsx
  10. 0 1
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/editor-over-limit-modal.tsx
  11. 0 1
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal.tsx
  12. 0 1
      services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/view-only-access-modal.tsx
  13. 39 84
      services/web/frontend/js/features/token-access/components/require-accept-screen.tsx
  14. 3 14
      services/web/frontend/js/features/token-access/components/token-access-root.tsx
  15. 1 5
      services/web/frontend/js/shared/hooks/use-viewer-permissions.ts
  16. 0 2
      services/web/frontend/js/utils/meta.ts
  17. 0 4
      services/web/locales/en.json
  18. 218 747
      services/web/test/acceptance/src/TokenAccessTests.mjs
  19. 3 5
      services/web/test/frontend/components/token-access/token-access-page.spec.tsx
  20. 55 67
      services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.mjs
  21. 123 285
      services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.mjs
  22. 24 50
      services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.mjs
  23. 38 90
      services/web/test/unit/src/Project/ProjectControllerTests.js
  24. 183 375
      services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.mjs

+ 6 - 18
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(

+ 4 - 21
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
     )

+ 0 - 7
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
       ))

+ 18 - 43
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 ===

+ 48 - 111
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}`,
+        requireAccept: {
+          projectName: project.name,
+        },
       })
-    } 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' }
-        )
+    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 }
+    )
 
-      await TokenAccessHandler.promises.addReadAndWriteUserToProject(
-        userId,
-        project._id
-      )
+    // 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}`,
-        tokenAccessGranted: tokenType,
-      })
-    }
+    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
     ))

+ 0 - 2
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)

+ 0 - 5
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": "",

+ 8 - 21
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 (
     <>
       <EditorNavigationToolbarRoot
@@ -32,22 +28,13 @@ function EditorNavigationToolbar() {
         openDoc={openDoc}
         openShareProjectModal={handleOpenShareModal}
       />
-      {showNewShareModal ? (
-        <>
-          <EditorOverLimitModal />
-          <ViewOnlyAccessModal />
-          <NewShareProjectModal
-            show={showShareModal}
-            handleOpen={handleOpenShareModal}
-            handleHide={handleHideShareModal}
-          />
-        </>
-      ) : (
-        <ShareProjectModal
-          show={showShareModal}
-          handleHide={handleHideShareModal}
-        />
-      )}
+      <EditorOverLimitModal />
+      <ViewOnlyAccessModal />
+      <ShareProjectModal
+        show={showShareModal}
+        handleOpen={handleOpenShareModal}
+        handleHide={handleHideShareModal}
+      />
     </>
   )
 }

+ 37 - 79
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 (
     <div className="invite-warning">
-      {linkSharingEnforcement ? (
-        <Notification
-          type="info"
-          customIcon={
-            <img
-              src="/img/share-modal/add-more-editors.svg"
-              alt=""
-              aria-hidden="true"
-            />
-          }
-          title={t('add_more_editors')}
-          content={
-            <p>
-              {t(
-                'upgrade_to_add_more_editors_and_access_collaboration_features'
-              )}
-            </p>
-          }
-          isActionBelowContent
-          action={
-            user.allowedFreeTrial ? (
-              <StartFreeTrialButton
-                buttonProps={{ variant: 'premium' }}
-                source="project-sharing"
-                variant="limit"
-              >
-                {t('upgrade')}
-              </StartFreeTrialButton>
-            ) : (
-              <OLButton
-                variant="premium"
-                onClick={() => {
-                  upgradePlan('project-sharing')
-                }}
-              >
-                {t('upgrade')}
-              </OLButton>
-            )
-          }
-        />
-      ) : (
-        <Notification
-          type="info"
-          customIcon={<div />}
-          title={t('upgrade_to_add_more_editors')}
-          content={
-            <p>
-              {t('you_can_only_add_n_people_to_edit_a_project', {
-                count: features.collaborators,
-              })}
-            </p>
-          }
-          action={
-            user.allowedFreeTrial ? (
-              <StartFreeTrialButton
-                buttonProps={{ variant: 'secondary', size: 'sm' }}
-                source="project-sharing"
-                variant="limit"
-              >
-                {t('upgrade')}
-              </StartFreeTrialButton>
-            ) : (
-              <OLButton
-                size="sm"
-                variant="secondary"
-                onClick={() => {
-                  upgradePlan('project-sharing')
-                }}
-              >
-                {t('upgrade')}
-              </OLButton>
-            )
-          }
-        />
-      )}
+      <Notification
+        type="info"
+        customIcon={
+          <img
+            src="/img/share-modal/add-more-editors.svg"
+            alt=""
+            aria-hidden="true"
+          />
+        }
+        title={t('add_more_editors')}
+        content={
+          <p>
+            {t('upgrade_to_add_more_editors_and_access_collaboration_features')}
+          </p>
+        }
+        isActionBelowContent
+        action={
+          user.allowedFreeTrial ? (
+            <StartFreeTrialButton
+              buttonProps={{ variant: 'premium' }}
+              source="project-sharing"
+              variant="limit"
+            >
+              {t('upgrade')}
+            </StartFreeTrialButton>
+          ) : (
+            <OLButton
+              variant="premium"
+              onClick={() => {
+                upgradePlan('project-sharing')
+              }}
+            >
+              {t('upgrade')}
+            </OLButton>
+          )
+        }
+      />
     </div>
   )
 }

+ 0 - 1
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)

+ 0 - 1
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(() => {

+ 0 - 1
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)

+ 39 - 84
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 (
-      <div className="container">
-        <div className="row">
-          <div className="col-md-12">
-            <div className="card">
-              <div className="text-centered link-sharing-invite">
-                <div className="link-sharing-invite-header">
-                  <p>
-                    {t('youre_joining')}
-                    <br />
-                    <em>
-                      <strong>
-                        {requireAcceptData.projectName || 'This project'}
-                      </strong>
-                    </em>
-                    {user && (
-                      <>
-                        <br />
-                        {t('as_email', { email: user.email })}
-                      </>
-                    )}
-                  </p>
-                </div>
-              </div>
-
-              <div className="row row-spaced text-center">
-                <div className="col-md-12">
-                  <p>
-                    {t(
-                      'your_name_and_email_address_will_be_visible_to_the_project_owner_and_other_editors'
-                    )}
-                  </p>
-                </div>
-              </div>
-
-              <div className="row row-spaced text-center">
-                <div className="col-md-12">
-                  <button
-                    className="btn btn-lg btn-primary"
-                    type="submit"
-                    onClick={() => sendPostRequest(true)}
-                  >
-                    {t('ok_join_project')}
-                  </button>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-    )
-  }
-
   return (
-    <div className="loading-screen">
-      <div className="container">
-        <div className="row">
-          <div className="col-md-8 col-md-offset-2">
-            <div className="card">
-              <div className="page-header text-centered">
-                <h1>
-                  {t('invited_to_join')}
+    <div className="container">
+      <div className="row">
+        <div className="col-md-12">
+          <div className="card">
+            <div className="text-centered link-sharing-invite">
+              <div className="link-sharing-invite-header">
+                <p>
+                  {t('youre_joining')}
                   <br />
-                  <em>{requireAcceptData.projectName || 'This project'}</em>
-                </h1>
+                  <em>
+                    <strong>
+                      {requireAcceptData.projectName || 'This project'}
+                    </strong>
+                  </em>
+                  {user && (
+                    <>
+                      <br />
+                      {t('as_email', { email: user.email })}
+                    </>
+                  )}
+                </p>
               </div>
+            </div>
 
-              {user && (
-                <div className="row text-center">
-                  <div className="col-md-12">
-                    <p>
-                      {t('accepting_invite_as')} <em>{user.email}</em>
-                    </p>
-                  </div>
-                </div>
-              )}
+            <div className="row row-spaced text-center">
+              <div className="col-md-12">
+                <p>
+                  {t(
+                    'your_name_and_email_address_will_be_visible_to_the_project_owner_and_other_editors'
+                  )}
+                </p>
+              </div>
+            </div>
 
-              <div className="row text-center">
-                <div className="col-md-12">
-                  <button
-                    className="btn btn-lg btn-primary"
-                    type="submit"
-                    onClick={() => sendPostRequest(true)}
-                  >
-                    {t('join_project')}
-                  </button>
-                </div>
+            <div className="row row-spaced text-center">
+              <div className="col-md-12">
+                <button
+                  className="btn btn-lg btn-primary"
+                  type="submit"
+                  onClick={() => sendPostRequest(true)}
+                >
+                  {t('ok_join_project')}
+                </button>
               </div>
             </div>
           </div>

+ 3 - 14
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 (
       <RequireAcceptScreen
         requireAcceptData={requireAcceptData}
@@ -137,13 +133,6 @@ function TokenAccessRoot() {
       {V1ImportDataScreen && mode === 'v1Import' && v1ImportData && (
         <V1ImportDataScreen v1ImportData={v1ImportData} />
       )}
-
-      {mode === 'requireAccept' && requireAcceptData && (
-        <RequireAcceptScreen
-          requireAcceptData={requireAcceptData}
-          sendPostRequest={sendPostRequest}
-        />
-      )}
     </div>
   )
 }

+ 1 - 5
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

+ 0 - 2
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[]

+ 0 - 4
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</0>.",
   "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</0>.",
-  "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</0> 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.",

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 218 - 747
services/web/test/acceptance/src/TokenAccessTests.mjs


+ 3 - 5
services/web/test/frontend/components/token-access/token-access-page.spec.tsx

@@ -29,13 +29,11 @@ describe('<TokenAccessPage/>', 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('<TokenAccessPage/>', 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

+ 55 - 67
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 setting privilege level to readAndWrite', function () {
+      describe('when owner can add new edit collaborators', function () {
         beforeEach(function () {
-          this.req.body = { privilegeLevel: 'readAndWrite' }
+          this.LimitationsManager.promises.canAddXEditCollaborators.resolves(
+            true
+          )
         })
 
-        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)
-          })
+        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
-            )
-          })
+      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 () {
+      beforeEach(function () {
+        this.req.body = { privilegeLevel: 'readOnly' }
+      })
 
-      describe('when setting privilege level to readOnly', function () {
+      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)
         })
       })
     })

+ 123 - 285
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 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 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 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('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 inviteToProject', function () {
+          this.CollaboratorsInviteHandler.promises.inviteToProject.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)
-      })
+      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 _checkShouldInviteEmail', function () {
-        this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal(
-          0
-        )
-        this.CollaboratorsInviteController._checkShouldInviteEmail
-          .calledWith(this.currentUser, this.targetEmail)
-          .should.equal(false)
-      })
+        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 inviteToProject', function () {
-        this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal(
-          0
-        )
+        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,
+            }
+          )
+        })
       })
     })
 
@@ -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)
       })

+ 24 - 50
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',
-        })
-      })
-
-      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 }
-          )
-        })
+        this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves(
+          false
+        )
       })
 
-      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 }
+        )
       })
     })
 

+ 38 - 90
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)

+ 183 - 375
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 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' }
-          )
-        })
+      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 }
+        )
       })
 
-      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' }
-              }
-            }
-          )
-        })
-
-        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('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('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 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('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('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('when there are collaborator slots available', function () {
-        beforeEach(function () {
-          this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves(
-            true
+        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('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(
+          false
+        )
       })
 
-      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 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)
-          })
+        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)
         })
       })
     })

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio