Files
overleaf-cep/server-ce/test/project-sharing.spec.ts
Brian Gough 892b52c2e9 Merge pull request #31695 from overleaf/bg-Jenkins-build-failure-Server-Pro-E2E-Tests-1029
reload server cleanly in project sharing tests

GitOrigin-RevId: 1232d870b69a54994441a54da33c93b78d393f22
2026-03-06 09:06:54 +00:00

539 lines
16 KiB
TypeScript

import { v4 as uuid } from 'uuid'
import {
isExcludedBySharding,
startWith,
reloadWith,
STARTUP_TIMEOUT,
} from './helpers/config'
import { ensureUserExists, login } from './helpers/login'
import {
createProjectAndOpenInNewEditor,
enableLinkSharing,
getSpamSafeProjectName,
openProjectByName,
openProjectViaLinkSharingAsAnon,
openProjectViaLinkSharingAsUser,
shareProjectByEmailAndAcceptInviteViaDash,
shareProjectByEmailAndAcceptInviteViaEmail,
} from './helpers/project'
import { prepareWaitForNextCompileSlot } from './helpers/compile'
import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
describe('Project Sharing', function () {
if (isExcludedBySharding('PRO_CUSTOM_4')) return
ensureUserExists({ email: 'user@example.com' })
startWith({ withDataDir: true, pro: true })
let projectName: string
let recompile: () => void
let waitForCompile: (triggerCompile: () => void) => void
beforeWithReRunOnTestRetry(() => {
projectName = getSpamSafeProjectName()
;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
setupTestProject()
})
beforeEach(function () {
// Always start with a fresh session
cy.session([uuid()], () => {})
})
let linkSharingReadOnly: string
let linkSharingReadAndWrite: string
function setupTestProject() {
login('user@example.com')
waitForCompile(() => {
createProjectAndOpenInNewEditor(projectName)
})
// Add chat message
cy.findByRole('tab', { name: 'Chat' }).click()
// wait for lazy loading of the chat pane
cy.findByText('Start the conversation by saying hello or sharing an update')
cy.get(
'textarea[placeholder="Send a message to your collaborators…"]'
).type('New Chat Message{enter}')
// Get link sharing links
enableLinkSharing().then(
({ linkSharingReadOnly: ro, linkSharingReadAndWrite: rw }) => {
linkSharingReadAndWrite = rw
linkSharingReadOnly = ro
}
)
}
function expectContentReadOnlyAccess() {
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
'contain.text',
'\\maketitle'
)
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
'have.attr',
'contenteditable',
'false'
)
}
function expectContentWriteAccess() {
const section = `Test Section ${uuid()}`
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
// wait for the editor to finish loading
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
'contain.text',
'\\maketitle'
)
// the editor should be writable
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
'have.attr',
'contenteditable',
'true'
)
cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
cy.findByText('\\maketitle').parent().click()
cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`)
})
// should have written
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
'contain.text',
`\\section{${section}}`
)
// check PDF
recompile()
cy.findByRole('region', { name: 'PDF preview' }).within(() => {
cy.findByLabelText(/Page.*1/i).should('be.visible')
cy.findByText(projectName).should('be.visible')
})
cy.findByRole('region', { name: 'PDF preview' }).within(() => {
cy.findByLabelText(/Page.*1/i).should('be.visible')
cy.contains(section)
})
}
function expectNoAccess() {
// try read only access link
cy.visit(linkSharingReadOnly)
cy.url().should('match', /\/login/)
// Cypress bugs: cypress resolves the link-sharing link outside the browser, and it carries over the hash of the link-sharing link to the login page redirect (bug 1).
// Effectively, cypress then instructs the browser to change the page from /login#read-only-hash to /login#read-and-write-hash.
// This is turn does not trigger a "page load", but rather just "scrolling", which in turn trips up the "page loaded" detection in cypress (bug 2).
// Work around this by navigating away from the /login page in between checks.
cy.visit('/user/password/reset')
// try read and write access link
cy.visit(linkSharingReadAndWrite)
cy.url().should('match', /\/login/)
}
function expectChatAccess() {
cy.findByRole('tab', { name: 'Chat' }).click()
cy.findByText('New Chat Message')
}
function expectHistoryAccess() {
cy.findByRole('button', { name: 'History' }).click()
// The input is not clickable due to being visually hidden, click its label instead
cy.findByRole('complementary', {
name: 'Project history and labels',
}).within(() => {
cy.findByRole('group', {
name: 'Show all of the project history or only labelled versions.',
}).within(() => {
cy.findByText('All history').click()
})
cy.findByRole('radio', { name: 'Labels' }).should('not.be.checked')
cy.findByRole('radio', { name: 'All history' }).should('be.checked')
})
cy.findByText(/\\begin\{document}/)
cy.findByRole('complementary', {
name: 'Project history and labels',
}).within(() => {
cy.findAllByTestId('history-version-metadata-users')
.last()
.should('have.text', 'user')
})
cy.findByRole('button', { name: 'Back to editor' }).click()
}
function expectNoChatAccess() {
cy.findByRole('button', { name: 'Layout options' }) // wait for lazy loading
cy.findByRole('tab', { name: 'Chat' }).should('not.exist')
}
function expectNoHistoryAccess() {
cy.findByRole('button', { name: 'Layout options' }) // wait for lazy loading
cy.findByRole('button', { name: 'History' }).should('not.exist')
}
function expectCommentAccess() {
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
'contain.text',
'\\maketitle'
)
cy.findByText('\\maketitle').parent().dblclick()
cy.findByRole('button', { name: 'Add comment' }).should('be.visible')
cy.findByRole('textbox', { name: 'Source Editor editing' }).click()
}
function expectNoCommentAccess() {
cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
'contain.text',
'\\maketitle'
)
cy.findByText('\\maketitle').parent().dblclick()
cy.findByRole('button', { name: 'Add comment' }).should('not.exist')
cy.findByRole('textbox', { name: 'Source Editor editing' }).click()
}
function expectFullReadOnlyAccess() {
expectContentReadOnlyAccess()
expectChatAccess()
expectHistoryAccess()
expectNoCommentAccess()
}
function expectRestrictedReadOnlyAccess() {
expectContentReadOnlyAccess()
expectNoChatAccess()
expectNoHistoryAccess()
expectNoCommentAccess()
}
function expectFullReadAndWriteAccess() {
expectContentWriteAccess()
expectChatAccess()
expectHistoryAccess()
expectCommentAccess()
}
function expectAnonymousReadAndWriteAccess() {
expectContentWriteAccess()
expectChatAccess()
expectHistoryAccess()
expectNoCommentAccess()
}
function expectProjectDashboardEntry() {
cy.visit('/project')
cy.findByText(projectName)
}
function expectEditAuthoredAs(author: string) {
cy.findByRole('button', { name: 'History' }).click()
cy.findByRole('complementary', {
name: 'Project history and labels',
}).within(() => {
cy.findAllByTestId('history-version-metadata-users')
.first()
.should('contain.text', author) // might have other edits in the same group
})
}
describe('via email', function () {
const email = 'collaborator-email@example.com'
ensureUserExists({ email })
beforeEach(function () {
login('user@example.com')
shareProjectByEmailAndAcceptInviteViaEmail(
projectName,
email,
'Viewer',
true
)
})
it('should grant the collaborator read access', function () {
expectFullReadOnlyAccess()
expectProjectDashboardEntry()
})
})
describe('read only', function () {
const email = 'collaborator-ro@example.com'
ensureUserExists({ email })
beforeWithReRunOnTestRetry(() => {
login('user@example.com')
shareProjectByEmailAndAcceptInviteViaDash(
projectName,
email,
'Viewer',
true
)
})
it('should grant the collaborator read access', function () {
login(email)
openProjectByName(projectName, true)
expectFullReadOnlyAccess()
expectProjectDashboardEntry()
})
})
describe('read and write', function () {
const email = 'collaborator-rw@example.com'
ensureUserExists({ email })
beforeWithReRunOnTestRetry(() => {
login('user@example.com')
shareProjectByEmailAndAcceptInviteViaDash(
projectName,
email,
'Editor',
true
)
})
it('should grant the collaborator write access', function () {
login(email)
openProjectByName(projectName, true)
expectFullReadAndWriteAccess()
expectEditAuthoredAs('You')
expectProjectDashboardEntry()
})
})
describe('token access', function () {
describe('logged in', function () {
describe('read only', function () {
const email = 'collaborator-link-ro@example.com'
ensureUserExists({ email })
it('should grant restricted read access', function () {
login(email)
openProjectViaLinkSharingAsUser(
linkSharingReadOnly,
projectName,
email,
true
)
expectRestrictedReadOnlyAccess()
expectProjectDashboardEntry()
})
})
describe('read and write', function () {
const email = 'collaborator-link-rw@example.com'
ensureUserExists({ email })
it('should grant full write access', function () {
login(email)
openProjectViaLinkSharingAsUser(
linkSharingReadAndWrite,
projectName,
email,
true
)
expectFullReadAndWriteAccess()
expectEditAuthoredAs('You')
expectProjectDashboardEntry()
})
})
})
describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=false', function () {
describe('wrap startup', function () {
startWith({
pro: true,
vars: {
OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
},
withDataDir: true,
})
it('should block access', function () {
expectNoAccess()
})
})
describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', function () {
startWith({
pro: true,
vars: {
OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
},
withDataDir: true,
})
it('should block access', function () {
expectNoAccess()
})
})
})
describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=true', function () {
describe('wrap startup', function () {
startWith({
pro: true,
vars: {
OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
},
withDataDir: true,
})
it('should grant read access with read link', function () {
openProjectViaLinkSharingAsAnon(linkSharingReadOnly, true)
expectRestrictedReadOnlyAccess()
})
it('should prompt for login with write link', function () {
cy.visit(linkSharingReadAndWrite)
cy.url().should('match', /\/login/)
})
})
describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', function () {
startWith({
pro: true,
vars: {
OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
},
withDataDir: true,
})
it('should grant read access with read link', function () {
openProjectViaLinkSharingAsAnon(linkSharingReadOnly, true)
expectRestrictedReadOnlyAccess()
})
it('should grant write access with write link', function () {
openProjectViaLinkSharingAsAnon(linkSharingReadAndWrite, true)
expectAnonymousReadAndWriteAccess()
expectEditAuthoredAs('Anonymous')
})
})
})
describe('with OVERLEAF_DISABLE_LINK_SHARING=true', function () {
const email = 'collaborator-email@example.com'
ensureUserExists({ email })
const invitedEmail = 'invited-email@example.com'
ensureUserExists({ email: invitedEmail })
const retainedViewerEmail = 'collaborator-retained-viewer@example.com'
ensureUserExists({ email: retainedViewerEmail })
const retainedEditorEmail = 'collaborator-retained-editor@example.com'
ensureUserExists({ email: retainedEditorEmail })
// Link-sharing urls have to be created before disabling link sharing.
// We use the `beforeEach` hook to reload the server with link sharing
// disabled **after** the initial setup which happens in the `before`
// block. The `before` hook always runs prior to the `beforeEach` hook.
// Set up retained access before disabling link sharing
beforeWithReRunOnTestRetry(() => {
// undo the link sharing restriction before joining via link-sharing below
cy.wrap(reloadWith({ withDataDir: true, pro: true }), {
timeout: STARTUP_TIMEOUT,
})
// Set up retained viewer access
login(retainedViewerEmail)
openProjectViaLinkSharingAsUser(
linkSharingReadOnly,
projectName,
retainedViewerEmail,
true
)
// Set up retained editor access
login(retainedEditorEmail)
openProjectViaLinkSharingAsUser(
linkSharingReadAndWrite,
projectName,
retainedEditorEmail,
true
)
})
beforeEach(function () {
this.timeout(STARTUP_TIMEOUT) // Increase timeout for server reload
return cy.wrap(
reloadWith({
pro: true,
vars: {
OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
OVERLEAF_DISABLE_LINK_SHARING: 'true',
},
withDataDir: true,
}),
{ timeout: STARTUP_TIMEOUT }
)
})
it('should not display link sharing in the sharing modal', function () {
login('user@example.com')
openProjectByName(projectName, true)
cy.findByRole('navigation', {
name: 'Project actions',
})
.findByRole('button', { name: 'Share' })
.click()
cy.findByRole('button', { name: 'Turn on link sharing' }).should(
'not.exist'
)
})
it('should block new access to read-only link shared projects', function () {
login(email)
// Test read-only link returns 404
cy.request({
url: linkSharingReadOnly,
failOnStatusCode: false,
}).then(response => {
expect(response.status).to.eq(404)
})
})
it('should block new access to read-write link shared projects', function () {
login(email)
// Test read-write link returns 404
cy.request({
url: linkSharingReadAndWrite,
failOnStatusCode: false,
}).then(response => {
expect(response.status).to.eq(404)
})
})
it('should continue to allow email sharing', function () {
login('user@example.com')
shareProjectByEmailAndAcceptInviteViaEmail(
projectName,
invitedEmail,
'Viewer',
true
)
expectFullReadOnlyAccess()
expectProjectDashboardEntry()
})
it('should retain read-only access when project was joined via link before link sharing was turned off', function () {
login(retainedViewerEmail)
openProjectByName(projectName, true)
expectRestrictedReadOnlyAccess()
expectProjectDashboardEntry()
})
it('should retain read-write access when project was joined via link before link sharing was turned off', function () {
login(retainedEditorEmail)
openProjectByName(projectName, true)
expectFullReadAndWriteAccess()
expectProjectDashboardEntry()
})
})
})
})