From 5adb9a63c35509a740f8e1f57ff64d44d7b152f0 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Tue, 11 Jun 2024 15:25:33 +0200 Subject: [PATCH] Merge pull request #18678 from overleaf/jpa-test-sharing [server-pro] add tests for project sharing GitOrigin-RevId: c1862e01ff6feba048e340ba5549ccad9fd07d2c --- server-ce/test/host-admin.js | 2 + server-ce/test/project-sharing.spec.ts | 309 +++++++++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 server-ce/test/project-sharing.spec.ts diff --git a/server-ce/test/host-admin.js b/server-ce/test/host-admin.js index ae46e17edc..a16423523c 100644 --- a/server-ce/test/host-admin.js +++ b/server-ce/test/host-admin.js @@ -117,6 +117,8 @@ const allowedVars = Joi.object( 'ALL_TEX_LIVE_DOCKER_IMAGE_NAMES', 'OVERLEAF_TEMPLATES_USER_ID', 'OVERLEAF_NEW_PROJECT_TEMPLATE_LINKS', + 'OVERLEAF_ALLOW_PUBLIC_ACCESS', + 'OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING', // Old branding, used for upgrade tests 'SHARELATEX_MONGO_URL', 'SHARELATEX_REDIS_HOST', diff --git a/server-ce/test/project-sharing.spec.ts b/server-ce/test/project-sharing.spec.ts new file mode 100644 index 0000000000..e19ddb1380 --- /dev/null +++ b/server-ce/test/project-sharing.spec.ts @@ -0,0 +1,309 @@ +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') + }) + }) + }) + }) +})