Merge pull request #19719 from overleaf/jpa-e2e-register

[server-pro] add e2e tests for registering users via GUI/script/email

GitOrigin-RevId: 25243532038c8df72f1360c433af215b3a551f3a
This commit is contained in:
Jakob Ackermann 2024-08-02 14:10:06 +02:00 committed by Copybot
parent fa8161d4ce
commit 05ecfa2e23
6 changed files with 129 additions and 18 deletions

View file

@ -1,10 +1,105 @@
import { isExcludedBySharding, startWith } from './helpers/config' import { isExcludedBySharding, startWith } from './helpers/config'
import { activateUser, ensureUserExists, login } from './helpers/login' import {
activateUser,
createMongoUser,
ensureUserExists,
login,
} from './helpers/login'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { createProject } from './helpers/project' import { createProject } from './helpers/project'
import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry' import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
import { openEmail } from './helpers/email'
describe('admin panel', function () { describe('admin panel', function () {
function registrationTests() {
it('via GUI and opening URL manually', () => {
const user = `${uuid()}@example.com`
cy.get('input[name="email"]').type(user + '{enter}')
cy.get('td')
.contains(/\/user\/activate/)
.then($td => {
const url = $td.text().trim()
activateUser(url)
})
})
it('via GUI and email', () => {
const user = `${uuid()}@example.com`
cy.get('input[name="email"]').type(user + '{enter}')
let url: string
cy.get('td')
.contains(/\/user\/activate/)
.then($td => {
url = $td.text().trim()
})
cy.then(() => {
openEmail(
'Activate your Overleaf Community Edition Account',
(frame, { url }) => {
frame.contains('Set password').then(el => {
expect(el.attr('href')!).to.equal(url)
})
},
{ url }
)
// Run activateUser in the main origin instead of inside openEmail. See docs on openEmail.
activateUser(url)
})
})
it('via script and opening URL manually', () => {
const user = `${uuid()}@example.com`
let url: string
cy.then(async () => {
;({ url } = await createMongoUser({ email: user }))
})
cy.then(() => {
activateUser(url)
})
})
it('via script and email', () => {
const user = `${uuid()}@example.com`
let url: string
cy.then(async () => {
;({ url } = await createMongoUser({ email: user }))
})
cy.then(() => {
openEmail(
'Activate your Overleaf Community Edition Account',
(frame, { url }) => {
frame.contains('Set password').then(el => {
expect(el.attr('href')!).to.equal(url)
})
},
{ url }
)
// Run activateUser in the main origin instead of inside openEmail. See docs on openEmail.
activateUser(url)
})
})
}
describe('in CE', () => {
if (isExcludedBySharding('CE_DEFAULT')) return
startWith({ pro: false, version: 'latest' })
const admin = 'admin@example.com'
const user = `user+${uuid()}@example.com`
ensureUserExists({ email: admin, isAdmin: true })
ensureUserExists({ email: user })
describe('create users', () => {
beforeEach(() => {
login(admin)
cy.visit('/project')
cy.get('nav').findByText('Admin').click()
cy.get('nav').findByText('Manage Users').click()
})
registrationTests()
})
})
describe('in server pro', () => { describe('in server pro', () => {
const admin = 'admin@example.com' const admin = 'admin@example.com'
const user1 = 'user@example.com' const user1 = 'user@example.com'
@ -83,18 +178,11 @@ describe('admin panel', function () {
cy.get('nav').findByText('Manage Users').click() cy.get('nav').findByText('Manage Users').click()
}) })
it('create and login user', () => { describe('create users', () => {
const user = `${uuid()}@example.com` beforeEach(() => {
cy.get('a').contains('New User').click() cy.get('a').contains('New User').click()
cy.get('input[name="email"]').type(user + '{enter}')
cy.get('td')
.contains(/\/user\/activate/)
.then($td => {
const url = $td.text().trim()
activateUser(url)
}) })
registrationTests()
}) })
it('user list RegExp search', () => { it('user list RegExp search', () => {

View file

@ -1,5 +1,5 @@
import { reconfigure } from './hostAdminClient' import { reconfigure } from './hostAdminClient'
import { resetCreatedUsersCache } from './login' import { resetActivateUserRateLimit, resetCreatedUsersCache } from './login'
export const STARTUP_TIMEOUT = export const STARTUP_TIMEOUT =
parseInt(Cypress.env('STARTUP_TIMEOUT'), 10) || 120_000 parseInt(Cypress.env('STARTUP_TIMEOUT'), 10) || 120_000
@ -39,6 +39,7 @@ export function startWith({
}) })
if (resetData) { if (resetData) {
resetCreatedUsersCache() resetCreatedUsersCache()
resetActivateUserRateLimit()
// no return here, always reconfigure when resetting data // no return here, always reconfigure when resetting data
} else if (lastConfig === cfg) { } else if (lastConfig === cfg) {
return return

View file

@ -2,12 +2,12 @@
* Helper function for opening an email in Roundcube based mailtrap. * Helper function for opening an email in Roundcube based mailtrap.
* We need to cross an origin boundary, which complicates the use of variables. * We need to cross an origin boundary, which complicates the use of variables.
* Any variables need to be explicitly defined and the "runner" may only reference these and none from its scope. * Any variables need to be explicitly defined and the "runner" may only reference these and none from its scope.
* It is not possible to use Cypress helper functions, e.g. from the testing library inside the "runner". * It is not possible to use Cypress helper functions, e.g. from the testing library or other functions like "activateUser", inside the "runner".
* REF: https://github.com/testing-library/cypress-testing-library/issues/221 * REF: https://github.com/testing-library/cypress-testing-library/issues/221
*/ */
export function openEmail<T>( export function openEmail<T>(
subject: string | RegExp, subject: string | RegExp,
runner: (frame: Cypress.Chainable<JQuery<any>>, args?: T) => void, runner: (frame: Cypress.Chainable<JQuery<any>>, args: T) => void,
args?: T args?: T
) { ) {
const runnerS = runner.toString() const runnerS = runner.toString()

View file

@ -21,7 +21,7 @@ export async function createMongoUser({
script: 'modules/server-ce-scripts/scripts/create-user.js', script: 'modules/server-ce-scripts/scripts/create-user.js',
args: [`--email=${email}`, `--admin=${isAdmin}`], args: [`--email=${email}`, `--admin=${isAdmin}`],
}) })
const [url] = stdout.match(/\/user\/activate\?token=\S+/)! const [url] = stdout.match(/http:\/\/.+\/user\/activate\?token=\S+/)!
const userId = new URL(url, location.origin).searchParams.get('user_id')! const userId = new URL(url, location.origin).searchParams.get('user_id')!
const signupDate = parseInt(userId.slice(0, 8), 16) const signupDate = parseInt(userId.slice(0, 8), 16)
if (signupDate < t0) { if (signupDate < t0) {
@ -70,7 +70,27 @@ export function login(username: string, password = DEFAULT_PASSWORD) {
return startOrResumeSession return startOrResumeSession
} }
let activateRateLimitState = { count: 0, reset: 0 }
export function resetActivateUserRateLimit() {
activateRateLimitState = { count: 0, reset: 0 }
}
function handleActivateUserRateLimit() {
cy.then(() => {
activateRateLimitState.count++
if (activateRateLimitState.reset < Date.now()) {
activateRateLimitState.reset = Date.now() + 65_000
activateRateLimitState.count = 1
} else if (activateRateLimitState.count >= 6) {
cy.wait(activateRateLimitState.reset - Date.now())
activateRateLimitState.count = 1
}
})
}
export function activateUser(url: string, password = DEFAULT_PASSWORD) { export function activateUser(url: string, password = DEFAULT_PASSWORD) {
handleActivateUserRateLimit()
cy.session(url, () => { cy.session(url, () => {
cy.visit(url) cy.visit(url)
cy.url().then(url => { cy.url().then(url => {

View file

@ -159,6 +159,7 @@ const allowedVars = Joi.object(
'OVERLEAF_LDAP_LAST_NAME_ATT', 'OVERLEAF_LDAP_LAST_NAME_ATT',
'OVERLEAF_LDAP_UPDATE_USER_DETAILS_ON_LOGIN', 'OVERLEAF_LDAP_UPDATE_USER_DETAILS_ON_LOGIN',
// Old branding, used for upgrade tests // Old branding, used for upgrade tests
'SHARELATEX_SITE_URL',
'SHARELATEX_MONGO_URL', 'SHARELATEX_MONGO_URL',
'SHARELATEX_REDIS_HOST', 'SHARELATEX_REDIS_HOST',
].map(name => [name, Joi.string()]) ].map(name => [name, Joi.string()])

View file

@ -145,7 +145,8 @@ describe('Upgrading', function () {
const optionsFourDotTwo = { const optionsFourDotTwo = {
version: '4.2', version: '4.2',
vars: { vars: {
// Add database vars with old branding // Add core vars with old branding
SHARELATEX_SITE_URL: 'http://sharelatex',
SHARELATEX_MONGO_URL: 'mongodb://mongo/sharelatex', SHARELATEX_MONGO_URL: 'mongodb://mongo/sharelatex',
SHARELATEX_REDIS_HOST: 'redis', SHARELATEX_REDIS_HOST: 'redis',
}, },