diff --git a/server-ce/config/settings.js b/server-ce/config/settings.js index 2c13a22f74..6ee695f74e 100644 --- a/server-ce/config/settings.js +++ b/server-ce/config/settings.js @@ -403,76 +403,6 @@ if ( } } -// ###################### -// Overleaf Server Pro -// ###################### - -if (parse(process.env.OVERLEAF_IS_SERVER_PRO) === true) { - settings.bypassPercentageRollouts = true - settings.apis.references = { url: 'http://127.0.0.1:3040' } -} - -// Compiler -// -------- -if (process.env.SANDBOXED_COMPILES === 'true') { - settings.clsi = { - dockerRunner: true, - docker: { - image: process.env.TEX_LIVE_DOCKER_IMAGE, - env: { - HOME: '/tmp', - PATH: - process.env.COMPILER_PATH || - '/usr/local/texlive/2015/bin/x86_64-linux:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', - }, - user: 'www-data', - }, - } - - if (settings.path == null) { - settings.path = {} - } - settings.path.synctexBaseDir = () => '/compile' - if (process.env.SANDBOXED_COMPILES_SIBLING_CONTAINERS === 'true') { - console.log('Using sibling containers for sandboxed compiles') - if (process.env.SANDBOXED_COMPILES_HOST_DIR) { - settings.path.sandboxedCompilesHostDir = - process.env.SANDBOXED_COMPILES_HOST_DIR - } else { - console.error( - 'Sibling containers, but SANDBOXED_COMPILES_HOST_DIR not set' - ) - } - } -} - -// Templates -// --------- -if (process.env.OVERLEAF_TEMPLATES_USER_ID) { - settings.templates = { - mountPointUrl: '/templates', - user_id: process.env.OVERLEAF_TEMPLATES_USER_ID, - } - - settings.templateLinks = parse( - process.env.OVERLEAF_NEW_PROJECT_TEMPLATE_LINKS - ) -} - -// /Learn -// ------- -if (process.env.OVERLEAF_PROXY_LEARN != null) { - settings.proxyLearn = parse(process.env.OVERLEAF_PROXY_LEARN) - if (settings.proxyLearn) { - settings.nav.header_extras = [ - { - url: '/learn', - text: 'documentation', - }, - ].concat(settings.nav.header_extras || []) - } -} - // /References // ----------- if (process.env.OVERLEAF_ELASTICSEARCH_URL != null) { diff --git a/server-ce/test/Makefile b/server-ce/test/Makefile index 19dd67fb1c..5b44f30f18 100644 --- a/server-ce/test/Makefile +++ b/server-ce/test/Makefile @@ -6,6 +6,9 @@ all: test-e2e # Notable the container labels com.docker.compose.project.working_dir and com.docker.compose.project.config_files need to match when creating containers from the docker host (how you started things) and from host-admin (how tests reconfigure the instance). export PWD = $(shell pwd) +export TEX_LIVE_DOCKER_IMAGE ?= quay.io/sharelatex/texlive-full:2022.1 +export ALL_TEX_LIVE_DOCKER_IMAGES ?= quay.io/sharelatex/texlive-full:2022.1,quay.io/sharelatex/texlive-full:2021.1 + test-e2e: docker compose up --build --no-log-prefix --exit-code-from=e2e e2e @@ -18,5 +21,7 @@ clean: prefetch: docker compose pull e2e mongo redis docker compose build + echo -n "$$ALL_TEX_LIVE_DOCKER_IMAGES" | xargs -d, -I% \ + sh -exc 'tag=%; re_tag=quay.io/sharelatex/$${tag#*/}; docker pull $$tag; docker tag $$tag $$re_tag' .PHONY: test-e2e test-e2e-open diff --git a/server-ce/test/create-and-compile-project.spec.ts b/server-ce/test/create-and-compile-project.spec.ts index 8ac7f2f620..a1f70be0d8 100644 --- a/server-ce/test/create-and-compile-project.spec.ts +++ b/server-ce/test/create-and-compile-project.spec.ts @@ -1,6 +1,7 @@ import { ensureUserExists, login } from './helpers/login' import { createProject } from './helpers/project' import { startWith } from './helpers/config' +import { throttledRecompile } from './helpers/compile' describe('Project creation and compilation', function () { startWith({}) @@ -13,12 +14,10 @@ describe('Project creation and compilation', function () { // this is the first project created, the welcome screen is displayed instead of the project list createProject('test-project') cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/) + const recompile = throttledRecompile() cy.findByText('\\maketitle').parent().click() cy.findByText('\\maketitle').parent().type('\n\\section{{}Test Section}') - // Wait for the PDF compilation throttling - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(3000) - cy.findByText('Recompile').click() + recompile() cy.get('.pdf-viewer').should('contain.text', 'Test Section') }) diff --git a/server-ce/test/cypress/.gitignore b/server-ce/test/cypress/.gitignore index 9ab47dd2f2..0bdd0f1b2a 100644 --- a/server-ce/test/cypress/.gitignore +++ b/server-ce/test/cypress/.gitignore @@ -1,2 +1,3 @@ downloads/ results/ +compiles/ diff --git a/server-ce/test/cypress/support/e2e.js b/server-ce/test/cypress/support/e2e.js index 5aed0db505..960f202329 100644 --- a/server-ce/test/cypress/support/e2e.js +++ b/server-ce/test/cypress/support/e2e.js @@ -1 +1,7 @@ import '@testing-library/cypress/add-commands' + +Cypress.on('uncaught:exception', (err, runnable) => { + if (err.message.includes('ResizeObserver')) { + return false + } +}) diff --git a/server-ce/test/docker-compose.yml b/server-ce/test/docker-compose.yml index 3a7b85a9f7..aff84e10eb 100644 --- a/server-ce/test/docker-compose.yml +++ b/server-ce/test/docker-compose.yml @@ -14,6 +14,7 @@ services: # The host-admin service initiates the mongo replica set condition: service_healthy environment: + OVERLEAF_SITE_URL: 'http://sharelatex' OVERLEAF_APP_NAME: Overleaf Community Edition OVERLEAF_MONGO_URL: mongodb://mongo/sharelatex?directConnection=true OVERLEAF_REDIS_HOST: redis @@ -39,6 +40,16 @@ services: redis: image: redis:7.2.1 + git-bridge: + image: quay.io/sharelatex/git-bridge:latest + environment: + GIT_BRIDGE_API_BASE_URL: "http://sharelatex:3000/api/v0/" # "http://sharelatex/api/v0/" for version 4.1.6 and earlier + GIT_BRIDGE_OAUTH2_SERVER: "http://sharelatex" + GIT_BRIDGE_POSTBACK_BASE_URL: "http://git-bridge:8000" + GIT_BRIDGE_ROOT_DIR: "/data/git-bridge" + user: root + command: ["/server-pro-start.sh"] + e2e: image: cypress/included:13.6.6 stop_grace_period: 0s @@ -87,6 +98,8 @@ services: stop_grace_period: 0s environment: PWD: + TEX_LIVE_DOCKER_IMAGE: + ALL_TEX_LIVE_DOCKER_IMAGES: IMAGE_TAG_CE: ${IMAGE_TAG_CE:-sharelatex/sharelatex:latest} IMAGE_TAG_PRO: ${IMAGE_TAG_PRO:-quay.io/sharelatex/sharelatex-pro:latest} depends_on: diff --git a/server-ce/test/git-bridge.spec.ts b/server-ce/test/git-bridge.spec.ts new file mode 100644 index 0000000000..b26b175fdd --- /dev/null +++ b/server-ce/test/git-bridge.spec.ts @@ -0,0 +1,280 @@ +import { startWith } from './helpers/config' +import { ensureUserExists, login } from './helpers/login' +import { createProject } from './helpers/project' + +import git from 'isomorphic-git' +import http from 'isomorphic-git/http/web' +import LightningFS from '@isomorphic-git/lightning-fs' +import { throttledRecompile } from './helpers/compile' + +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', + } + + describe('enabled in Server Pro', function () { + startWith({ + pro: true, + vars: ENABLED_VARS, + }) + ensureUserExists({ email: 'user@example.com' }) + + function clearAllTokens() { + cy.get('button.linking-git-bridge-revoke-button').each(el => { + cy.wrap(el).click() + cy.findByText('Delete token').click() + }) + } + function maybeClearAllTokens() { + cy.visit('/user/settings') + cy.findByText('Git Integration') + cy.get('button') + .contains(/Generate token|Add another token/) + .then(btn => { + if (btn.text() === 'Add another token') { + clearAllTokens() + } + }) + } + + beforeEach(function () { + login('user@example.com') + maybeClearAllTokens() + }) + + it('should render the git-bridge UI in the settings', () => { + cy.visit('/user/settings') + cy.findByText('Git Integration') + cy.get('button').contains('Generate token').click() + cy.get('code') + .contains(/olp_[a-zA-Z0-9]{16}/) + .as('newToken') + cy.findAllByText('Close').last().click() + cy.get('@newToken').then(token => { + // There can be more than one token with the same prefix when retrying + cy.findAllByText( + `${token.text().slice(0, 'olp_1234'.length)}${'*'.repeat(12)}` + ).should('have.length.at.least', 1) + }) + cy.get('button').contains('Generate token').should('not.exist') + cy.get('button').contains('Add another token').should('exist') + clearAllTokens() + cy.get('button').contains('Generate token').should('exist') + cy.get('button').contains('Add another token').should('not.exist') + }) + + it('should render the git-bridge UI in the editor', function () { + cy.visit('/project') + createProject('git').as('projectId') + cy.get('header').findByText('Menu').click() + cy.findByText('Sync') + cy.findByText('Git').click() + cy.findByRole('dialog').within(() => { + cy.get('@projectId').then(id => { + cy.get('code').contains(`git clone http://git@sharelatex/git/${id}`) + }) + cy.findByRole('button', { + name: 'Generate token', + }).click() + cy.get('code').contains(/olp_[a-zA-Z0-9]{16}/) + }) + + // Re-open + cy.url().then(url => cy.visit(url)) + cy.get('header').findByText('Menu').click() + cy.findByText('Git').click() + cy.findByRole('dialog').within(() => { + cy.get('@projectId').then(id => { + cy.get('code').contains(`git clone http://git@sharelatex/git/${id}`) + }) + cy.findByText('Generate token').should('not.exist') + cy.findByText(/generate a new one in Account Settings/) + cy.findByText('Go to settings') + .should('have.attr', 'target', '_blank') + .and('have.attr', 'href', '/user/settings') + }) + }) + + it('should expose interface for git', () => { + cy.visit('/project') + createProject('git').as('projectId') + const recompile = throttledRecompile() + + cy.get('header').findByText('Menu').click() + cy.findByText('Sync') + cy.findByText('Git').click() + cy.get('@projectId').then(projectId => { + cy.findByRole('dialog').within(() => { + cy.get('code').contains( + `git clone http://git@sharelatex/git/${projectId}` + ) + }) + cy.findByRole('button', { + name: 'Generate token', + }).click() + cy.get('code') + .contains(/olp_[a-zA-Z0-9]{16}/) + .then(async tokenEl => { + const token = tokenEl.text() + + // close Git modal + cy.findAllByText('Close').last().click() + // 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) { + return new Promise((resolve, reject) => { + fs.readFile(path, { encoding: 'utf8' }, (err, blob) => { + if (err) return reject(err) + resolve(blob) + }) + }) + } + async function writeFile(path: string, data: string) { + return new Promise((resolve, reject) => { + fs.writeFile(path, data, undefined, err => { + if (err) return reject(err) + resolve() + }) + }) + } + + const commonOptions = { + dir, + fs, + } + const httpOptions = { + http, + url: `http://sharelatex/git/${projectId}`, + headers: { + Authorization: `Basic ${Buffer.from(`git:${token}`).toString('base64')}`, + }, + } + const authorOptions = { + author: { name: 'user', email: 'user@example.com' }, + committer: { name: 'user', email: 'user@example.com' }, + } + + // Clone + cy.then({ timeout: 10_000 }, async () => { + await git.clone({ + ...commonOptions, + ...httpOptions, + }) + }) + + const mainTex = `${dir}/main.tex` + 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', + }) + await git.push({ + ...commonOptions, + ...httpOptions, + }) + }) + + // 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.findAllByText('History').last().click() + cy.findAllByText('Back to editor').last().click() + + // check push in history + cy.findAllByText('History').last().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() + .click() + .type('% via editor{enter}') + + // Trigger flush via compile + recompile() + + // Back into the history, check what we just added + cy.findAllByText('History').last().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@example.com' }) + + it('should not render the git-bridge UI in the settings', () => { + login('user@example.com') + cy.visit('/user/settings') + cy.findByText('Git Integration').should('not.exist') + }) + it('should not render the git-bridge UI in the editor', function () { + login('user@example.com') + cy.visit('/project') + createProject('maybe git') + cy.get('header').findByText('Menu').click() + cy.findByText('Sync').should('not.exist') + cy.findByText('Git').should('not.exist') + }) + } + + describe('disabled in Server Pro', () => { + startWith({ + pro: true, + }) + checkDisabled() + }) + + describe('unavailable in CE', () => { + startWith({ + pro: false, + vars: ENABLED_VARS, + }) + checkDisabled() + }) +}) diff --git a/server-ce/test/helpers/compile.ts b/server-ce/test/helpers/compile.ts new file mode 100644 index 0000000000..826538312d --- /dev/null +++ b/server-ce/test/helpers/compile.ts @@ -0,0 +1,22 @@ +/** + * Helper function for throttling clicks on the recompile button to avoid hitting server side rate limits. + * The naive approach is waiting a fixed a mount of time (3s) just before clicking the button. + * This helper takes into account that other UI interactions take time. We can deduce that latency from the fixed delay (3s minus other latency). This can bring down the effective waiting time to 0s. + */ +export function throttledRecompile() { + let lastCompile = 0 + function queueReset() { + cy.then(() => { + lastCompile = Date.now() + }) + } + + queueReset() + return () => + cy.then(() => { + const msSinceLastCompile = Date.now() - lastCompile + cy.wait(Math.max(0, 3_000 - msSinceLastCompile)) + cy.findByText('Recompile').click() + queueReset() + }) +} diff --git a/server-ce/test/helpers/hostAdminClient.ts b/server-ce/test/helpers/hostAdminClient.ts index dec68f4f4c..da3b19bfa0 100644 --- a/server-ce/test/helpers/hostAdminClient.ts +++ b/server-ce/test/helpers/hostAdminClient.ts @@ -57,6 +57,8 @@ async function fetchJSON( const { error, stdout, stderr } = await res.json() if (error) { console.error(input, init, 'failed:', error) + if (stdout) console.log(stdout) + if (stderr) console.warn(stderr) const err = new Error(error.message) Object.assign(err, error) throw err diff --git a/server-ce/test/host-admin.js b/server-ce/test/host-admin.js index 54b6027c7c..f912014570 100644 --- a/server-ce/test/host-admin.js +++ b/server-ce/test/host-admin.js @@ -1,4 +1,5 @@ const fs = require('fs') +const Path = require('path') const { execFile } = require('child_process') const express = require('express') const bodyParser = require('body-parser') @@ -9,8 +10,9 @@ const { } = require('celebrate') const YAML = require('js-yaml') -const FILES = { - DOCKER_COMPOSE: 'docker-compose.override.yml', +const PATHS = { + DOCKER_COMPOSE_OVERRIDE: 'docker-compose.override.yml', + SANDBOXED_COMPILES_HOST_DIR: Path.join(__dirname, 'cypress/compiles'), } const IMAGES = { CE: process.env.IMAGE_TAG_CE.replace(/:.+/, ''), @@ -21,7 +23,7 @@ let mongoIsInitialized = false function readDockerComposeOverride() { try { - return YAML.load(fs.readFileSync(FILES.DOCKER_COMPOSE, 'utf-8')) + return YAML.load(fs.readFileSync(PATHS.DOCKER_COMPOSE_OVERRIDE, 'utf-8')) } catch (error) { if (error.code !== 'ENOENT') { throw error @@ -31,13 +33,14 @@ function readDockerComposeOverride() { sharelatex: { environment: {}, }, + 'git-bridge': {}, }, } } } function writeDockerComposeOverride(cfg) { - fs.writeFileSync(FILES.DOCKER_COMPOSE, YAML.dump(cfg)) + fs.writeFileSync(PATHS.DOCKER_COMPOSE_OVERRIDE, YAML.dump(cfg)) } const app = express() @@ -95,6 +98,7 @@ function setVersionDockerCompose({ pro, version }) { const cfg = readDockerComposeOverride() cfg.services.sharelatex.image = `${pro ? IMAGES.PRO : IMAGES.CE}:${version}` + cfg.services['git-bridge'].image = `quay.io/sharelatex/git-bridge:${version}` writeDockerComposeOverride(cfg) } @@ -128,17 +132,52 @@ app.post( } ) -const allowedVars = Joi.object().keys({ - OVERLEAF_APP_NAME: Joi.string(), - OVERLEAF_LEFT_FOOTER: Joi.string(), - OVERLEAF_RIGHT_FOOTER: Joi.string(), -}) +const allowedVars = Joi.object( + Object.fromEntries( + [ + 'OVERLEAF_APP_NAME', + 'OVERLEAF_LEFT_FOOTER', + 'OVERLEAF_RIGHT_FOOTER', + 'OVERLEAF_PROXY_LEARN', + 'GIT_BRIDGE_ENABLED', + 'GIT_BRIDGE_HOST', + 'GIT_BRIDGE_PORT', + 'V1_HISTORY_URL', + 'DOCKER_RUNNER', + 'SANDBOXED_COMPILES', + 'SANDBOXED_COMPILES_SIBLING_CONTAINERS', + 'ALL_TEX_LIVE_DOCKER_IMAGE_NAMES', + ].map(name => [name, Joi.string()]) + ) +) function setVarsDockerCompose({ vars }) { const cfg = readDockerComposeOverride() cfg.services.sharelatex.environment = vars + if (cfg.services.sharelatex.environment.GIT_BRIDGE_ENABLED === 'true') { + cfg.services.sharelatex.depends_on = ['git-bridge'] + } + + if ( + cfg.services.sharelatex.environment + .SANDBOXED_COMPILES_SIBLING_CONTAINERS === 'true' + ) { + cfg.services.sharelatex.environment.SANDBOXED_COMPILES_HOST_DIR = + PATHS.SANDBOXED_COMPILES_HOST_DIR + cfg.services.sharelatex.environment.TEX_LIVE_DOCKER_IMAGE = + process.env.TEX_LIVE_DOCKER_IMAGE + cfg.services.sharelatex.environment.ALL_TEX_LIVE_DOCKER_IMAGES = + process.env.ALL_TEX_LIVE_DOCKER_IMAGES + cfg.services.sharelatex.volumes = [ + '/var/run/docker.sock:/var/run/docker.sock', + `${PATHS.SANDBOXED_COMPILES_HOST_DIR}:/var/lib/overleaf/data/compiles`, + ] + } else { + cfg.services.sharelatex.volumes = [] + } + writeDockerComposeOverride(cfg) } @@ -182,6 +221,7 @@ app.post( '--timeout', '0', 'sharelatex', + 'git-bridge', 'mongo', 'redis' ), diff --git a/server-ce/test/learn-wiki.spec.ts b/server-ce/test/learn-wiki.spec.ts new file mode 100644 index 0000000000..75432eefe2 --- /dev/null +++ b/server-ce/test/learn-wiki.spec.ts @@ -0,0 +1,83 @@ +import { startWith } from './helpers/config' +import { ensureUserExists, login } from './helpers/login' + +describe('LearnWiki', function () { + const COPYING_A_PROJECT_URL = '/learn/how-to/Copying_a_project' + const UPLOADING_A_PROJECT_URL = '/learn/how-to/Uploading_a_project' + + describe('enabled in Pro', () => { + startWith({ + pro: true, + vars: { + OVERLEAF_PROXY_LEARN: 'true', + }, + }) + ensureUserExists({ email: 'user@example.com' }) + + it('should add a documentation entry to the nav bar', () => { + login('user@example.com') + cy.visit('/project') + cy.get('nav').findByText('Documentation') + }) + + it('should render wiki page', () => { + login('user@example.com') + cy.visit(UPLOADING_A_PROJECT_URL) + // Wiki content + cy.get('.page').findByText('Uploading a project') + cy.get('.page').contains(/how to create an Overleaf project/) + cy.get('img[alt="Creating a new project on Overleaf"]') + .should('be.visible') + .and((el: any) => { + expect(el[0].naturalWidth, 'renders image').to.be.greaterThan(0) + }) + // Wiki navigation + cy.get('.contents').findByText('Copying a project') + }) + + it('should navigate back and forth', function () { + login('user@example.com') + cy.visit(COPYING_A_PROJECT_URL) + cy.get('.page').findByText('Copying a project') + cy.get('.contents').findByText('Uploading a project').click() + cy.url().should('contain', UPLOADING_A_PROJECT_URL) + cy.get('.page').findByText('Uploading a project') + cy.get('.contents').findByText('Copying a project').click() + cy.url().should('contain', COPYING_A_PROJECT_URL) + cy.get('.page').findByText('Copying a project') + }) + }) + + describe('disabled in Pro', () => { + startWith({ pro: true }) + checkDisabled() + }) + + describe('unavailable in CE', () => { + startWith({ + pro: false, + vars: { + OVERLEAF_PROXY_LEARN: 'true', + }, + }) + checkDisabled() + }) + + function checkDisabled() { + ensureUserExists({ email: 'user@example.com' }) + + it('should not add a documentation entry to the nav bar', () => { + login('user@example.com') + cy.visit('/project') + cy.findByText('Documentation').should('not.exist') + }) + + it('should not render wiki page', () => { + login('user@example.com') + cy.visit(COPYING_A_PROJECT_URL, { + failOnStatusCode: false, + }) + cy.findByText('Not found') + }) + } +}) diff --git a/server-ce/test/package-lock.json b/server-ce/test/package-lock.json index 5cef80cf2a..41be17790d 100644 --- a/server-ce/test/package-lock.json +++ b/server-ce/test/package-lock.json @@ -6,11 +6,13 @@ "": { "name": "@overleaf/server-ce/test", "dependencies": { + "@isomorphic-git/lightning-fs": "^4.6.0", "@testing-library/cypress": "^10.0.1", "body-parser": "^1.20.2", "celebrate": "^15.0.3", "cypress": "13.6.6", "express": "^4.19.2", + "isomorphic-git": "^1.25.10", "js-yaml": "^4.1.0", "typescript": "^5.0.4" } @@ -238,6 +240,25 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@isomorphic-git/idb-keyval": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@isomorphic-git/idb-keyval/-/idb-keyval-3.3.2.tgz", + "integrity": "sha512-r8/AdpiS0/WJCNR/t/gsgL+M8NMVj/ek7s60uz3LmpCaTF2mEVlZJlB01ZzalgYzRLXwSPC92o+pdzjM7PN/pA==" + }, + "node_modules/@isomorphic-git/lightning-fs": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@isomorphic-git/lightning-fs/-/lightning-fs-4.6.0.tgz", + "integrity": "sha512-tfon8f1h6LawjFI/d8lZPWRPTxmdvyTMbkT/j5yo6dB0hALhKw5D9JsdCcUu/D1pAcMMiU7GZFDsDGqylerr7g==", + "dependencies": { + "@isomorphic-git/idb-keyval": "3.3.2", + "isomorphic-textencoder": "1.0.1", + "just-debounce-it": "1.1.0", + "just-once": "1.1.0" + }, + "bin": { + "superblocktxt": "src/superblocktxt.js" + } + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -469,6 +490,11 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -719,6 +745,11 @@ "node": ">=8" } }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -857,6 +888,17 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -972,6 +1014,20 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-equal": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", @@ -1054,6 +1110,11 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==" + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -1284,6 +1345,11 @@ "node >=0.6.0" ] }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -1649,6 +1715,14 @@ } ] }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "engines": { + "node": ">= 4" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -1976,6 +2050,46 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isomorphic-git": { + "version": "1.25.10", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.25.10.tgz", + "integrity": "sha512-IxGiaKBwAdcgBXwIcxJU6rHLk+NrzYaaPKXXQffcA0GW3IUrQXdUPDXDo+hkGVcYruuz/7JlGBiuaeTCgIgivQ==", + "dependencies": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^3.4.0", + "sha.js": "^2.4.9", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/isomorphic-git/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/isomorphic-textencoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-textencoder/-/isomorphic-textencoder-1.0.1.tgz", + "integrity": "sha512-676hESgHullDdHDsj469hr+7t3i/neBKU9J7q1T4RHaWwLAsaQnywC0D1dIUId0YZ+JtVrShzuBk1soo0+GVcQ==", + "dependencies": { + "fast-text-encoding": "^1.0.0" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -2049,6 +2163,16 @@ "verror": "1.10.0" } }, + "node_modules/just-debounce-it": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-1.1.0.tgz", + "integrity": "sha512-87Nnc0qZKgBZuhFZjYVjSraic0x7zwjhaTMrCKlj0QYKH6lh0KbFzVnfu6LHan03NO7J8ygjeBeD0epejn5Zcg==" + }, + "node_modules/just-once": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/just-once/-/just-once-1.1.0.tgz", + "integrity": "sha512-+rZVpl+6VyTilK7vB/svlMPil4pxqIJZkbnN7DKZTOzyXfun6ZiFeq2Pk4EtCEHZ0VU4EkdFzG8ZK5F3PErcDw==" + }, "node_modules/lazy-ass": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", @@ -2237,6 +2361,17 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2256,6 +2391,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "dependencies": { + "minimist": "^1.2.5" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2380,6 +2523,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2555,6 +2703,19 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", @@ -2753,6 +2914,18 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2790,6 +2963,49 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -2846,6 +3062,14 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3035,6 +3259,11 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/server-ce/test/package.json b/server-ce/test/package.json index 53315c320c..d3ea012dc4 100644 --- a/server-ce/test/package.json +++ b/server-ce/test/package.json @@ -9,11 +9,13 @@ "format:fix": "prettier --write $PWD/'**/*.{js,mjs,ts,tsx,json}'" }, "dependencies": { + "@isomorphic-git/lightning-fs": "^4.6.0", "@testing-library/cypress": "^10.0.1", "body-parser": "^1.20.2", "celebrate": "^15.0.3", "cypress": "13.6.6", "express": "^4.19.2", + "isomorphic-git": "^1.25.10", "js-yaml": "^4.1.0", "typescript": "^5.0.4" } diff --git a/server-ce/test/sandboxed-compiles.spec.ts b/server-ce/test/sandboxed-compiles.spec.ts new file mode 100644 index 0000000000..73959d2678 --- /dev/null +++ b/server-ce/test/sandboxed-compiles.spec.ts @@ -0,0 +1,79 @@ +import { ensureUserExists, login } from './helpers/login' +import { createProject } from './helpers/project' +import { startWith } from './helpers/config' +import { throttledRecompile } from './helpers/compile' + +describe('SandboxedCompiles', function () { + ensureUserExists({ email: 'user@example.com' }) + + const enabledVars = { + DOCKER_RUNNER: 'true', + SANDBOXED_COMPILES: 'true', + SANDBOXED_COMPILES_SIBLING_CONTAINERS: 'true', + ALL_TEX_LIVE_DOCKER_IMAGE_NAMES: '2023,2022', + } + + describe('enabled in Server Pro', () => { + startWith({ + pro: true, + vars: enabledVars, + }) + beforeEach(function () { + login('user@example.com') + }) + + it('should offer TexLive images and switch the compiler', () => { + cy.visit('/project') + createProject('sandboxed') + const recompile = throttledRecompile() + // check produced PDF + cy.get('.pdf-viewer').should('contain.text', 'sandboxed') + cy.get('[aria-label="View logs"]').click() + cy.findByText(/This is pdfTeX, Version .+ \(TeX Live 2023\) /) + cy.get('header').findByText('Menu').click() + cy.findByText('TeX Live version') + .parent() + .findByText('2023') + .parent() + .select('2022') + + // close editor menu + cy.get('#left-menu-modal').click() + + // Trigger compile with other TexLive version + recompile() + + cy.get('[aria-label="View logs"]').click() + cy.findByText(/This is pdfTeX, Version .+ \(TeX Live 2022\) /) + }) + }) + + function checkUsesDefaultCompiler() { + beforeEach(function () { + login('user@example.com') + }) + + it('should not offer TexLive images and use default compiler', () => { + cy.visit('/project') + createProject('sandboxed') + // check produced PDF + cy.get('.pdf-viewer').should('contain.text', 'sandboxed') + cy.get('[aria-label="View logs"]').click() + cy.findByText(/This is pdfTeX, Version .+ \(TeX Live 2024\) /) + cy.get('header').findByText('Menu').click() + cy.findByText('TeX Live version').should('not.exist') + }) + } + + describe('disabled in Server Pro', () => { + startWith({ pro: true }) + + checkUsesDefaultCompiler() + }) + + describe.skip('unavailable in CE', () => { + startWith({ pro: false, vars: enabledVars }) + + checkUsesDefaultCompiler() + }) +})