Ver código fonte

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

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

GitOrigin-RevId: c8391b57c1ee882499cfe5dc02817b5fadcd7ff4
Jakob Ackermann 2 anos atrás
pai
commit
8748ac7475

+ 1 - 0
server-ce/test/.gitignore

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

+ 10 - 3
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

+ 5 - 0
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
   }
 })

+ 11 - 0
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'

+ 3 - 3
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'

+ 36 - 0
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<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)
+      })
+    }
+  )
+}

+ 33 - 1
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

+ 25 - 3
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', () => {