Merge pull request #31827 from overleaf/ii-project-sharing-access-denied

[web] Project sharing access denied redesign

GitOrigin-RevId: b1e3016eb7ef9e2a502e0b67abc3b10c08531fe9
This commit is contained in:
ilkin-overleaf
2026-03-18 14:18:31 +02:00
committed by Copybot
parent 79cb219ad9
commit d61413e57d
14 changed files with 494 additions and 139 deletions

View File

@@ -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', {

View File

@@ -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")}

View File

@@ -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

View File

@@ -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

View File

@@ -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": "",

View File

@@ -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 <InviteNotValid email={user?.email} />
}

View File

@@ -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 (
<div className="container">
<OLRow>
<OLCol lg={{ span: 6, offset: 3 }}>
<div className="project-join-container">
<img src={overleafLogo} alt={appName} />
<h1 className="h4 mb-2">
{t('sorry_this_project_is_not_available')}
</h1>
<div className="mb-4">
{t('the_link_may_be_broken_or_you_may_not_have_access_rights')}
</div>
{email && (
<>
<OLButton
variant="primary"
size="lg"
href="/project"
className="mb-4"
>
{t('back_to_my_projects')}
</OLButton>
<div>
<small>
<Trans
i18nKey="you_are_currently_logged_in_as_x_you_might_need_to_log_in_with_different_email"
components={[<b />]} // eslint-disable-line react/jsx-key
values={{ email }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
/>
</small>
</div>
</>
)}
</div>
</OLCol>
</OLRow>
</div>
)
}
export default InviteNotValid

View File

@@ -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 <InviteNotValid email={user?.email} />
}
return (
<div className="vertically-centered-content">
<div className="loading-screen">
<div className="loading-screen-brand-container">
<div
className="loading-screen-brand"
style={{ height: loadingScreenBrandHeight }}
/>
</div>
<h3 className="loading-screen-label text-center">
{t('join_project')}
{inflight && <LoadingScreenEllipses />}
</h3>
</div>
</div>
)
}
return (
<div className="loading-screen">

View File

@@ -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(<InviteNotValidRoot />)
}

View File

@@ -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') {

View File

@@ -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</0> for help.",
"sorry_there_was_an_issue_upgrading_your_subscription": "Sorry, there was an issue upgrading your subscription. Please <0>contact our Support team</0> for help.",
"sorry_this_account_has_been_suspended": "Sorry, this account has been suspended.",
"sorry_this_project_is_not_available": "Sorry, this project isnt 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 cant 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 <strong>__date__</strong>.",
"the_original_text_has_changed": "The original text has changed, so this suggestion cant 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</1> 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>manager</1> 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 <b>__email__</b>.",
"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": "Youre on an __appName__ Paid plan. <0>Contact Support</0> 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 member</1> 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>member</1> of the group subscription <1>__groupName__</1> administered by <1>__adminEmail__</1>",

View File

@@ -79,10 +79,16 @@ describe('<TokenAccessPage/>', 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 isnt 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 () {

View File

@@ -0,0 +1,52 @@
import InviteNotValid from '@/features/share-project/invite-not-valid'
describe('<InviteNotValid />', function () {
const email = 'test@example.com'
it('renders the sorry message', function () {
cy.mount(<InviteNotValid email={email} />)
cy.findByRole('heading', {
name: /sorry, this project isnt available/i,
})
})
it('renders the broken link message', function () {
cy.mount(<InviteNotValid email={email} />)
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(<InviteNotValid email={email} />)
cy.findByRole('link', { name: /back to my projects/i }).should(
'have.attr',
'href',
'/project'
)
})
it('renders the logged-in email', function () {
cy.mount(<InviteNotValid email={email} />)
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(<InviteNotValid />)
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')
})
})

View File

@@ -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)
})
})
})
})