mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
606f9eaec7
commit
7eacbe898e
3 changed files with 186 additions and 61 deletions
|
@ -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() {
|
||||||
|
|
|
@ -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 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Reference in a new issue