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-19 09:06:31 +00:00
committed by Copybot
parent 79cb219ad9
commit d61413e57d
14 changed files with 494 additions and 139 deletions
@@ -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 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 () {
@@ -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 isn’t 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')
})
})
@@ -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)
})
})
})
})