diff --git a/server-ce/test/git-bridge.spec.ts b/server-ce/test/git-bridge.spec.ts index b26b175fdd..ce32a57913 100644 --- a/server-ce/test/git-bridge.spec.ts +++ b/server-ce/test/git-bridge.spec.ts @@ -258,6 +258,7 @@ Hello world cy.visit('/project') createProject('maybe git') cy.get('header').findByText('Menu').click() + cy.findByText('Word Count') // wait for lazy loading cy.findByText('Sync').should('not.exist') cy.findByText('Git').should('not.exist') }) diff --git a/server-ce/test/helpers/config.ts b/server-ce/test/helpers/config.ts index b9aeb874f5..d1d411b113 100644 --- a/server-ce/test/helpers/config.ts +++ b/server-ce/test/helpers/config.ts @@ -2,8 +2,14 @@ import { reconfigure } from './hostAdminClient' let lastConfig: string -export function startWith({ pro = false, version = 'latest', vars = {} }) { +export function startWith({ + pro = false, + version = 'latest', + vars = {}, + varsFn = () => ({}), +}) { before(async function () { + Object.assign(vars, varsFn()) const cfg = JSON.stringify({ pro, version, vars }) if (lastConfig === cfg) return diff --git a/server-ce/test/helpers/login.ts b/server-ce/test/helpers/login.ts index 35911840c1..e701874ca1 100644 --- a/server-ce/test/helpers/login.ts +++ b/server-ce/test/helpers/login.ts @@ -52,13 +52,18 @@ export function ensureUserExists({ } export function login(username: string, password = DEFAULT_PASSWORD) { - cy.session([username, password, new Date()], () => { - cy.visit('/login') - cy.get('input[name="email"]').type(username) - cy.get('input[name="password"]').type(password) - cy.findByRole('button', { name: 'Login' }).click() - cy.url().should('contain', '/project') - }) + const id = [username, password, new Date()] + function startOrResumeSession() { + cy.session(id, () => { + cy.visit('/login') + cy.get('input[name="email"]').type(username) + cy.get('input[name="password"]').type(password) + cy.findByRole('button', { name: 'Login' }).click() + cy.url().should('contain', '/project') + }) + } + startOrResumeSession() + return startOrResumeSession } export function activateUser(url: string, password = DEFAULT_PASSWORD) { diff --git a/server-ce/test/host-admin.js b/server-ce/test/host-admin.js index f912014570..bf530e76a9 100644 --- a/server-ce/test/host-admin.js +++ b/server-ce/test/host-admin.js @@ -147,6 +147,8 @@ const allowedVars = Joi.object( 'SANDBOXED_COMPILES', 'SANDBOXED_COMPILES_SIBLING_CONTAINERS', 'ALL_TEX_LIVE_DOCKER_IMAGE_NAMES', + 'OVERLEAF_TEMPLATES_USER_ID', + 'OVERLEAF_NEW_PROJECT_TEMPLATE_LINKS', ].map(name => [name, Joi.string()]) ) ) diff --git a/server-ce/test/sandboxed-compiles.spec.ts b/server-ce/test/sandboxed-compiles.spec.ts index 73959d2678..c88b776bdd 100644 --- a/server-ce/test/sandboxed-compiles.spec.ts +++ b/server-ce/test/sandboxed-compiles.spec.ts @@ -61,6 +61,7 @@ describe('SandboxedCompiles', function () { cy.get('[aria-label="View logs"]').click() cy.findByText(/This is pdfTeX, Version .+ \(TeX Live 2024\) /) cy.get('header').findByText('Menu').click() + cy.findByText('Word Count') // wait for lazy loading cy.findByText('TeX Live version').should('not.exist') }) } diff --git a/server-ce/test/templates.spec.ts b/server-ce/test/templates.spec.ts new file mode 100644 index 0000000000..7f0308542c --- /dev/null +++ b/server-ce/test/templates.spec.ts @@ -0,0 +1,249 @@ +import { startWith } from './helpers/config' +import { ensureUserExists, login } from './helpers/login' +import { createProject } from './helpers/project' + +const WITHOUT_PROJECTS_USER = 'user-without-projects@example.com' +const ADMIN_USER = 'admin@example.com' +const REGULAR_USER = 'user@example.com' +const TEMPLATES_USER = 'templates@example.com' + +describe('Templates', () => { + ensureUserExists({ email: TEMPLATES_USER }) + ensureUserExists({ email: WITHOUT_PROJECTS_USER }) + + let OVERLEAF_TEMPLATES_USER_ID: string + before(function () { + login(TEMPLATES_USER) + cy.visit('/') + cy.get('meta[name="ol-user_id"]').then(el => { + OVERLEAF_TEMPLATES_USER_ID = el.attr('content')! + }) + }) + + function varsFn() { + return { + OVERLEAF_TEMPLATES_USER_ID, + OVERLEAF_NEW_PROJECT_TEMPLATE_LINKS: + '[{"name":"All Templates","url":"/templates/all"}]', + } + } + + describe('enabled in Server Pro', () => { + startWith({ + pro: true, + varsFn, + }) + ensureUserExists({ email: REGULAR_USER }) + ensureUserExists({ email: ADMIN_USER, isAdmin: true }) + + it('should show templates link on welcome page', () => { + login(WITHOUT_PROJECTS_USER) + cy.visit('/') + cy.findByText('Browse templates').click() + cy.url().should('match', /\/templates$/) + }) + + it('should have templates feature', () => { + const resumeTemplatesUserSession = login(TEMPLATES_USER) + const name = `Template ${Date.now()}` + const description = `Template Description ${Date.now()}` + + cy.visit('/') + createProject(name).as('templateProjectId') + + cy.get('header').findByText('Menu').click() + cy.findByText('Manage Template').click() + + cy.findByText('Template Description') + .click() + .parent() + .get('textarea') + .type(description) + cy.findByText('Publish').click() + cy.findByText('Publishing…').should('be.disabled') + cy.findByText('Publish').should('not.exist') + cy.findByText('Unpublish', { timeout: 10_000 }) + cy.findByText('Republish') + + cy.findByText('View it in the template gallery').click() + cy.url() + .should('match', /\/templates\/[a-f0-9]{24}$/) + .as('templateURL') + + cy.findAllByText(name).first().should('exist') + cy.findByText(description) + cy.findByText('Open as Template') + cy.findByText('Unpublish') + cy.findByText('Republish') + cy.get('img') + .should('have.attr', 'src') + .and('match', /\/v\/0\//) + cy.findByText('Republish').click() + cy.findByText('Publishing…').parent().should('be.disabled') + cy.findByText('Republish', { timeout: 10_000 }) + cy.get('img', { timeout: 10_000 }) + .should('have.attr', 'src') + .and('match', /\/v\/1\//) + + // custom tag + const tagName = `${Date.now()}` + cy.visit('/') + cy.findByText(name) + .parent() + .parent() + .within(() => cy.get('input[type="checkbox"]').first().check()) + cy.get('.project-list-sidebar-react').within(() => { + cy.findAllByText('New Tag').first().click() + }) + cy.focused().type(tagName) + cy.findByText('Create').click() + cy.get('.project-list-sidebar-react').within(() => { + cy.findByText(tagName) + .parent() + .within(() => cy.get('.name').should('have.text', `${tagName} (1)`)) + }) + + // Check listing + cy.visit('/templates') + cy.findByText(tagName) + cy.visit('/templates/all') + cy.findByText(name) + cy.visit(`/templates/${tagName}`) + cy.findByText(name) + + // Unpublish via template page + cy.get('@templateURL').then(url => cy.visit(`${url}`)) + cy.findByText('Unpublish').click() + cy.url().should('match', /\/templates$/) + cy.get('@templateURL').then(url => + cy.visit(`${url}`, { + failOnStatusCode: false, + }) + ) + cy.findByText('Not found') + cy.visit('/templates/all') + cy.findByText(name).should('not.exist') + cy.visit(`/templates/${tagName}`) + cy.findByText(name).should('not.exist') + + // Publish again + cy.get('@templateProjectId').then(projectId => + cy.visit(`/project/${projectId}`) + ) + cy.get('header').findByText('Menu').click() + cy.findByText('Manage Template').click() + cy.findByText('Publish').click() + cy.findByText('Unpublish', { timeout: 10_000 }) + + // Should assign a new template id + cy.findByText('View it in the template gallery').click() + cy.url() + .should('match', /\/templates\/[a-f0-9]{24}$/) + .as('newTemplateURL') + cy.get('@newTemplateURL').then(newURL => { + cy.get('@templateURL').then(prevURL => { + expect(newURL).to.match(/\/templates\/[a-f0-9]{24}$/) + expect(prevURL).to.not.equal(newURL) + }) + }) + + // Open project from template + login(REGULAR_USER) + cy.visit('/templates') + cy.findByText(tagName).click() + cy.findByText(name).click() + cy.findByText('Open as Template').click() + cy.url().should('match', /\/project\/[a-f0-9]{24}$/) + cy.get('.project-name').findByText(name) + cy.get('header').findByText('Menu').click() + cy.findByText('Word Count') // wait for lazy loading + cy.findByText('Manage Template').should('not.exist') + + // Check management as regular user + cy.get('@newTemplateURL').then(url => cy.visit(`${url}`)) + cy.findByText('Open as Template') + cy.findByText('Unpublish').should('not.exist') + cy.findByText('Republish').should('not.exist') + + // Check management as admin user + login(ADMIN_USER) + cy.get('@newTemplateURL').then(url => cy.visit(`${url}`)) + cy.findByText('Open as Template') + cy.findByText('Unpublish') + cy.findByText('Republish') + cy.get('@templateProjectId').then(projectId => + cy.visit(`/project/${projectId}`) + ) + cy.get('header').findByText('Menu').click() + cy.findByText('Manage Template').click() + cy.findByText('Unpublish') + + resumeTemplatesUserSession() + + // Unpublish via editor + cy.get('@templateProjectId').then(projectId => + cy.visit(`/project/${projectId}`) + ) + cy.get('header').findByText('Menu').click() + cy.findByText('Manage Template').click() + cy.findByText('Unpublish').click() + cy.findByText('Publish') + cy.visit('/templates/all') + cy.findByText(name).should('not.exist') + + // check for template links, after creating the first project + cy.visit('/') + cy.findAllByRole('button') + .contains(/new project/i) + .click() + cy.findAllByText('All Templates') + .first() + .should('have.attr', 'href', '/templates/all') + }) + }) + + function checkDisabled() { + it('should not have templates feature', () => { + login(TEMPLATES_USER) + + cy.visit('/') + createProject('maybe templates') + + cy.get('header').findByText('Menu').click() + cy.findByText('Word Count') // wait for lazy loading + cy.findByText('Manage Template').should('not.exist') + + cy.visit('/templates', { failOnStatusCode: false }) + cy.findByText('Not found') + cy.visit('/templates/all', { failOnStatusCode: false }) + cy.findByText('Not found') + + // check for template links, after creating the first project + cy.visit('/') + cy.findAllByRole('button') + .contains(/new project/i) + .click() + cy.findAllByText('All Templates').should('not.exist') + }) + + it('should not show templates link on welcome page', () => { + login(WITHOUT_PROJECTS_USER) + cy.visit('/') + cy.findByText(/new project/i) // wait for lazy loading + cy.findByText('Browse templates').should('not.exist') + }) + } + + describe('disabled Server Pro', () => { + startWith({ pro: true }) + checkDisabled() + }) + + describe('unavailable in CE', () => { + startWith({ + pro: false, + varsFn, + }) + checkDisabled() + }) +}) diff --git a/services/web/app/src/infrastructure/Features.js b/services/web/app/src/infrastructure/Features.js index ee77ac853d..2236461eda 100644 --- a/services/web/app/src/infrastructure/Features.js +++ b/services/web/app/src/infrastructure/Features.js @@ -65,7 +65,7 @@ const Features = { case 'oauth': return Boolean(Settings.oauth) case 'templates-server-pro': - return Boolean(Settings.templates) + return Boolean(Settings.templates?.user_id) case 'affiliations': case 'analytics': return Boolean(_.get(Settings, ['apis', 'v1', 'url']))