From 8748ac74751481352771b05817a593c3f99e843c Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Mon, 29 Jul 2024 11:56:56 +0200 Subject: [PATCH] Merge pull request #19631 from overleaf/jpa-e2e-emails [server-pro] add e2e test for accepting project invite via email GitOrigin-RevId: c8391b57c1ee882499cfe5dc02817b5fadcd7ff4 --- server-ce/test/.gitignore | 1 + server-ce/test/Makefile | 13 +++++++--- server-ce/test/cypress/support/e2e.js | 5 ++++ server-ce/test/docker-compose.yml | 11 ++++++++ server-ce/test/git-bridge.spec.ts | 6 ++--- server-ce/test/helpers/email.ts | 36 ++++++++++++++++++++++++++ server-ce/test/helpers/project.ts | 34 +++++++++++++++++++++++- server-ce/test/project-sharing.spec.ts | 28 +++++++++++++++++--- 8 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 server-ce/test/helpers/email.ts diff --git a/server-ce/test/.gitignore b/server-ce/test/.gitignore index 8fce603003..56be476514 100644 --- a/server-ce/test/.gitignore +++ b/server-ce/test/.gitignore @@ -1 +1,2 @@ data/ +docker-mailtrap/ diff --git a/server-ce/test/Makefile b/server-ce/test/Makefile index fc773956f4..59e29ea198 100644 --- a/server-ce/test/Makefile +++ b/server-ce/test/Makefile @@ -12,14 +12,14 @@ export IMAGE_TAG_PRO ?= quay.io/sharelatex/sharelatex-pro:latest export CYPRESS_SHARD ?= export COMPOSE_PROJECT_NAME ?= test -test-e2e-native: +test-e2e-native: build_mailtrap docker compose -f docker-compose.yml -f docker-compose.native.yml up --build --no-log-prefix sharelatex host-admin -d CYPRESS_ADMIN_CLIENT_URL='http://localhost:8081' CYPRESS_GIT_BRIDGE_PUBLIC_HOST='localhost' CYPRESS_SAML_PUBLIC_HOST='localhost:8082' CYPRESS_OVERLEAF_PUBLIC_HOST='localhost:8082' npm run cypress:open test-e2e: docker compose up --build --no-log-prefix --exit-code-from=e2e e2e -test-e2e-open: +test-e2e-open: build_mailtrap docker compose up --build --no-log-prefix --exit-code-from=e2e-open e2e-open clean: @@ -32,7 +32,7 @@ prefetch_default_compose: prefetch_default: prefetch_default_compose_build prefetch_default_compose_build: - docker compose build + docker compose build host-admin prefetch: prefetch_custom prefetch_custom: prefetch_custom_compose_pull @@ -50,4 +50,11 @@ prefetch_old: docker pull $(IMAGE_TAG_PRO:latest=5.0.1-RC1) docker pull $(IMAGE_TAG_PRO:latest=5.0) +# Google Cloud Build runs on a very ancient Docker version that does not support the subdir flag. +# Use services -> mailtrap -> build -> context = https://github.com/dbck/docker-mailtrap.git#v1.5.0:build in docker-compose.yml eventually. +prefetch_default_compose_build: build_mailtrap +build_mailtrap: + git clone https://github.com/dbck/docker-mailtrap.git || true && cd docker-mailtrap && git checkout v1.5.0 + docker build -t mailtrap docker-mailtrap/build + .PHONY: test-e2e test-e2e-open diff --git a/server-ce/test/cypress/support/e2e.js b/server-ce/test/cypress/support/e2e.js index 960f202329..f8f1eac89c 100644 --- a/server-ce/test/cypress/support/e2e.js +++ b/server-ce/test/cypress/support/e2e.js @@ -2,6 +2,11 @@ import '@testing-library/cypress/add-commands' Cypress.on('uncaught:exception', (err, runnable) => { if (err.message.includes('ResizeObserver')) { + // spurious error from PDF preview + return false + } + if (err.message.includes('rcube_webmail')) { + // spurious error from mailtrap return false } }) diff --git a/server-ce/test/docker-compose.yml b/server-ce/test/docker-compose.yml index c9023ac934..16b65b2235 100644 --- a/server-ce/test/docker-compose.yml +++ b/server-ce/test/docker-compose.yml @@ -11,12 +11,18 @@ services: host-admin: # The host-admin service initiates the mongo replica set condition: service_healthy + mailtrap: + condition: service_started environment: OVERLEAF_SITE_URL: 'http://sharelatex' OVERLEAF_APP_NAME: Overleaf Community Edition OVERLEAF_MONGO_URL: mongodb://mongo/sharelatex?directConnection=true OVERLEAF_REDIS_HOST: redis REDIS_HOST: redis + OVERLEAF_EMAIL_FROM_ADDRESS: 'welcome@example.com' + OVERLEAF_EMAIL_SMTP_HOST: 'mailtrap' + OVERLEAF_EMAIL_SMTP_PORT: '25' + OVERLEAF_EMAIL_SMTP_IGNORE_TLS: 'true' ENABLED_LINKED_FILE_TYPES: 'project_file,project_output_file' ENABLE_CONVERSIONS: 'true' EMAIL_CONFIRMATION_DISABLED: 'true' @@ -26,6 +32,11 @@ services: timeout: 3s retries: 30 + mailtrap: + image: mailtrap + environment: + MAILTRAP_PASSWORD: 'password-for-mailtrap' + mongo: image: mongo:5.0.17 command: '--replSet overleaf' diff --git a/server-ce/test/git-bridge.spec.ts b/server-ce/test/git-bridge.spec.ts index 2025128e35..1c6bff4a42 100644 --- a/server-ce/test/git-bridge.spec.ts +++ b/server-ce/test/git-bridge.spec.ts @@ -4,7 +4,7 @@ import { ensureUserExists, login } from './helpers/login' import { createProject, enableLinkSharing, - shareProjectByEmailAndAcceptInvite, + shareProjectByEmailAndAcceptInviteViaDash, } from './helpers/project' import git from 'isomorphic-git' @@ -134,7 +134,7 @@ describe('git-bridge', function () { }) it('should expose r/w interface to invited r/w collaborator', () => { - shareProjectByEmailAndAcceptInvite( + shareProjectByEmailAndAcceptInviteViaDash( projectName, 'collaborator-rw@example.com', 'Can edit' @@ -146,7 +146,7 @@ describe('git-bridge', function () { }) it('should expose r/o interface to invited r/o collaborator', () => { - shareProjectByEmailAndAcceptInvite( + shareProjectByEmailAndAcceptInviteViaDash( projectName, 'collaborator-ro@example.com', 'Read only' diff --git a/server-ce/test/helpers/email.ts b/server-ce/test/helpers/email.ts new file mode 100644 index 0000000000..b5f059d6df --- /dev/null +++ b/server-ce/test/helpers/email.ts @@ -0,0 +1,36 @@ +/** + * Helper function for opening an email in Roundcube based mailtrap. + * We need to cross an origin boundary, which complicates the use of variables. + * Any variables need to be explicitly defined and the "runner" may only reference these and none from its scope. + * It is not possible to use Cypress helper functions, e.g. from the testing library inside the "runner". + * REF: https://github.com/testing-library/cypress-testing-library/issues/221 + */ +export function openEmail( + subject: string | RegExp, + runner: (frame: Cypress.Chainable>, args?: T) => void, + args?: T +) { + const runnerS = runner.toString() + cy.origin( + 'http://mailtrap', + { args: { args, runnerS, subject } }, + ({ args, runnerS, subject }) => { + cy.visit('/') + cy.get('input[name="_user"]').type('mailtrap') + cy.get('input[name="_pass"]').type('password-for-mailtrap') + cy.get('button[type="submit"]').click() + cy.log('mailtrap login is flaky in cypress, submit again') + cy.get('input[name="_pass"]').type('password-for-mailtrap') + cy.get('button[type="submit"]').click() + // Use force as the subject is partially hidden + cy.contains(subject).click({ force: true }) + cy.log('wait for iframe loading') + cy.wait(1000) + cy.get('iframe[id="messagecontframe"]').then(frame => { + // runnerS='(frame, args) => { runner body }'. Extract the runnable function. + const runner = new Function('return ' + runnerS)() + runner(cy.wrap(frame.prop('contentWindow').document.body), args) + }) + } + ) +} diff --git a/server-ce/test/helpers/project.ts b/server-ce/test/helpers/project.ts index eb8c4c86d6..c4d885a57f 100644 --- a/server-ce/test/helpers/project.ts +++ b/server-ce/test/helpers/project.ts @@ -1,4 +1,5 @@ import { login } from './login' +import { openEmail } from './email' export function createProject( name: string, @@ -23,7 +24,7 @@ export function createProject( .then(url => url.split('/').pop()) } -export function shareProjectByEmailAndAcceptInvite( +function shareProjectByEmail( projectName: string, email: string, level: 'Read only' | 'Can edit' @@ -38,6 +39,14 @@ export function shareProjectByEmailAndAcceptInvite( .within(() => cy.findByText('Can edit').parent().select(level)) cy.findByText('Share').click({ force: true }) }) +} + +export function shareProjectByEmailAndAcceptInviteViaDash( + projectName: string, + email: string, + level: 'Read only' | 'Can edit' +) { + shareProjectByEmail(projectName, email, level) login(email) cy.visit('/project') @@ -49,6 +58,29 @@ export function shareProjectByEmailAndAcceptInvite( }) } +export function shareProjectByEmailAndAcceptInviteViaEmail( + projectName: string, + email: string, + level: 'Read only' | 'Can edit' +) { + shareProjectByEmail(projectName, email, level) + + login(email) + + openEmail(projectName, frame => { + frame.contains('View project').then(a => { + cy.log( + 'bypass target=_blank and navigate current browser tab/cypress-iframe to project invite' + ) + cy.visit(a.attr('href')!) + }) + }) + cy.url().should('match', /\/project\/[a-f0-9]+\/invite\/token\/[a-f0-9]+/) + cy.findByText(/user would like you to join/) + cy.contains(new RegExp(`You are accepting this invite as ${email}`)) + cy.findByText('Join Project').click() +} + export function enableLinkSharing() { let linkSharingReadOnly: string let linkSharingReadAndWrite: string diff --git a/server-ce/test/project-sharing.spec.ts b/server-ce/test/project-sharing.spec.ts index ac7f1e6442..482b4be14e 100644 --- a/server-ce/test/project-sharing.spec.ts +++ b/server-ce/test/project-sharing.spec.ts @@ -4,7 +4,8 @@ import { ensureUserExists, login } from './helpers/login' import { createProject, enableLinkSharing, - shareProjectByEmailAndAcceptInvite, + shareProjectByEmailAndAcceptInviteViaDash, + shareProjectByEmailAndAcceptInviteViaEmail, } from './helpers/project' import { throttledRecompile } from './helpers/compile' import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry' @@ -143,13 +144,34 @@ describe('Project Sharing', function () { .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, + 'Read only' + ) + }) + + it('should grant the collaborator read access', () => { + cy.visit('/project') + cy.findByText(projectName).click() + expectFullReadOnlyAccess() + expectProjectDashboardEntry() + }) + }) + describe('read only', () => { const email = 'collaborator-ro@example.com' ensureUserExists({ email }) beforeWithReRunOnTestRetry(function () { login('user@example.com') - shareProjectByEmailAndAcceptInvite(projectName, email, 'Read only') + shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Read only') }) it('should grant the collaborator read access', () => { @@ -167,7 +189,7 @@ describe('Project Sharing', function () { beforeWithReRunOnTestRetry(function () { login('user@example.com') - shareProjectByEmailAndAcceptInvite(projectName, email, 'Can edit') + shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Can edit') }) it('should grant the collaborator write access', () => {