mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
[e2e] Tidy E2E tests after removal of old editor GitOrigin-RevId: 5f8194ec46a047f98c7af8bbcb60c41212ad4bd2
459 lines
15 KiB
TypeScript
459 lines
15 KiB
TypeScript
import { v4 as uuid } from 'uuid'
|
|
import { isExcludedBySharding, startWith } from './helpers/config'
|
|
import { ensureUserExists, login } from './helpers/login'
|
|
import {
|
|
createProject,
|
|
createProjectAndOpenInNewEditor,
|
|
enableLinkSharing,
|
|
openProjectByName,
|
|
openProjectViaLinkSharingAsUser,
|
|
shareProjectByEmailAndAcceptInviteViaDash,
|
|
} from './helpers/project'
|
|
|
|
import git from 'isomorphic-git'
|
|
import http from 'isomorphic-git/http/web'
|
|
import LightningFS from '@isomorphic-git/lightning-fs'
|
|
import { prepareWaitForNextCompileSlot } from './helpers/compile'
|
|
|
|
const USER = 'user@example.com'
|
|
|
|
describe('git-bridge', function () {
|
|
const ENABLED_VARS = {
|
|
GIT_BRIDGE_ENABLED: 'true',
|
|
GIT_BRIDGE_HOST: 'git-bridge',
|
|
GIT_BRIDGE_PORT: '8000',
|
|
V1_HISTORY_URL: 'http://sharelatex:3100/api',
|
|
}
|
|
|
|
function gitURL(projectId: string) {
|
|
const url = new URL(Cypress.config().baseUrl!)
|
|
url.username = 'git'
|
|
url.pathname = `/git/${projectId}`
|
|
return url
|
|
}
|
|
|
|
describe('enabled in Server Pro', function () {
|
|
if (isExcludedBySharding('PRO_CUSTOM_1')) return
|
|
startWith({
|
|
pro: true,
|
|
vars: ENABLED_VARS,
|
|
})
|
|
ensureUserExists({ email: USER })
|
|
|
|
function clearAllTokens() {
|
|
cy.findAllByRole('button', { name: 'Remove' })
|
|
.not('[disabled]')
|
|
.each($button => {
|
|
cy.wrap($button).click()
|
|
cy.findByRole('button', { name: 'Delete token' }).click()
|
|
})
|
|
cy.findByRole('dialog').should('not.exist')
|
|
}
|
|
|
|
function maybeClearAllTokens() {
|
|
cy.visit('/user/settings')
|
|
cy.findByRole('heading', { name: 'Git integration' })
|
|
cy.findByRole('button', {
|
|
name: /Generate token|Add another token/i,
|
|
}).then(btn => {
|
|
if (btn.text() === 'Add another token') {
|
|
clearAllTokens()
|
|
}
|
|
})
|
|
}
|
|
|
|
beforeEach(function () {
|
|
login(USER)
|
|
})
|
|
|
|
it('should render the git-bridge UI in the settings', function () {
|
|
maybeClearAllTokens()
|
|
cy.visit('/user/settings')
|
|
cy.findByRole('heading', { name: 'Git integration' })
|
|
cy.findByRole('button', {
|
|
name: 'Git integration Generate token',
|
|
}).click()
|
|
cy.findByRole('dialog').within(() => {
|
|
cy.findByLabelText('Git authentication token')
|
|
.contains(/olp_[a-zA-Z0-9]{16}/)
|
|
.then(el => el.text())
|
|
.as('newToken')
|
|
cy.findByRole('button', { name: 'Close dialog' }).click()
|
|
})
|
|
cy.get('@newToken').then(token => {
|
|
// There can be more than one token with the same prefix when retrying
|
|
cy.findAllByText(
|
|
`${token.slice(0, 'olp_1234'.length)}${'*'.repeat(12)}`
|
|
).should('have.length.at.least', 1)
|
|
})
|
|
cy.findByRole('button', {
|
|
name: 'Git integration Generate token',
|
|
}).should('not.exist')
|
|
cy.findByRole('button', { name: 'Add another token' }).should('exist')
|
|
clearAllTokens()
|
|
cy.findByRole('button', {
|
|
name: 'Git integration Generate token',
|
|
}).should('exist')
|
|
cy.findByRole('button', { name: 'Add another token' }).should('not.exist')
|
|
})
|
|
|
|
it('should render the git-bridge UI in the editor', function () {
|
|
maybeClearAllTokens()
|
|
createProjectAndOpenInNewEditor('git').as('projectId')
|
|
cy.findByRole('tab', { name: 'Integrations' }).click()
|
|
cy.findByText('Git clone this project.').click()
|
|
|
|
cy.findByTestId('git-bridge-modal').within(() => {
|
|
cy.get('@projectId').then(id => {
|
|
cy.findByLabelText('Git clone project command').contains(
|
|
`git clone ${gitURL(id.toString())}`
|
|
)
|
|
})
|
|
cy.findByRole('button', {
|
|
name: 'Generate token',
|
|
}).click()
|
|
cy.findByLabelText('Git authentication token').contains(
|
|
/olp_[a-zA-Z0-9]{16}/
|
|
)
|
|
})
|
|
|
|
// Re-open
|
|
cy.url().then(url => cy.visit(url))
|
|
cy.findByText('Git clone this project.').click()
|
|
|
|
cy.findByTestId('git-bridge-modal').within(() => {
|
|
cy.get('@projectId').then(id => {
|
|
cy.findByLabelText('Git clone project command').contains(
|
|
`git clone ${gitURL(id.toString())}`
|
|
)
|
|
})
|
|
cy.findByRole('button', {
|
|
name: 'Generate token',
|
|
}).should('not.exist')
|
|
cy.findByText(/generate a new one in Account settings/)
|
|
cy.findByRole('link', { name: 'Go to settings' })
|
|
.should('have.attr', 'target', '_blank')
|
|
.and('have.attr', 'href', '/user/settings')
|
|
})
|
|
})
|
|
|
|
describe('git access', function () {
|
|
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
|
|
let recompile: () => void
|
|
let waitForCompile: (triggerCompile: () => void) => void
|
|
beforeEach(function () {
|
|
projectName = uuid()
|
|
createProject(projectName, { open: false }).as('projectId')
|
|
;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
|
|
})
|
|
|
|
it('should expose r/w interface to owner', function () {
|
|
maybeClearAllTokens()
|
|
waitForCompile(() => {
|
|
openProjectByName(projectName, true)
|
|
})
|
|
checkGitAccess('readAndWrite')
|
|
})
|
|
|
|
it('should expose r/w interface to invited r/w collaborator', function () {
|
|
shareProjectByEmailAndAcceptInviteViaDash(
|
|
projectName,
|
|
'collaborator-rw@example.com',
|
|
'Editor',
|
|
true
|
|
)
|
|
maybeClearAllTokens()
|
|
waitForCompile(() => {
|
|
openProjectByName(projectName, true)
|
|
})
|
|
checkGitAccess('readAndWrite')
|
|
})
|
|
|
|
it('should expose r/o interface to invited r/o collaborator', function () {
|
|
shareProjectByEmailAndAcceptInviteViaDash(
|
|
projectName,
|
|
'collaborator-ro@example.com',
|
|
'Viewer',
|
|
true
|
|
)
|
|
maybeClearAllTokens()
|
|
waitForCompile(() => {
|
|
openProjectByName(projectName, true)
|
|
})
|
|
checkGitAccess('readOnly')
|
|
})
|
|
|
|
it('should expose r/w interface to link-sharing r/w collaborator', function () {
|
|
openProjectByName(projectName, true)
|
|
enableLinkSharing().then(({ linkSharingReadAndWrite }) => {
|
|
const email = 'collaborator-link-rw@example.com'
|
|
login(email)
|
|
maybeClearAllTokens()
|
|
waitForCompile(() => {
|
|
openProjectViaLinkSharingAsUser(
|
|
linkSharingReadAndWrite,
|
|
projectName,
|
|
email,
|
|
true
|
|
)
|
|
})
|
|
checkGitAccess('readAndWrite')
|
|
})
|
|
})
|
|
|
|
it('should expose r/o interface to link-sharing r/o collaborator', function () {
|
|
waitForCompile(() => {
|
|
openProjectByName(projectName, true)
|
|
})
|
|
enableLinkSharing().then(({ linkSharingReadOnly }) => {
|
|
const email = 'collaborator-link-ro@example.com'
|
|
login(email)
|
|
maybeClearAllTokens()
|
|
waitForCompile(() => {
|
|
openProjectViaLinkSharingAsUser(
|
|
linkSharingReadOnly,
|
|
projectName,
|
|
email,
|
|
true
|
|
)
|
|
})
|
|
checkGitAccess('readOnly')
|
|
})
|
|
})
|
|
|
|
function checkGitAccess(access: 'readOnly' | 'readAndWrite') {
|
|
cy.findByRole('tab', { name: 'Integrations' }).click()
|
|
cy.findByText('Git clone this project.').click()
|
|
|
|
cy.get('@projectId').then(projectId => {
|
|
cy.findByTestId('git-bridge-modal').within(() => {
|
|
cy.findByLabelText('Git clone project command').contains(
|
|
`git clone ${gitURL(projectId.toString())}`
|
|
)
|
|
cy.findByRole('heading', { name: 'Clone with Git' })
|
|
cy.findByRole('button', {
|
|
name: 'Generate token',
|
|
}).click()
|
|
})
|
|
cy.findByLabelText('Git authentication token')
|
|
.contains(/olp_[a-zA-Z0-9]{16}/)
|
|
.then(async tokenEl => {
|
|
const token = tokenEl.text()
|
|
|
|
// close Git modal
|
|
cy.findByRole('button', { name: 'Close dialog' }).click()
|
|
cy.findByTestId('git-bridge-modal').should('not.exist')
|
|
// close the modal
|
|
cy.get('body').type('{esc}')
|
|
cy.findByTestId('left-menu').should('not.exist')
|
|
const fs = new LightningFS('fs')
|
|
const dir = `/${projectId}`
|
|
|
|
async function readFile(path: string): Promise<string> {
|
|
return await new Promise((resolve, reject) => {
|
|
fs.readFile(path, { encoding: 'utf8' }, (err, blob) => {
|
|
if (err) return reject(err)
|
|
resolve(blob as string)
|
|
})
|
|
})
|
|
}
|
|
|
|
async function writeFile(path: string, data: string) {
|
|
return await new Promise<void>((resolve, reject) => {
|
|
fs.writeFile(path, data, undefined, err => {
|
|
if (err) return reject(err)
|
|
resolve()
|
|
})
|
|
})
|
|
}
|
|
|
|
const commonOptions = {
|
|
dir,
|
|
fs,
|
|
}
|
|
const url = gitURL(projectId.toString())
|
|
url.username = '' // basic auth is specified separately.
|
|
const httpOptions = {
|
|
http,
|
|
url: url.toString(),
|
|
headers: {
|
|
Authorization: `Basic ${Buffer.from(`git:${token}`).toString('base64')}`,
|
|
},
|
|
}
|
|
const authorOptions = {
|
|
author: { name: 'user', email: USER },
|
|
committer: { name: 'user', email: USER },
|
|
}
|
|
const mainTex = `${dir}/main.tex`
|
|
|
|
// Clone
|
|
cy.then({ timeout: 10_000 }, async () => {
|
|
await git.clone({
|
|
...commonOptions,
|
|
...httpOptions,
|
|
})
|
|
})
|
|
cy.findByText(/\\documentclass/)
|
|
.parent()
|
|
.parent()
|
|
.then(async editor => {
|
|
const onDisk = await readFile(mainTex)
|
|
expect(onDisk.replaceAll('\n', '')).to.equal(editor.text())
|
|
})
|
|
const text = `
|
|
\\documentclass{article}
|
|
\\begin{document}
|
|
Hello world
|
|
\\end{document}
|
|
`
|
|
|
|
// Make a change
|
|
cy.then(async () => {
|
|
await writeFile(mainTex, text)
|
|
await git.add({
|
|
...commonOptions,
|
|
filepath: 'main.tex',
|
|
})
|
|
await git.commit({
|
|
...commonOptions,
|
|
...authorOptions,
|
|
message: 'Swap main.tex',
|
|
})
|
|
})
|
|
|
|
if (access === 'readAndWrite') {
|
|
// check history before push
|
|
cy.findByRole('navigation', {
|
|
name: 'Project actions',
|
|
})
|
|
.findByRole('button', { name: 'History' })
|
|
.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
|
|
cy.findByText(/\\documentclass/)
|
|
.parent()
|
|
.parent()
|
|
.should('have.text', text.replaceAll('\n', ''))
|
|
|
|
// Wait for history sync - trigger flush by toggling the UI
|
|
cy.findByRole('navigation', {
|
|
name: 'Project actions',
|
|
})
|
|
.findByRole('button', { name: 'History' })
|
|
.click()
|
|
cy.findAllByText('Back to editor').last().click()
|
|
|
|
// check push in history
|
|
cy.findByRole('navigation', {
|
|
name: 'Project actions',
|
|
})
|
|
.findByRole('button', { name: 'History' })
|
|
.click()
|
|
cy.findByText(/Hello world/)
|
|
cy.findByText('(via Git)').should('exist')
|
|
|
|
// Back to the editor
|
|
cy.findAllByText('Back to editor').last().click()
|
|
cy.findByText(/\\documentclass/)
|
|
.parent()
|
|
.parent()
|
|
.as('editor')
|
|
.click()
|
|
cy.get('@editor').type('% via editor{enter}')
|
|
|
|
// Trigger flush via compile
|
|
recompile()
|
|
|
|
// Back into the history, check what we just added
|
|
cy.findByRole('navigation', {
|
|
name: 'Project actions',
|
|
})
|
|
.findByRole('button', { name: 'History' })
|
|
.click()
|
|
cy.findByText(/% via editor/)
|
|
|
|
// Pull the change
|
|
cy.then(async () => {
|
|
await git.pull({
|
|
...commonOptions,
|
|
...httpOptions,
|
|
...authorOptions,
|
|
})
|
|
|
|
expect(await readFile(mainTex)).to.equal(
|
|
text + '% via editor\n'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
})
|
|
})
|
|
|
|
function checkDisabled() {
|
|
ensureUserExists({ email: USER })
|
|
|
|
it('should not render the git-bridge UI in the settings', function () {
|
|
login(USER)
|
|
cy.visit('/user/settings')
|
|
cy.findByRole('heading', { name: 'Git integration' }).should('not.exist')
|
|
})
|
|
it('should not render the git-bridge UI in the editor', function () {
|
|
login(USER)
|
|
createProjectAndOpenInNewEditor('maybe git')
|
|
cy.findByRole('tab', {
|
|
name: 'File tree',
|
|
}).should('exist') // Wait for load
|
|
cy.findByRole('tab', {
|
|
name: 'Integrations',
|
|
}).should('not.exist')
|
|
})
|
|
}
|
|
|
|
describe('disabled in Server Pro', function () {
|
|
if (isExcludedBySharding('PRO_DEFAULT_1')) return
|
|
startWith({
|
|
pro: true,
|
|
})
|
|
checkDisabled()
|
|
})
|
|
|
|
describe('unavailable in CE', function () {
|
|
if (isExcludedBySharding('CE_CUSTOM_1')) return
|
|
startWith({
|
|
pro: false,
|
|
vars: ENABLED_VARS,
|
|
})
|
|
checkDisabled()
|
|
})
|
|
})
|