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

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

GitOrigin-RevId: 3e6f3901037636140470b8169df224c329155598
This commit is contained in:
Jakob Ackermann 2024-07-25 17:13:52 +02:00 committed by Copybot
parent 606f9eaec7
commit 7eacbe898e
3 changed files with 186 additions and 61 deletions

View file

@ -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', () => { describe('git access', () => {
cy.visit('/project') ensureUserExists({ email: 'collaborator-rw@example.com' })
createProject('git').as('projectId') 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() {

View file

@ -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 }
})
}

View file

@ -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() enableLinkSharing().then(
cy.findByText('Turn on link sharing').click() ({ linkSharingReadOnly: ro, linkSharingReadAndWrite: rw }) => {
cy.findByText('Anyone with this link can view this project') linkSharingReadAndWrite = rw
.next() linkSharingReadOnly = ro
.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() { 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', () => {