From d61413e57d9bf074f13f2cb215b0bbd2ed0525ff Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:18:31 +0200 Subject: [PATCH] Merge pull request #31827 from overleaf/ii-project-sharing-access-denied [web] Project sharing access denied redesign GitOrigin-RevId: b1e3016eb7ef9e2a502e0b67abc3b10c08531fe9 --- .../CollaboratorsInviteController.mjs | 13 +- .../views/project/invite/not-valid-legacy.pug | 18 + .../app/views/project/invite/not-valid.pug | 26 +- .../project/token/access-react-legacy.pug | 2 +- .../web/frontend/extracted-translations.json | 4 + .../share-project/invite-not-valid-root.tsx | 15 + .../share-project/invite-not-valid.tsx | 58 +++ .../components/access-attempt-screen.tsx | 29 ++ .../js/pages/project-invite-not-valid.tsx | 12 + .../stylesheets/pages/token-access.scss | 10 +- services/web/locales/en.json | 4 + .../token-access/token-access-page.spec.tsx | 12 +- .../components/invite-not-valid.spec.tsx | 52 +++ .../CollaboratorsInviteController.test.mjs | 378 ++++++++++++------ 14 files changed, 494 insertions(+), 139 deletions(-) create mode 100644 services/web/app/views/project/invite/not-valid-legacy.pug create mode 100644 services/web/frontend/js/features/share-project/invite-not-valid-root.tsx create mode 100644 services/web/frontend/js/features/share-project/invite-not-valid.tsx create mode 100644 services/web/frontend/js/pages/project-invite-not-valid.tsx create mode 100644 services/web/test/frontend/features/share-project/components/invite-not-valid.spec.tsx diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs index 075eb1d784..7cbcb38f8e 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs +++ b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.mjs @@ -263,10 +263,18 @@ async function viewInvite(req, res) { const projectId = req.params.Project_id const { token } = req.params + const { variant: sharingUpdates } = + await SplitTestHandler.promises.getAssignment(req, res, 'sharing-updates') + const _renderInvalidPage = function () { res.status(404) logger.debug({ projectId }, 'invite not valid, rendering not-valid page') - res.render('project/invite/not-valid', { title: 'Invalid Invite' }) + + if (sharingUpdates === 'enabled') { + res.render('project/invite/not-valid', { title: 'Invalid Invite' }) + } else { + res.render('project/invite/not-valid-legacy', { title: 'Invalid Invite' }) + } } // check if the user is already a member of the project @@ -329,9 +337,6 @@ async function viewInvite(req, res) { // cleanup if set for register page delete req.session.sharedProjectData - const { variant: sharingUpdates } = - await SplitTestHandler.promises.getAssignment(req, res, 'sharing-updates') - // finally render the invite if (sharingUpdates === 'enabled') { res.render('project/invite/show', { diff --git a/services/web/app/views/project/invite/not-valid-legacy.pug b/services/web/app/views/project/invite/not-valid-legacy.pug new file mode 100644 index 0000000000..3722ab7919 --- /dev/null +++ b/services/web/app/views/project/invite/not-valid-legacy.pug @@ -0,0 +1,18 @@ +extends ../../layout-marketing + +block content + main#main-content.content.content-alt + .container + .row + .col-md-8.col-md-offset-2.offset-md-2 + .card.project-invite-invalid + .card-body + .page-header.text-center + h1 #{translate("invite_not_valid")} + .row.text-center + .col-12.col-md-12 + p + | #{translate("invite_not_valid_description")}. + .row.text-center.actions + .col-12.col-md-12 + a.btn.btn-secondary-info.btn-secondary(href='/project') #{translate("back_to_your_projects")} diff --git a/services/web/app/views/project/invite/not-valid.pug b/services/web/app/views/project/invite/not-valid.pug index 3722ab7919..07e7bda008 100644 --- a/services/web/app/views/project/invite/not-valid.pug +++ b/services/web/app/views/project/invite/not-valid.pug @@ -1,18 +1,14 @@ -extends ../../layout-marketing +extends ../../layout-website-redesign + +block vars + - isWebsiteRedesign = true + +block entrypointVar + - entrypoint = 'pages/project-invite-not-valid' + +block append meta + meta(name='ol-user' data-type='json' content=user) block content main#main-content.content.content-alt - .container - .row - .col-md-8.col-md-offset-2.offset-md-2 - .card.project-invite-invalid - .card-body - .page-header.text-center - h1 #{translate("invite_not_valid")} - .row.text-center - .col-12.col-md-12 - p - | #{translate("invite_not_valid_description")}. - .row.text-center.actions - .col-12.col-md-12 - a.btn.btn-secondary-info.btn-secondary(href='/project') #{translate("back_to_your_projects")} + #project-invite-not-valid-page diff --git a/services/web/app/views/project/token/access-react-legacy.pug b/services/web/app/views/project/token/access-react-legacy.pug index 90d5f9ea4b..84c864d84f 100644 --- a/services/web/app/views/project/token/access-react-legacy.pug +++ b/services/web/app/views/project/token/access-react-legacy.pug @@ -13,4 +13,4 @@ block append meta meta(name='ol-user' data-type='json' content=user) block content - #token-access-page + #token-access-page.token-access-legacy-page diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 85c334e666..3fa78c4c06 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -188,6 +188,7 @@ "back_to_configuration": "", "back_to_editing": "", "back_to_editor": "", + "back_to_my_projects": "", "back_to_subscription": "", "back_to_your_projects": "", "basic_compile_time": "", @@ -1725,6 +1726,7 @@ "sorry_there_are_no_experiments": "", "sorry_there_was_an_issue_adding_x_users_to_your_subscription": "", "sorry_there_was_an_issue_upgrading_your_subscription": "", + "sorry_this_project_is_not_available": "", "sorry_you_can_only_change_to_group_from_trial_via_support": "", "sorry_you_can_only_change_to_group_via_support": "", "sorry_your_table_cant_be_displayed_at_the_moment": "", @@ -1901,6 +1903,7 @@ "the_following_folder_already_exists_in_this_project_plural": "", "the_home_of_research_writing": "", "the_latex_engine_used_for_compiling": "", + "the_link_may_be_broken_or_you_may_not_have_access_rights": "", "the_new_and_improved_overleaf_editor_design": "", "the_next_payment_will_be_collected_on": "", "the_original_text_has_changed": "", @@ -2282,6 +2285,7 @@ "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "", "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z_you": "", "you_are_currently_logged_in_as": "", + "you_are_currently_logged_in_as_x_you_might_need_to_log_in_with_different_email": "", "you_are_on_a_paid_plan_contact_support_to_find_out_more": "", "you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "", "you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "", diff --git a/services/web/frontend/js/features/share-project/invite-not-valid-root.tsx b/services/web/frontend/js/features/share-project/invite-not-valid-root.tsx new file mode 100644 index 0000000000..e3cb4d5e75 --- /dev/null +++ b/services/web/frontend/js/features/share-project/invite-not-valid-root.tsx @@ -0,0 +1,15 @@ +import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n' +import getMeta from '@/utils/meta' +import InviteNotValid from '@/features/share-project/invite-not-valid' +import { User } from '@ol-types/user' + +export default function InviteNotValidRoot() { + const user = getMeta('ol-user') as User | undefined + const { isReady } = useWaitForI18n() + + if (!isReady) { + return null + } + + return +} diff --git a/services/web/frontend/js/features/share-project/invite-not-valid.tsx b/services/web/frontend/js/features/share-project/invite-not-valid.tsx new file mode 100644 index 0000000000..64ce9b8c7d --- /dev/null +++ b/services/web/frontend/js/features/share-project/invite-not-valid.tsx @@ -0,0 +1,58 @@ +import { useTranslation, Trans } from 'react-i18next' +import OLRow from '@/shared/components/ol/ol-row' +import OLCol from '@/shared/components/ol/ol-col' +import OLButton from '@/shared/components/ol/ol-button' +import overleafLogo from '@/shared/svgs/overleaf-logo.svg' +import getMeta from '@/utils/meta' + +type InviteNotValidProps = { + email?: string +} + +function InviteNotValid({ email }: InviteNotValidProps) { + const { t } = useTranslation() + const { appName } = getMeta('ol-ExposedSettings') + + return ( +
+ + +
+ {appName} +

+ {t('sorry_this_project_is_not_available')} +

+
+ {t('the_link_may_be_broken_or_you_may_not_have_access_rights')} +
+ {email && ( + <> + + {t('back_to_my_projects')} + +
+ + ]} // eslint-disable-line react/jsx-key + values={{ email }} + shouldUnescape + tOptions={{ interpolation: { escapeValue: true } }} + /> + +
+ + )} +
+
+
+
+ ) +} + +export default InviteNotValid diff --git a/services/web/frontend/js/features/token-access/components/access-attempt-screen.tsx b/services/web/frontend/js/features/token-access/components/access-attempt-screen.tsx index 0813325bd7..743febc58b 100644 --- a/services/web/frontend/js/features/token-access/components/access-attempt-screen.tsx +++ b/services/web/frontend/js/features/token-access/components/access-attempt-screen.tsx @@ -1,5 +1,8 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' +import InviteNotValid from '@/features/share-project/invite-not-valid' +import getMeta from '@/utils/meta' +import { useFeatureFlag } from '@/shared/context/split-test-context' export const AccessAttemptScreen: FC<{ loadingScreenBrandHeight: string @@ -7,6 +10,32 @@ export const AccessAttemptScreen: FC<{ accessError: string | boolean }> = ({ loadingScreenBrandHeight, inflight, accessError }) => { const { t } = useTranslation() + const user = getMeta('ol-user') + const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates') + + if (isSharingUpdatesEnabled) { + if (accessError) { + return + } + + return ( +
+
+
+
+
+ +

+ {t('join_project')} + {inflight && } +

+
+
+ ) + } return (
diff --git a/services/web/frontend/js/pages/project-invite-not-valid.tsx b/services/web/frontend/js/pages/project-invite-not-valid.tsx new file mode 100644 index 0000000000..8f853b7ef2 --- /dev/null +++ b/services/web/frontend/js/pages/project-invite-not-valid.tsx @@ -0,0 +1,12 @@ +import './../utils/meta' +import '../utils/webpack-public-path' +import './../infrastructure/error-reporter' +import '@/i18n' +import { createRoot } from 'react-dom/client' +import InviteNotValidRoot from '@/features/share-project/invite-not-valid-root' + +const element = document.getElementById('project-invite-not-valid-page') +if (element) { + const root = createRoot(element) + root.render() +} diff --git a/services/web/frontend/stylesheets/pages/token-access.scss b/services/web/frontend/stylesheets/pages/token-access.scss index a047f02042..baf2a24bc8 100644 --- a/services/web/frontend/stylesheets/pages/token-access.scss +++ b/services/web/frontend/stylesheets/pages/token-access.scss @@ -1,6 +1,12 @@ #token-access-page { - height: 100vh; - height: 100dvh; + &.token-access-legacy-page { + height: 100vh; + height: 100dvh; + } + + .vertically-centered-content { + height: $thin-footer-content-height; + } } @include theme('default') { diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 359ae8a4f0..f7c5afc831 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -236,6 +236,7 @@ "back_to_editing": "Back to editing", "back_to_editor": "Back to editor", "back_to_log_in": "Back to log in", + "back_to_my_projects": "Back to my projects", "back_to_subscription": "Back to subscription", "back_to_your_projects": "Back to your projects", "basic": "Basic", @@ -2211,6 +2212,7 @@ "sorry_there_was_an_issue_adding_x_users_to_your_subscription": "Sorry, there was an issue adding __count__ users to your subscription. Please <0>contact our Support team for help.", "sorry_there_was_an_issue_upgrading_your_subscription": "Sorry, there was an issue upgrading your subscription. Please <0>contact our Support team for help.", "sorry_this_account_has_been_suspended": "Sorry, this account has been suspended.", + "sorry_this_project_is_not_available": "Sorry, this project isn’t available", "sorry_you_can_only_change_to_group_from_trial_via_support": "Sorry, you can only change to a group plan during a free trial by contacting support.", "sorry_you_can_only_change_to_group_via_support": "Sorry, you can only change to a group plan by contacting support.", "sorry_your_table_cant_be_displayed_at_the_moment": "Sorry, your table can’t be displayed at the moment.", @@ -2426,6 +2428,7 @@ "the_following_folder_already_exists_in_this_project_plural": "The following folders already exist in this project:", "the_home_of_research_writing": "The home of research writing.", "the_latex_engine_used_for_compiling": "The LaTeX engine used for compiling", + "the_link_may_be_broken_or_you_may_not_have_access_rights": "The link may be broken or you may not have access rights.", "the_new_and_improved_overleaf_editor_design": "The new and improved __appName__ editor design brings you a cleaner, less cluttered interface to help you focus on what matters—your work.", "the_next_payment_will_be_collected_on": "The next payment will be collected on __date__.", "the_original_text_has_changed": "The original text has changed, so this suggestion can’t be applied", @@ -2851,6 +2854,7 @@ "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "You are a <1>manager of the <0>__planName__ group subscription <1>__groupName__ administered by <1>__adminEmail__.", "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z_you": "You are a <1>manager of the <0>__planName__ group subscription <1>__groupName__ administered by <1>you (__adminEmail__).", "you_are_currently_logged_in_as": "You are currently logged in as __email__.", + "you_are_currently_logged_in_as_x_you_might_need_to_log_in_with_different_email": "You are currently logged in as <0>__email__. You might need to log in with a different email address.", "you_are_on_a_paid_plan_contact_support_to_find_out_more": "You’re on an __appName__ Paid plan. <0>Contact Support to find out more.", "you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "You are on our <0>__planName__ plan as a <1>confirmed member of <1>__institutionName__", "you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "You are on our <0>__planName__ plan as a <1>member of the group subscription <1>__groupName__ administered by <1>__adminEmail__", diff --git a/services/web/test/frontend/components/token-access/token-access-page.spec.tsx b/services/web/test/frontend/components/token-access/token-access-page.spec.tsx index 299ef787b4..e7da824dee 100644 --- a/services/web/test/frontend/components/token-access/token-access-page.spec.tsx +++ b/services/web/test/frontend/components/token-access/token-access-page.spec.tsx @@ -79,10 +79,16 @@ describe('', function () { cy.wait('@grantRequest') - cy.get('h3').should('have.text', 'Join Project') - cy.get('h4').should('have.text', 'Project not found') - + cy.findByRole('heading', { name: /sorry, this project isn’t available/i }) + cy.findByText(/the link may be broken or you may not have access rights/i) cy.findByRole('button', { name: 'Join Project' }).should('not.exist') + cy.contains( + new RegExp( + 'you are currently logged in as test@example.com. ' + + 'you might need to log in with a different email address', + 'i' + ) + ) }) it('handles a redirect response', function () { diff --git a/services/web/test/frontend/features/share-project/components/invite-not-valid.spec.tsx b/services/web/test/frontend/features/share-project/components/invite-not-valid.spec.tsx new file mode 100644 index 0000000000..62ece0b18f --- /dev/null +++ b/services/web/test/frontend/features/share-project/components/invite-not-valid.spec.tsx @@ -0,0 +1,52 @@ +import InviteNotValid from '@/features/share-project/invite-not-valid' + +describe('', function () { + const email = 'test@example.com' + + it('renders the sorry message', function () { + cy.mount() + + cy.findByRole('heading', { + name: /sorry, this project isn’t available/i, + }) + }) + + it('renders the broken link message', function () { + cy.mount() + + cy.findByText(/the link may be broken or you may not have access rights/i) + }) + + it('renders a back to projects button linking to /project', function () { + cy.mount() + + cy.findByRole('link', { name: /back to my projects/i }).should( + 'have.attr', + 'href', + '/project' + ) + }) + + it('renders the logged-in email', function () { + cy.mount() + + cy.contains( + new RegExp( + `you are currently logged in as ${email}. you might need to log in with a different email address`, + 'i' + ) + ) + }) + + it('does not render the CTA and email when email not provided', function () { + cy.mount() + + cy.findByRole('link', { name: /back to my projects/i }).should('not.exist') + cy.contains( + new RegExp( + `you are currently logged in as ${email}. you might need to log in with a different email address`, + 'i' + ) + ).should('not.exist') + }) +}) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs index 84b0887eeb..8c61e5e479 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs @@ -1035,54 +1035,108 @@ describe('CollaboratorsInviteController', function () { }) describe('when the getInviteByToken does not produce an invite', function () { - beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(null) - ctx.res.callback = () => resolve() - ctx.CollaboratorsInviteController.viewInvite( - ctx.req, - ctx.res, - ctx.next + describe('when the sharing-updates variant is "enabled"', function () { + beforeEach(async function (ctx) { + ctx.SplitTestHandler.promises.getAssignment.resolves({ + variant: 'enabled', + }) + await new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves( + null + ) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) + }) + + it('should render the not-valid view template', function (ctx) { + expect(ctx.res.render).toHaveBeenCalledTimes(1) + expect(ctx.res.render).toHaveBeenCalledWith( + 'project/invite/not-valid', + expect.anything() ) }) + + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) + }) }) - it('should render the not-valid view template', function (ctx) { - expect(ctx.res.render).toHaveBeenCalledTimes(1) - expect(ctx.res.render).toHaveBeenCalledWith( - 'project/invite/not-valid', - expect.anything() - ) + describe('when the sharing-updates variant is "default"', function () { + beforeEach(async function (ctx) { + ctx.SplitTestHandler.promises.getAssignment.resolves({ + variant: 'default', + }) + await new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves( + null + ) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) + }) + + it('should render the not-valid-legacy view template', function (ctx) { + expect(ctx.res.render).toHaveBeenCalledTimes(1) + expect(ctx.res.render).toHaveBeenCalledWith( + 'project/invite/not-valid-legacy', + expect.anything() + ) + }) + + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) + }) }) - it('should not call next', function (ctx) { - ctx.next.callCount.should.equal(0) - }) + describe('common behaviour', function () { + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves( + null + ) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) + }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { - ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( - 1 - ) - ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(ctx.currentUser._id, ctx.projectId) - .should.equal(true) - }) + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) + .should.equal(true) + }) - it('should call getInviteByToken', function (ctx) { - ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( - 1 - ) - ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(ctx.currentUser._id, ctx.projectId) - .should.equal(true) - }) + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + 1 + ) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) + .should.equal(true) + }) - it('should not call User.getUser', function (ctx) { - ctx.UserGetter.promises.getUser.callCount.should.equal(0) - }) + it('should not call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(0) + }) - it('should not call ProjectGetter.getProject', function (ctx) { - ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) + }) }) }) @@ -1132,54 +1186,102 @@ describe('CollaboratorsInviteController', function () { }) describe('when User.getUser does not find a user', function () { - beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.UserGetter.promises.getUser.resolves(null) - ctx.res.callback = () => resolve() - ctx.CollaboratorsInviteController.viewInvite( - ctx.req, - ctx.res, - ctx.next + describe('when the sharing-updates variant is "enabled"', function () { + beforeEach(async function (ctx) { + ctx.SplitTestHandler.promises.getAssignment.resolves({ + variant: 'enabled', + }) + await new Promise(resolve => { + ctx.UserGetter.promises.getUser.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) + }) + + it('should render the not-valid view template', function (ctx) { + expect(ctx.res.render).toHaveBeenCalledTimes(1) + expect(ctx.res.render).toHaveBeenCalledWith( + 'project/invite/not-valid', + expect.anything() ) }) + + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) + }) }) - it('should render the not-valid view template', function (ctx) { - expect(ctx.res.render).toHaveBeenCalledTimes(1) - expect(ctx.res.render).toHaveBeenCalledWith( - 'project/invite/not-valid', - expect.anything() - ) + describe('when the sharing-updates variant is "default"', function () { + beforeEach(async function (ctx) { + ctx.SplitTestHandler.promises.getAssignment.resolves({ + variant: 'default', + }) + await new Promise(resolve => { + ctx.UserGetter.promises.getUser.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) + }) + + it('should render the not-valid-legacy view template', function (ctx) { + expect(ctx.res.render).toHaveBeenCalledTimes(1) + expect(ctx.res.render).toHaveBeenCalledWith( + 'project/invite/not-valid-legacy', + expect.anything() + ) + }) + + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) + }) }) - it('should not call next', function (ctx) { - ctx.next.callCount.should.equal(0) - }) + describe('common behaviour', function () { + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.UserGetter.promises.getUser.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) + }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { - ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( - 1 - ) - ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(ctx.currentUser._id, ctx.projectId) - .should.equal(true) - }) + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) + .should.equal(true) + }) - it('should call getInviteByToken', function (ctx) { - ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( - 1 - ) - }) + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + 1 + ) + }) - it('should call User.getUser', function (ctx) { - ctx.UserGetter.promises.getUser.callCount.should.equal(1) - ctx.UserGetter.promises.getUser - .calledWith({ _id: ctx.fakeProject.owner_ref }) - .should.equal(true) - }) + it('should call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) + .should.equal(true) + }) - it('should not call ProjectGetter.getProject', function (ctx) { - ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) + }) }) }) @@ -1229,54 +1331,102 @@ describe('CollaboratorsInviteController', function () { }) describe('when Project.getUser does not find a user', function () { - beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.ProjectGetter.promises.getProject.resolves(null) - ctx.res.callback = () => resolve() - ctx.CollaboratorsInviteController.viewInvite( - ctx.req, - ctx.res, - ctx.next + describe('when the sharing-updates variant is "enabled"', function () { + beforeEach(async function (ctx) { + ctx.SplitTestHandler.promises.getAssignment.resolves({ + variant: 'enabled', + }) + await new Promise(resolve => { + ctx.ProjectGetter.promises.getProject.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) + }) + + it('should render the not-valid view template', function (ctx) { + expect(ctx.res.render).toHaveBeenCalledTimes(1) + expect(ctx.res.render).toHaveBeenCalledWith( + 'project/invite/not-valid', + expect.anything() ) }) + + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) + }) }) - it('should render the not-valid view template', function (ctx) { - expect(ctx.res.render).toHaveBeenCalledTimes(1) - expect(ctx.res.render).toHaveBeenCalledWith( - 'project/invite/not-valid', - expect.anything() - ) + describe('when the sharing-updates variant is "default"', function () { + beforeEach(async function (ctx) { + ctx.SplitTestHandler.promises.getAssignment.resolves({ + variant: 'default', + }) + await new Promise(resolve => { + ctx.ProjectGetter.promises.getProject.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) + }) + + it('should render the not-valid-legacy view template', function (ctx) { + expect(ctx.res.render).toHaveBeenCalledTimes(1) + expect(ctx.res.render).toHaveBeenCalledWith( + 'project/invite/not-valid-legacy', + expect.anything() + ) + }) + + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) + }) }) - it('should not call next', function (ctx) { - ctx.next.callCount.should.equal(0) - }) + describe('common behaviour', function () { + beforeEach(async function (ctx) { + await new Promise(resolve => { + ctx.ProjectGetter.promises.getProject.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) + }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { - ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( - 1 - ) - ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(ctx.currentUser._id, ctx.projectId) - .should.equal(true) - }) + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) + .should.equal(true) + }) - it('should call getInviteByToken', function (ctx) { - ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( - 1 - ) - }) + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + 1 + ) + }) - it('should call getUser', function (ctx) { - ctx.UserGetter.promises.getUser.callCount.should.equal(1) - ctx.UserGetter.promises.getUser - .calledWith({ _id: ctx.fakeProject.owner_ref }) - .should.equal(true) - }) + it('should call getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) + .should.equal(true) + }) - it('should call ProjectGetter.getProject', function (ctx) { - ctx.ProjectGetter.promises.getProject.callCount.should.equal(1) + it('should call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(1) + }) }) }) })