|
|
@@ -1,6 +1,11 @@
|
|
|
+import { v4 as uuid } from 'uuid'
|
|
|
import { isExcludedBySharding, startWith } from './helpers/config'
|
|
|
import { ensureUserExists, login } from './helpers/login'
|
|
|
-import { createProject } from './helpers/project'
|
|
|
+import {
|
|
|
+ createProject,
|
|
|
+ enableLinkSharing,
|
|
|
+ shareProjectByEmailAndAcceptInvite,
|
|
|
+} from './helpers/project'
|
|
|
|
|
|
import git from 'isomorphic-git'
|
|
|
import http from 'isomorphic-git/http/web'
|
|
|
@@ -32,6 +37,7 @@ describe('git-bridge', function () {
|
|
|
cy.findByText('Delete token').click()
|
|
|
})
|
|
|
}
|
|
|
+
|
|
|
function maybeClearAllTokens() {
|
|
|
cy.visit('/user/settings')
|
|
|
cy.findByText('Git Integration')
|
|
|
@@ -46,10 +52,10 @@ describe('git-bridge', function () {
|
|
|
|
|
|
beforeEach(function () {
|
|
|
login('user@example.com')
|
|
|
- maybeClearAllTokens()
|
|
|
})
|
|
|
|
|
|
it('should render the git-bridge UI in the settings', () => {
|
|
|
+ maybeClearAllTokens()
|
|
|
cy.visit('/user/settings')
|
|
|
cy.findByText('Git Integration')
|
|
|
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 () {
|
|
|
+ maybeClearAllTokens()
|
|
|
cy.visit('/project')
|
|
|
createProject('git').as('projectId')
|
|
|
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()
|
|
|
|
|
|
cy.get('header').findByText('Menu').click()
|
|
|
@@ -133,22 +205,18 @@ describe('git-bridge', function () {
|
|
|
// close editor menu
|
|
|
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 dir = `/${projectId}`
|
|
|
|
|
|
- async function readFile(path: string) {
|
|
|
+ async function readFile(path: string): Promise<string> {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
fs.readFile(path, { encoding: 'utf8' }, (err, blob) => {
|
|
|
if (err) return reject(err)
|
|
|
- resolve(blob)
|
|
|
+ resolve(blob as string)
|
|
|
})
|
|
|
})
|
|
|
}
|
|
|
+
|
|
|
async function writeFile(path: string, data: string) {
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
|
fs.writeFile(path, data, undefined, err => {
|
|
|
@@ -173,6 +241,7 @@ describe('git-bridge', function () {
|
|
|
author: { name: 'user', email: 'user@example.com' },
|
|
|
committer: { name: 'user', email: 'user@example.com' },
|
|
|
}
|
|
|
+ const mainTex = `${dir}/main.tex`
|
|
|
|
|
|
// Clone
|
|
|
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 = `
|
|
|
\\documentclass{article}
|
|
|
\\begin{document}
|
|
|
@@ -202,12 +278,37 @@ Hello world
|
|
|
...authorOptions,
|
|
|
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
|
|
|
cy.findByText(/\\documentclass/)
|
|
|
.parent()
|
|
|
@@ -250,7 +351,7 @@ Hello world
|
|
|
})
|
|
|
})
|
|
|
})
|
|
|
- })
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
function checkDisabled() {
|