import { v4 as uuid } from 'uuid' import { startWith } from './helpers/config' import { ensureUserExists, login } from './helpers/login' import { createProject } from './helpers/project' import { throttledRecompile } from './helpers/compile' describe('Project Sharing', function () { ensureUserExists({ email: 'user@example.com' }) startWith({ withDataDir: true }) let projectName: string before(function () { projectName = `Project ${uuid()}` setupTestProject() }) beforeEach(() => { // Always start with a fresh session cy.session([uuid()], () => {}) }) let linkSharingReadOnly: string let linkSharingReadAndWrite: string function setupTestProject() { login('user@example.com') cy.visit('/project') createProject(projectName) // Add chat message cy.findByText('Chat').click() cy.get( 'textarea[placeholder="Send a message to your collaborators…"]' ).type('New Chat Message{enter}') // Get link sharing links cy.findByText('Share').click() cy.findByText('Turn on link sharing').click() cy.findByText('Anyone with this link can view this project') .next() .should('contain.text', 'http://sharelatex/') .then(el => { linkSharingReadOnly = el.text() }) cy.findByText('Anyone with this link can edit this project') .next() .should('contain.text', 'http://sharelatex/') .then(el => { linkSharingReadAndWrite = el.text() }) } function shareProjectByEmailAndAcceptInvite( email: string, level: 'Read Only' | 'Can Edit' ) { login('user@example.com') cy.visit('/project') cy.findByText(projectName).click() cy.findByText('Share').click() cy.findByRole('dialog').within(() => { cy.get('input').type(`${email},`) cy.get('input') .parents('form') .within(() => cy.findByText('Can Edit').parent().select(level)) cy.findByText('Share').click({ force: true }) }) login(email) cy.visit('/project') cy.findByText(new RegExp(projectName)) .parent() .parent() .within(() => { cy.findByText('Join Project').click() }) } function expectContentReadOnlyAccess() { cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/) cy.get('.cm-content').should('contain.text', '\\maketitle') cy.get('.cm-content').should('have.attr', 'contenteditable', 'false') } function expectContentWriteAccess() { const section = `Test Section ${uuid()}` cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/) const recompile = throttledRecompile() // wait for the editor to finish loading cy.get('.cm-content').should('contain.text', '\\maketitle') // the editor should be writable cy.get('.cm-content').should('have.attr', 'contenteditable', 'true') cy.findByText('\\maketitle').parent().click() cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`) // should have written cy.get('.cm-content').should('contain.text', `\\section{${section}}`) // check PDF recompile() cy.get('.pdf-viewer').should('contain.text', projectName) cy.get('.pdf-viewer').should('contain.text', 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.findByText('Chat').click() cy.findByText('New Chat Message') } function expectHistoryAccess() { cy.findByText('History').click() cy.findByText('Labels') cy.findByText(/\\begin\{document}/) cy.findAllByTestId('history-version-metadata-users') .last() .should('have.text', 'user') cy.findByText('Back to editor').click() } function expectNoChatAccess() { cy.findByText('Layout') // wait for lazy loading cy.findByText('Chat').should('not.exist') } function expectNoHistoryAccess() { cy.findByText('Layout') // wait for lazy loading cy.findByText('History').should('not.exist') } function expectFullReadOnlyAccess() { expectContentReadOnlyAccess() expectChatAccess() expectHistoryAccess() } function expectRestrictedReadOnlyAccess() { expectContentReadOnlyAccess() expectNoChatAccess() expectNoHistoryAccess() } function expectReadAndWriteAccess() { expectContentWriteAccess() expectChatAccess() expectHistoryAccess() } function expectProjectDashboardEntry() { cy.visit('/project') cy.findByText(projectName) } function expectEditAuthoredAs(author: string) { cy.findByText('History').click() cy.findAllByTestId('history-version-metadata-users') .first() .should('contain.text', author) // might have other edits in the same group } describe('read only', () => { const email = 'collaborator-ro@example.com' ensureUserExists({ email }) before(function () { shareProjectByEmailAndAcceptInvite(email, 'Read Only') }) it('should grant the collaborator read access', () => { login(email) cy.visit('/project') cy.findByText(projectName).click() expectFullReadOnlyAccess() expectProjectDashboardEntry() }) }) describe('read and write', () => { const email = 'collaborator-rw@example.com' ensureUserExists({ email }) before(function () { shareProjectByEmailAndAcceptInvite(email, 'Can Edit') }) it('should grant the collaborator write access', () => { login(email) cy.visit('/project') cy.findByText(projectName).click() expectReadAndWriteAccess() expectEditAuthoredAs('You') expectProjectDashboardEntry() }) }) describe('token access', () => { describe('logged in', () => { describe('read only', () => { const email = 'collaborator-link-ro@example.com' ensureUserExists({ email }) it('should grant restricted read access', () => { login(email) cy.visit(linkSharingReadOnly) cy.findByText(projectName) // wait for lazy loading cy.findByText('Join Project').click() expectRestrictedReadOnlyAccess() expectProjectDashboardEntry() }) }) describe('read and write', () => { const email = 'collaborator-link-rw@example.com' ensureUserExists({ email }) it('should grant full write access', () => { login(email) cy.visit(linkSharingReadAndWrite) cy.findByText(projectName) // wait for lazy loading cy.findByText('Join Project').click() expectReadAndWriteAccess() expectEditAuthoredAs('You') expectProjectDashboardEntry() }) }) }) describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=false', () => { describe('wrap startup', () => { startWith({ vars: { OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false', }, withDataDir: true, }) it('should block access', () => { expectNoAccess() }) }) describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', () => { startWith({ vars: { OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false', OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true', }, withDataDir: true, }) it('should block access', () => { expectNoAccess() }) }) }) describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=true', () => { describe('wrap startup', () => { startWith({ vars: { OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true', }, withDataDir: true, }) it('should grant read access with read link', () => { cy.visit(linkSharingReadOnly) expectRestrictedReadOnlyAccess() }) it('should prompt for login with write link', () => { cy.visit(linkSharingReadAndWrite) cy.url().should('match', /\/login/) }) }) describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', () => { startWith({ vars: { OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true', OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true', }, withDataDir: true, }) it('should grant read access with read link', () => { cy.visit(linkSharingReadOnly) expectRestrictedReadOnlyAccess() }) it('should grant write access with write link', () => { cy.visit(linkSharingReadAndWrite) expectReadAndWriteAccess() expectEditAuthoredAs('Anonymous') }) }) }) }) })