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

+
+ {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 team0> for help.",
"sorry_there_was_an_issue_upgrading_your_subscription": "Sorry, there was an issue upgrading your subscription. Please <0>contact our Support team0> 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>manager1> of the <0>__planName__0> group subscription <1>__groupName__1> administered by <1>__adminEmail__1>.",
"you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z_you": "You are a <1>manager1> of the <0>__planName__0> group subscription <1>__groupName__1> administered by <1>you (__adminEmail__1>).",
"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__0>. 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 Support0> to find out more.",
"you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "You are on our <0>__planName__0> plan as a <1>confirmed member1> of <1>__institutionName__1>",
"you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "You are on our <0>__planName__0> plan as a <1>member1> of the group subscription <1>__groupName__1> administered by <1>__adminEmail__1>",
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)
+ })
})
})
})