Răsfoiți Sursa

Merge pull request #19608 from overleaf/jpa-git-bridge-e2e

[server-pro] extend e2e test coverage for git-access

GitOrigin-RevId: 3e6f3901037636140470b8169df224c329155598
Jakob Ackermann 2 ani în urmă
părinte
comite
7eacbe898e

+ 119 - 18
server-ce/test/git-bridge.spec.ts

@@ -1,6 +1,11 @@
+import { v4 as uuid } from 'uuid'
 import { isExcludedBySharding, startWith } from './helpers/config'
 import { isExcludedBySharding, startWith } from './helpers/config'
 import { ensureUserExists, login } from './helpers/login'
 import { ensureUserExists, login } from './helpers/login'
-import { createProject } from './helpers/project'
+import {
+  createProject,
+  enableLinkSharing,
+  shareProjectByEmailAndAcceptInvite,
+} from './helpers/project'
 
 
 import git from 'isomorphic-git'
 import git from 'isomorphic-git'
 import http from 'isomorphic-git/http/web'
 import http from 'isomorphic-git/http/web'
@@ -32,6 +37,7 @@ describe('git-bridge', function () {
         cy.findByText('Delete token').click()
         cy.findByText('Delete token').click()
       })
       })
     }
     }
+
     function maybeClearAllTokens() {
     function maybeClearAllTokens() {
       cy.visit('/user/settings')
       cy.visit('/user/settings')
       cy.findByText('Git Integration')
       cy.findByText('Git Integration')
@@ -46,10 +52,10 @@ describe('git-bridge', function () {
 
 
     beforeEach(function () {
     beforeEach(function () {
       login('user@example.com')
       login('user@example.com')
-      maybeClearAllTokens()
     })
     })
 
 
     it('should render the git-bridge UI in the settings', () => {
     it('should render the git-bridge UI in the settings', () => {
+      maybeClearAllTokens()
       cy.visit('/user/settings')
       cy.visit('/user/settings')
       cy.findByText('Git Integration')
       cy.findByText('Git Integration')
       cy.get('button').contains('Generate token').click()
       cy.get('button').contains('Generate token').click()
@@ -71,6 +77,7 @@ describe('git-bridge', function () {
     })
     })
 
 
     it('should render the git-bridge UI in the editor', function () {
     it('should render the git-bridge UI in the editor', function () {
+      maybeClearAllTokens()
       cy.visit('/project')
       cy.visit('/project')
       createProject('git').as('projectId')
       createProject('git').as('projectId')
       cy.get('header').findByText('Menu').click()
       cy.get('header').findByText('Menu').click()
@@ -106,9 +113,74 @@ describe('git-bridge', function () {
       })
       })
     })
     })
 
 
-    it('should expose interface for git', () => {
-      cy.visit('/project')
-      createProject('git').as('projectId')
+    describe('git access', () => {
+      ensureUserExists({ email: 'collaborator-rw@example.com' })
+      ensureUserExists({ email: 'collaborator-ro@example.com' })
+      ensureUserExists({ email: 'collaborator-link-rw@example.com' })
+      ensureUserExists({ email: 'collaborator-link-ro@example.com' })
+
+      let projectName: string
+      beforeEach(() => {
+        cy.visit('/project')
+        projectName = uuid()
+        createProject(projectName).as('projectId')
+      })
+
+      it('should expose r/w interface to owner', () => {
+        maybeClearAllTokens()
+        cy.visit('/project')
+        cy.findByText(projectName).click()
+        checkGitAccess('readAndWrite')
+      })
+
+      it('should expose r/w interface to invited r/w collaborator', () => {
+        shareProjectByEmailAndAcceptInvite(
+          projectName,
+          'collaborator-rw@example.com',
+          'Can edit'
+        )
+        maybeClearAllTokens()
+        cy.visit('/project')
+        cy.findByText(projectName).click()
+        checkGitAccess('readAndWrite')
+      })
+
+      it('should expose r/o interface to invited r/o collaborator', () => {
+        shareProjectByEmailAndAcceptInvite(
+          projectName,
+          'collaborator-ro@example.com',
+          'Read only'
+        )
+        maybeClearAllTokens()
+        cy.visit('/project')
+        cy.findByText(projectName).click()
+        checkGitAccess('readOnly')
+      })
+
+      it('should expose r/w interface to link-sharing r/w collaborator', () => {
+        enableLinkSharing().then(({ linkSharingReadAndWrite }) => {
+          login('collaborator-link-rw@example.com')
+          maybeClearAllTokens()
+          cy.visit(linkSharingReadAndWrite)
+          cy.findByText(projectName) // wait for lazy loading
+          cy.findByText('Join Project').click()
+          checkGitAccess('readAndWrite')
+        })
+      })
+
+      it('should expose r/o interface to link-sharing r/o collaborator', () => {
+        enableLinkSharing().then(({ linkSharingReadOnly }) => {
+          login('collaborator-link-ro@example.com')
+          maybeClearAllTokens()
+          cy.visit(linkSharingReadOnly)
+          cy.findByText(projectName) // wait for lazy loading
+          cy.findByText('Join Project').click()
+          checkGitAccess('readOnly')
+        })
+      })
+    })
+
+    function checkGitAccess(access: 'readOnly' | 'readAndWrite') {
       const recompile = throttledRecompile()
       const recompile = throttledRecompile()
 
 
       cy.get('header').findByText('Menu').click()
       cy.get('header').findByText('Menu').click()
@@ -133,22 +205,18 @@ describe('git-bridge', function () {
             // close editor menu
             // close editor menu
             cy.get('#left-menu-modal').click()
             cy.get('#left-menu-modal').click()
 
 
-            // check history
-            cy.findAllByText('History').last().click()
-            cy.findByText('(via Git)').should('not.exist')
-            cy.findAllByText('Back to editor').last().click()
-
             const fs = new LightningFS('fs')
             const fs = new LightningFS('fs')
             const dir = `/${projectId}`
             const dir = `/${projectId}`
 
 
-            async function readFile(path: string) {
+            async function readFile(path: string): Promise<string> {
               return new Promise((resolve, reject) => {
               return new Promise((resolve, reject) => {
                 fs.readFile(path, { encoding: 'utf8' }, (err, blob) => {
                 fs.readFile(path, { encoding: 'utf8' }, (err, blob) => {
                   if (err) return reject(err)
                   if (err) return reject(err)
-                  resolve(blob)
+                  resolve(blob as string)
                 })
                 })
               })
               })
             }
             }
+
             async function writeFile(path: string, data: string) {
             async function writeFile(path: string, data: string) {
               return new Promise<void>((resolve, reject) => {
               return new Promise<void>((resolve, reject) => {
                 fs.writeFile(path, data, undefined, err => {
                 fs.writeFile(path, data, undefined, err => {
@@ -173,6 +241,7 @@ describe('git-bridge', function () {
               author: { name: 'user', email: 'user@example.com' },
               author: { name: 'user', email: 'user@example.com' },
               committer: { name: 'user', email: 'user@example.com' },
               committer: { name: 'user', email: 'user@example.com' },
             }
             }
+            const mainTex = `${dir}/main.tex`
 
 
             // Clone
             // Clone
             cy.then({ timeout: 10_000 }, async () => {
             cy.then({ timeout: 10_000 }, async () => {
@@ -182,7 +251,14 @@ describe('git-bridge', function () {
               })
               })
             })
             })
 
 
-            const mainTex = `${dir}/main.tex`
+            cy.findByText(/\\documentclass/)
+              .parent()
+              .parent()
+              .then(async editor => {
+                const onDisk = await readFile(mainTex)
+                expect(onDisk.replaceAll('\n', '')).to.equal(editor.text())
+              })
+
             const text = `
             const text = `
 \\documentclass{article}
 \\documentclass{article}
 \\begin{document}
 \\begin{document}
@@ -202,12 +278,37 @@ Hello world
                 ...authorOptions,
                 ...authorOptions,
                 message: 'Swap main.tex',
                 message: 'Swap main.tex',
               })
               })
-              await git.push({
-                ...commonOptions,
-                ...httpOptions,
-              })
             })
             })
 
 
+            if (access === 'readAndWrite') {
+              // check history before push
+              cy.findAllByText('History').last().click()
+              cy.findByText('(via Git)').should('not.exist')
+              cy.findAllByText('Back to editor').last().click()
+
+              cy.then(async () => {
+                await git.push({
+                  ...commonOptions,
+                  ...httpOptions,
+                })
+              })
+            } else {
+              cy.then(async () => {
+                try {
+                  await git.push({
+                    ...commonOptions,
+                    ...httpOptions,
+                  })
+                  expect.fail('push should have failed')
+                } catch (err) {
+                  expect(err).to.match(/branches were not updated/)
+                  expect(err).to.match(/forbidden/)
+                }
+              })
+
+              return // return early, below are write access bits
+            }
+
             // check push in editor
             // check push in editor
             cy.findByText(/\\documentclass/)
             cy.findByText(/\\documentclass/)
               .parent()
               .parent()
@@ -250,7 +351,7 @@ Hello world
             })
             })
           })
           })
       })
       })
-    })
+    }
   })
   })
 
 
   function checkDisabled() {
   function checkDisabled() {

+ 52 - 0
server-ce/test/helpers/project.ts

@@ -1,3 +1,5 @@
+import { login } from './login'
+
 export function createProject(
 export function createProject(
   name: string,
   name: string,
   {
   {
@@ -20,3 +22,53 @@ export function createProject(
     .should('match', /\/project\/[a-fA-F0-9]{24}/)
     .should('match', /\/project\/[a-fA-F0-9]{24}/)
     .then(url => url.split('/').pop())
     .then(url => url.split('/').pop())
 }
 }
+
+export function shareProjectByEmailAndAcceptInvite(
+  projectName: string,
+  email: string,
+  level: 'Read only' | 'Can edit'
+) {
+  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()
+    })
+}
+
+export function enableLinkSharing() {
+  let linkSharingReadOnly: string
+  let linkSharingReadAndWrite: string
+
+  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()
+    })
+
+  return cy.then(() => {
+    return { linkSharingReadOnly, linkSharingReadAndWrite }
+  })
+}

+ 15 - 43
server-ce/test/project-sharing.spec.ts

@@ -1,7 +1,11 @@
 import { v4 as uuid } from 'uuid'
 import { v4 as uuid } from 'uuid'
 import { isExcludedBySharding, startWith } from './helpers/config'
 import { isExcludedBySharding, startWith } from './helpers/config'
 import { ensureUserExists, login } from './helpers/login'
 import { ensureUserExists, login } from './helpers/login'
-import { createProject } from './helpers/project'
+import {
+  createProject,
+  enableLinkSharing,
+  shareProjectByEmailAndAcceptInvite,
+} from './helpers/project'
 import { throttledRecompile } from './helpers/compile'
 import { throttledRecompile } from './helpers/compile'
 import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
 import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
 
 
@@ -36,46 +40,12 @@ describe('Project Sharing', function () {
     ).type('New Chat Message{enter}')
     ).type('New Chat Message{enter}')
 
 
     // Get link sharing links
     // 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()
-      })
+    enableLinkSharing().then(
+      ({ linkSharingReadOnly: ro, linkSharingReadAndWrite: rw }) => {
+        linkSharingReadAndWrite = rw
+        linkSharingReadOnly = ro
+      }
+    )
   }
   }
 
 
   function expectContentReadOnlyAccess() {
   function expectContentReadOnlyAccess() {
@@ -178,7 +148,8 @@ describe('Project Sharing', function () {
     ensureUserExists({ email })
     ensureUserExists({ email })
 
 
     beforeWithReRunOnTestRetry(function () {
     beforeWithReRunOnTestRetry(function () {
-      shareProjectByEmailAndAcceptInvite(email, 'Read only')
+      login('user@example.com')
+      shareProjectByEmailAndAcceptInvite(projectName, email, 'Read only')
     })
     })
 
 
     it('should grant the collaborator read access', () => {
     it('should grant the collaborator read access', () => {
@@ -195,7 +166,8 @@ describe('Project Sharing', function () {
     ensureUserExists({ email })
     ensureUserExists({ email })
 
 
     beforeWithReRunOnTestRetry(function () {
     beforeWithReRunOnTestRetry(function () {
-      shareProjectByEmailAndAcceptInvite(email, 'Can edit')
+      login('user@example.com')
+      shareProjectByEmailAndAcceptInvite(projectName, email, 'Can edit')
     })
     })
 
 
     it('should grant the collaborator write access', () => {
     it('should grant the collaborator write access', () => {