Merge pull request #19631 from overleaf/jpa-e2e-emails

[server-pro] add e2e test for accepting project invite via email

GitOrigin-RevId: c8391b57c1ee882499cfe5dc02817b5fadcd7ff4
This commit is contained in:
Jakob Ackermann 2024-07-29 11:56:56 +02:00 committed by Copybot
parent bd2a99aca7
commit 8748ac7475
8 changed files with 124 additions and 10 deletions

View file

@ -1 +1,2 @@
data/ data/
docker-mailtrap/

View file

@ -12,14 +12,14 @@ export IMAGE_TAG_PRO ?= quay.io/sharelatex/sharelatex-pro:latest
export CYPRESS_SHARD ?= export CYPRESS_SHARD ?=
export COMPOSE_PROJECT_NAME ?= test 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 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 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: test-e2e:
docker compose up --build --no-log-prefix --exit-code-from=e2e 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 docker compose up --build --no-log-prefix --exit-code-from=e2e-open e2e-open
clean: clean:
@ -32,7 +32,7 @@ prefetch_default_compose:
prefetch_default: prefetch_default_compose_build prefetch_default: prefetch_default_compose_build
prefetch_default_compose_build: prefetch_default_compose_build:
docker compose build docker compose build host-admin
prefetch: prefetch_custom prefetch: prefetch_custom
prefetch_custom: prefetch_custom_compose_pull 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.1-RC1)
docker pull $(IMAGE_TAG_PRO:latest=5.0) 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 .PHONY: test-e2e test-e2e-open

View file

@ -2,6 +2,11 @@ import '@testing-library/cypress/add-commands'
Cypress.on('uncaught:exception', (err, runnable) => { Cypress.on('uncaught:exception', (err, runnable) => {
if (err.message.includes('ResizeObserver')) { if (err.message.includes('ResizeObserver')) {
// spurious error from PDF preview
return false
}
if (err.message.includes('rcube_webmail')) {
// spurious error from mailtrap
return false return false
} }
}) })

View file

@ -11,12 +11,18 @@ services:
host-admin: host-admin:
# The host-admin service initiates the mongo replica set # The host-admin service initiates the mongo replica set
condition: service_healthy condition: service_healthy
mailtrap:
condition: service_started
environment: environment:
OVERLEAF_SITE_URL: 'http://sharelatex' OVERLEAF_SITE_URL: 'http://sharelatex'
OVERLEAF_APP_NAME: Overleaf Community Edition OVERLEAF_APP_NAME: Overleaf Community Edition
OVERLEAF_MONGO_URL: mongodb://mongo/sharelatex?directConnection=true OVERLEAF_MONGO_URL: mongodb://mongo/sharelatex?directConnection=true
OVERLEAF_REDIS_HOST: redis OVERLEAF_REDIS_HOST: redis
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' ENABLED_LINKED_FILE_TYPES: 'project_file,project_output_file'
ENABLE_CONVERSIONS: 'true' ENABLE_CONVERSIONS: 'true'
EMAIL_CONFIRMATION_DISABLED: 'true' EMAIL_CONFIRMATION_DISABLED: 'true'
@ -26,6 +32,11 @@ services:
timeout: 3s timeout: 3s
retries: 30 retries: 30
mailtrap:
image: mailtrap
environment:
MAILTRAP_PASSWORD: 'password-for-mailtrap'
mongo: mongo:
image: mongo:5.0.17 image: mongo:5.0.17
command: '--replSet overleaf' command: '--replSet overleaf'

View file

@ -4,7 +4,7 @@ import { ensureUserExists, login } from './helpers/login'
import { import {
createProject, createProject,
enableLinkSharing, enableLinkSharing,
shareProjectByEmailAndAcceptInvite, shareProjectByEmailAndAcceptInviteViaDash,
} from './helpers/project' } from './helpers/project'
import git from 'isomorphic-git' import git from 'isomorphic-git'
@ -134,7 +134,7 @@ describe('git-bridge', function () {
}) })
it('should expose r/w interface to invited r/w collaborator', () => { it('should expose r/w interface to invited r/w collaborator', () => {
shareProjectByEmailAndAcceptInvite( shareProjectByEmailAndAcceptInviteViaDash(
projectName, projectName,
'collaborator-rw@example.com', 'collaborator-rw@example.com',
'Can edit' 'Can edit'
@ -146,7 +146,7 @@ describe('git-bridge', function () {
}) })
it('should expose r/o interface to invited r/o collaborator', () => { it('should expose r/o interface to invited r/o collaborator', () => {
shareProjectByEmailAndAcceptInvite( shareProjectByEmailAndAcceptInviteViaDash(
projectName, projectName,
'collaborator-ro@example.com', 'collaborator-ro@example.com',
'Read only' 'Read only'

View file

@ -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<T>(
subject: string | RegExp,
runner: (frame: Cypress.Chainable<JQuery<any>>, 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)
})
}
)
}

View file

@ -1,4 +1,5 @@
import { login } from './login' import { login } from './login'
import { openEmail } from './email'
export function createProject( export function createProject(
name: string, name: string,
@ -23,7 +24,7 @@ export function createProject(
.then(url => url.split('/').pop()) .then(url => url.split('/').pop())
} }
export function shareProjectByEmailAndAcceptInvite( function shareProjectByEmail(
projectName: string, projectName: string,
email: string, email: string,
level: 'Read only' | 'Can edit' level: 'Read only' | 'Can edit'
@ -38,6 +39,14 @@ export function shareProjectByEmailAndAcceptInvite(
.within(() => cy.findByText('Can edit').parent().select(level)) .within(() => cy.findByText('Can edit').parent().select(level))
cy.findByText('Share').click({ force: true }) cy.findByText('Share').click({ force: true })
}) })
}
export function shareProjectByEmailAndAcceptInviteViaDash(
projectName: string,
email: string,
level: 'Read only' | 'Can edit'
) {
shareProjectByEmail(projectName, email, level)
login(email) login(email)
cy.visit('/project') 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() { export function enableLinkSharing() {
let linkSharingReadOnly: string let linkSharingReadOnly: string
let linkSharingReadAndWrite: string let linkSharingReadAndWrite: string

View file

@ -4,7 +4,8 @@ import { ensureUserExists, login } from './helpers/login'
import { import {
createProject, createProject,
enableLinkSharing, enableLinkSharing,
shareProjectByEmailAndAcceptInvite, shareProjectByEmailAndAcceptInviteViaDash,
shareProjectByEmailAndAcceptInviteViaEmail,
} from './helpers/project' } from './helpers/project'
import { throttledRecompile } from './helpers/compile' import { throttledRecompile } from './helpers/compile'
import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry' 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 .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', () => { describe('read only', () => {
const email = 'collaborator-ro@example.com' const email = 'collaborator-ro@example.com'
ensureUserExists({ email }) ensureUserExists({ email })
beforeWithReRunOnTestRetry(function () { beforeWithReRunOnTestRetry(function () {
login('user@example.com') login('user@example.com')
shareProjectByEmailAndAcceptInvite(projectName, email, 'Read only') shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Read only')
}) })
it('should grant the collaborator read access', () => { it('should grant the collaborator read access', () => {
@ -167,7 +189,7 @@ describe('Project Sharing', function () {
beforeWithReRunOnTestRetry(function () { beforeWithReRunOnTestRetry(function () {
login('user@example.com') login('user@example.com')
shareProjectByEmailAndAcceptInvite(projectName, email, 'Can edit') shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Can edit')
}) })
it('should grant the collaborator write access', () => { it('should grant the collaborator write access', () => {