From a92bf982b03e9cf923a93cd458c1e7d2a8d73ec5 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Mon, 2 Mar 2026 14:53:56 +0000 Subject: [PATCH] Merge pull request #31889 from overleaf/ar-remove-web-smoke-test [web] remove smoke test GitOrigin-RevId: 7911b5e800ef466c59131fd739f95b11a587359f --- .../HealthCheck/HealthCheckController.mjs | 41 -------- services/web/app/src/router.mjs | 15 --- services/web/config/settings.defaults.js | 5 - .../src/HealthCheckControllerTests.mjs | 96 ------------------- services/web/test/smoke/src/README.md | 40 -------- services/web/test/smoke/src/SmokeTests.mjs | 95 ------------------ .../test/smoke/src/steps/000_getLoginCsrf.mjs | 7 -- .../smoke/src/steps/001_clearRateLimits.mjs | 35 ------- .../web/test/smoke/src/steps/002_login.mjs | 35 ------- .../src/steps/100_loadProjectDashboard.mjs | 14 --- .../test/smoke/src/steps/101_loadEditor.mjs | 16 ---- services/web/test/smoke/src/support/Csrf.mjs | 23 ----- .../web/test/smoke/src/support/Errors.mjs | 3 - .../test/smoke/src/support/requestHelper.mjs | 54 ----------- .../test/smoke/src/support/timeoutHelper.mjs | 14 --- services/web/test/smoke/tsconfig.json | 1 - services/web/tsconfig.backend.json | 2 - 17 files changed, 496 deletions(-) delete mode 100644 services/web/test/acceptance/src/HealthCheckControllerTests.mjs delete mode 100644 services/web/test/smoke/src/README.md delete mode 100644 services/web/test/smoke/src/SmokeTests.mjs delete mode 100644 services/web/test/smoke/src/steps/000_getLoginCsrf.mjs delete mode 100644 services/web/test/smoke/src/steps/001_clearRateLimits.mjs delete mode 100644 services/web/test/smoke/src/steps/002_login.mjs delete mode 100644 services/web/test/smoke/src/steps/100_loadProjectDashboard.mjs delete mode 100644 services/web/test/smoke/src/steps/101_loadEditor.mjs delete mode 100644 services/web/test/smoke/src/support/Csrf.mjs delete mode 100644 services/web/test/smoke/src/support/Errors.mjs delete mode 100644 services/web/test/smoke/src/support/requestHelper.mjs delete mode 100644 services/web/test/smoke/src/support/timeoutHelper.mjs delete mode 100644 services/web/test/smoke/tsconfig.json diff --git a/services/web/app/src/Features/HealthCheck/HealthCheckController.mjs b/services/web/app/src/Features/HealthCheck/HealthCheckController.mjs index 853da549b6..c5a3eb329c 100644 --- a/services/web/app/src/Features/HealthCheck/HealthCheckController.mjs +++ b/services/web/app/src/Features/HealthCheck/HealthCheckController.mjs @@ -2,23 +2,10 @@ import RedisWrapper from '../../infrastructure/RedisWrapper.mjs' import settings from '@overleaf/settings' import logger from '@overleaf/logger' import UserGetter from '../User/UserGetter.mjs' -import SmokeTests from './../../../../test/smoke/src/SmokeTests.mjs' - -const { SmokeTestFailure, runSmokeTests } = SmokeTests const rclient = RedisWrapper.client('health_check') export default { - check(req, res, next) { - if (!settings.siteIsOpen || !settings.editorIsOpen) { - // always return successful health checks when site is closed - res.sendStatus(200) - } else { - // detach from express for cleaner stack traces - setTimeout(() => runSmokeTestsDetached(req, res).catch(next)) - } - }, - checkActiveHandles(req, res, next) { if (!(settings.maxActiveHandles > 0) || !process._getActiveHandles) { return next() @@ -94,31 +81,3 @@ export default { ) }, } - -async function runSmokeTestsDetached(req, res) { - function isAborted() { - return req.destroyed - } - const stats = { start: new Date(), steps: [] } - let status, response - try { - try { - await runSmokeTests({ isAborted, stats }) - } finally { - stats.end = new Date() - stats.duration = stats.end - stats.start - } - status = 200 - response = { stats } - } catch (e) { - let err = e - if (!(e instanceof SmokeTestFailure)) { - err = new SmokeTestFailure('low level error', {}, e) - } - logger.err({ err, stats }, 'health check failed') - status = 500 - response = { stats, error: err.message } - } - if (isAborted()) return - res.status(status).json(response) -} diff --git a/services/web/app/src/router.mjs b/services/web/app/src/router.mjs index fca9ac065e..10ed348a6c 100644 --- a/services/web/app/src/router.mjs +++ b/services/web/app/src/router.mjs @@ -1128,11 +1128,6 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) { plainTextResponse(res, res.locals.csrfToken) }) - publicApiRouter.get( - '/health_check', - HealthCheckController.checkActiveHandles, - HealthCheckController.check - ) privateApiRouter.get( '/health_check', HealthCheckController.checkActiveHandles, @@ -1148,16 +1143,6 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) { HealthCheckController.checkActiveHandles, HealthCheckController.checkApi ) - publicApiRouter.get( - '/health_check/full', - HealthCheckController.checkActiveHandles, - HealthCheckController.check - ) - privateApiRouter.get( - '/health_check/full', - HealthCheckController.checkActiveHandles, - HealthCheckController.check - ) publicApiRouter.get('/health_check/redis', HealthCheckController.checkRedis) privateApiRouter.get('/health_check/redis', HealthCheckController.checkRedis) diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index dd0553820f..14be7ca372 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -773,12 +773,7 @@ module.exports = { // some basic smoke tests to check the core functionality. // smokeTest: { - user: process.env.SMOKE_TEST_USER, userId: process.env.SMOKE_TEST_USER_ID, - password: process.env.SMOKE_TEST_PASSWORD, - projectId: process.env.SMOKE_TEST_PROJECT_ID, - rateLimitSubject: process.env.SMOKE_TEST_RATE_LIMIT_SUBJECT || '127.0.0.1', - stepTimeout: parseInt(process.env.SMOKE_TEST_STEP_TIMEOUT || '10000', 10), }, appName: process.env.APP_NAME || 'Overleaf (Community Edition)', diff --git a/services/web/test/acceptance/src/HealthCheckControllerTests.mjs b/services/web/test/acceptance/src/HealthCheckControllerTests.mjs deleted file mode 100644 index a05b7aef21..0000000000 --- a/services/web/test/acceptance/src/HealthCheckControllerTests.mjs +++ /dev/null @@ -1,96 +0,0 @@ -import { expect } from 'chai' -import Settings from '@overleaf/settings' -import UserHelper from './helpers/User.mjs' - -const User = UserHelper.promises - -describe('HealthCheckController', function () { - describe('SmokeTests', function () { - let user, projectId - const captchaDisabledBefore = Settings.recaptcha.disabled.login - - beforeEach(async function () { - user = new User() - await user.login() - projectId = await user.createProject('SmokeTest') - - // HACK: Inject the details into the app - Settings.smokeTest.userId = user.id - Settings.smokeTest.user = user.email - Settings.smokeTest.password = user.password - Settings.smokeTest.projectId = projectId - - Settings.recaptcha.disabled.login = true - }) - afterEach(function () { - Settings.recaptcha.disabled.login = captchaDisabledBefore - }) - - async function performSmokeTestRequest() { - const start = Date.now() - const { response, body } = await user.doRequest('GET', { - url: '/health_check/full', - json: true, - }) - const end = Date.now() - - expect(body).to.exist - expect(body.stats).to.exist - expect(Date.parse(body.stats.start)).to.be.within(start, start + 1000) - expect(Date.parse(body.stats.end)).to.be.within(end - 1000, end) - - expect(body.stats.duration).to.be.within(0, 10000) - expect(body.stats.steps).to.be.instanceof(Array) - return { response, body } - } - - describe('happy path', function () { - it('should respond with a 200 and stats', async function () { - const { response, body } = await performSmokeTestRequest() - - expect(body.error).to.not.exist - expect(response.statusCode).to.equal(200) - }) - }) - - describe('when the request is aborted', function () { - it('should not crash', async function () { - try { - await user.doRequest('GET', { - timeout: 1, - url: '/health_check/full', - json: true, - }) - } catch (err) { - expect(err.code).to.be.oneOf(['ETIMEDOUT', 'ESOCKETTIMEDOUT']) - return - } - expect.fail('expected request to fail with timeout error') - }) - }) - - describe('when the project does not exist', function () { - beforeEach(function () { - Settings.smokeTest.projectId = '404' - }) - it('should respond with a 500 ', async function () { - const { response, body } = await performSmokeTestRequest() - - expect(body.error).to.equal('run.101_loadEditor failed') - expect(response.statusCode).to.equal(500) - }) - }) - - describe('when the password mismatches', function () { - beforeEach(function () { - Settings.smokeTest.password = 'foo-bar' - }) - it('should respond with a 500 with mismatching password', async function () { - const { response, body } = await performSmokeTestRequest() - - expect(body.error).to.equal('run.002_login failed') - expect(response.statusCode).to.equal(500) - }) - }) - }) -}) diff --git a/services/web/test/smoke/src/README.md b/services/web/test/smoke/src/README.md deleted file mode 100644 index c9b918462b..0000000000 --- a/services/web/test/smoke/src/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# SmokeTests - -For the SmokeTests we implemented a Mini-Framework that is tailored for our -tooling, specifically OError, and does not need a large runner, such as mocha. - -The SmokeTests are separated into individual `steps`. -Each `step` can have a `run` function and a `cleanup` function. -The former will run in sequence with the other steps, the later in reverse -order from the finish, or the last failure. - -```js -async function run(ctx) { - // do something -} -async function cleanup(ctx) { - // cleanup something -} -module.exports = { cleanup, run } -``` - -Steps will get called with a context object with common helpers and details: - -- `request` a promisified `request` module with defaults for `baseUrl`, - `timeout` and internals for cookie handling. -- `assertHasStatusCode` a helper for asserting response status codes, pass - a response and desired status code. It will throw with OError context set. -- `getCsrfTokenFor` a helper for retrieving CSRF tokens, pass an endpoint. -- `processWithTimeout` a helper for awaiting Promises with a timeout, pass - `{ work: Promise.resolve(), timeout: 42, message: 'foo timedout' }` -- `stats` an object for performance tracking. -- `timeout` the step timeout - -Steps should handle timeouts locally to ensure appropriate cleanup of timed out -actions. - -Steps may pass values along to the next steps in returning an object with the -desired fields from the `run` or `cleanup` function. -The returned values will overwrite existing details in the `ctx`. - -Alpha-numeric sorting of step filenames determines the processing sequence. diff --git a/services/web/test/smoke/src/SmokeTests.mjs b/services/web/test/smoke/src/SmokeTests.mjs deleted file mode 100644 index 7da26f8b9a..0000000000 --- a/services/web/test/smoke/src/SmokeTests.mjs +++ /dev/null @@ -1,95 +0,0 @@ -import fs from 'fs' -import Path from 'path' -import Settings from '@overleaf/settings' -import { getCsrfTokenForFactory } from './support/Csrf.mjs' -import { SmokeTestFailure } from './support/Errors.mjs' -import { - requestFactory, - assertHasStatusCode, -} from './support/requestHelper.mjs' -import { processWithTimeout } from './support/timeoutHelper.mjs' - -const STEP_TIMEOUT = Settings.smokeTest.stepTimeout - -const PATH_STEPS = Path.join(import.meta.dirname, './steps') -const sortedSteps = fs.readdirSync(PATH_STEPS).sort() - -const STEPS = [] - -for (const name of sortedSteps) { - const step = (await import(Path.join(PATH_STEPS, name))).default - step.name = Path.basename(name, '.mjs') - STEPS.push(step) -} - -async function runSmokeTests({ isAborted, stats }) { - let lastStep = stats.start - function completeStep(key) { - const step = Date.now() - stats.steps.push({ [key]: step - lastStep }) - lastStep = step - } - - const request = requestFactory({ timeout: STEP_TIMEOUT }) - const getCsrfTokenFor = getCsrfTokenForFactory({ request }) - const ctx = { - assertHasStatusCode, - getCsrfTokenFor, - processWithTimeout, - request, - stats, - timeout: STEP_TIMEOUT, - } - const cleanupSteps = [] - - async function runAndTrack(id, fn) { - let result - try { - result = await fn(ctx) - } catch (e) { - throw new SmokeTestFailure(`${id} failed`, {}, e) - } finally { - completeStep(id) - } - Object.assign(ctx, result) - } - - completeStep('init') - - let err - try { - for (const step of STEPS) { - if (isAborted()) break - - const { name, run, cleanup } = step - if (cleanup) cleanupSteps.unshift({ name, cleanup }) - - await runAndTrack(`run.${name}`, run) - } - } catch (e) { - err = e - } - - const cleanupErrors = [] - for (const step of cleanupSteps) { - const { name, cleanup } = step - - try { - await runAndTrack(`cleanup.${name}`, cleanup) - } catch (e) { - // keep going with cleanup - cleanupErrors.push(e) - } - } - - if (err) throw err - if (cleanupErrors.length) { - if (cleanupErrors.length === 1) throw cleanupErrors[0] - throw new SmokeTestFailure('multiple cleanup steps failed', { - stats, - cleanupErrors, - }) - } -} - -export default { runSmokeTests, SmokeTestFailure } diff --git a/services/web/test/smoke/src/steps/000_getLoginCsrf.mjs b/services/web/test/smoke/src/steps/000_getLoginCsrf.mjs deleted file mode 100644 index 9b2bac49f7..0000000000 --- a/services/web/test/smoke/src/steps/000_getLoginCsrf.mjs +++ /dev/null @@ -1,7 +0,0 @@ -async function run({ getCsrfTokenFor }) { - const loginCsrfToken = await getCsrfTokenFor('/login') - - return { loginCsrfToken } -} - -export default { run } diff --git a/services/web/test/smoke/src/steps/001_clearRateLimits.mjs b/services/web/test/smoke/src/steps/001_clearRateLimits.mjs deleted file mode 100644 index befbe0ec28..0000000000 --- a/services/web/test/smoke/src/steps/001_clearRateLimits.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import Settings from '@overleaf/settings' -import { - overleafLoginRateLimiter, - openProjectRateLimiter, -} from '../../../../app/src/infrastructure/RateLimiter.mjs' -import LoginRateLimiter from '../../../../app/src/Features/Security/LoginRateLimiter.mjs' - -async function clearLoginRateLimit() { - await LoginRateLimiter.promises.recordSuccessfulLogin(Settings.smokeTest.user) -} - -async function clearOverleafLoginRateLimit() { - if (!Settings.overleaf) return - await overleafLoginRateLimiter.delete(Settings.smokeTest.rateLimitSubject) -} - -async function clearOpenProjectRateLimit() { - await openProjectRateLimiter.delete( - `${Settings.smokeTest.projectId}:${Settings.smokeTest.userId}` - ) -} - -async function run({ processWithTimeout, timeout }) { - await processWithTimeout({ - work: Promise.all([ - clearLoginRateLimit(), - clearOverleafLoginRateLimit(), - clearOpenProjectRateLimit(), - ]), - timeout, - message: 'cleanupRateLimits timed out', - }) -} - -export default { run } diff --git a/services/web/test/smoke/src/steps/002_login.mjs b/services/web/test/smoke/src/steps/002_login.mjs deleted file mode 100644 index 5738c09ad2..0000000000 --- a/services/web/test/smoke/src/steps/002_login.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import Settings from '@overleaf/settings' - -async function run({ assertHasStatusCode, loginCsrfToken, request }) { - const response = await request('/login', { - method: 'POST', - json: { - _csrf: loginCsrfToken, - email: Settings.smokeTest.user, - password: Settings.smokeTest.password, - }, - }) - - const body = response.body - // login success and login failure both receive a status code of 200 - // see the frontend logic on how to handle the response: - // frontend/js/directives/asyncForm.js -> submitRequest - if (body && body.message && body.message.type === 'error') { - throw new Error(`login failed: ${body.message.text}`) - } - - assertHasStatusCode(response, 200) -} - -async function cleanup({ assertHasStatusCode, getCsrfTokenFor, request }) { - const logoutCsrfToken = await getCsrfTokenFor('/project') - const response = await request('/logout', { - method: 'POST', - headers: { - 'X-CSRF-Token': logoutCsrfToken, - }, - }) - assertHasStatusCode(response, 302) -} - -export default { cleanup, run } diff --git a/services/web/test/smoke/src/steps/100_loadProjectDashboard.mjs b/services/web/test/smoke/src/steps/100_loadProjectDashboard.mjs deleted file mode 100644 index a281b23b1f..0000000000 --- a/services/web/test/smoke/src/steps/100_loadProjectDashboard.mjs +++ /dev/null @@ -1,14 +0,0 @@ -const TITLE_REGEX = - /]*>Your projects - .*, Online LaTeX Editor<\/title>/ - -async function run({ request, assertHasStatusCode }) { - const response = await request('/project') - - assertHasStatusCode(response, 200) - - if (!TITLE_REGEX.test(response.body)) { - throw new Error('body does not have correct title') - } -} - -export default { run } diff --git a/services/web/test/smoke/src/steps/101_loadEditor.mjs b/services/web/test/smoke/src/steps/101_loadEditor.mjs deleted file mode 100644 index d5b2d56821..0000000000 --- a/services/web/test/smoke/src/steps/101_loadEditor.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import Settings from '@overleaf/settings' - -async function run({ assertHasStatusCode, request }) { - const response = await request(`/project/${Settings.smokeTest.projectId}`) - - assertHasStatusCode(response, 200) - - const PROJECT_ID_REGEX = new RegExp( - `` - ) - if (!PROJECT_ID_REGEX.test(response.body)) { - throw new Error('project page html does not have project_id') - } -} - -export default { run } diff --git a/services/web/test/smoke/src/support/Csrf.mjs b/services/web/test/smoke/src/support/Csrf.mjs deleted file mode 100644 index e492f3c368..0000000000 --- a/services/web/test/smoke/src/support/Csrf.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import OError from '@overleaf/o-error' -import { assertHasStatusCode } from './requestHelper.mjs' -const CSRF_REGEX = // - -export function _parseCsrf(body) { - const match = CSRF_REGEX.exec(body) - if (!match) { - throw new Error('Cannot find csrfToken in HTML') - } - return match[1] -} - -export function getCsrfTokenForFactory({ request }) { - return async function getCsrfTokenFor(endpoint) { - try { - const response = await request(endpoint) - assertHasStatusCode(response, 200) - return _parseCsrf(response.body) - } catch (err) { - throw new OError('error fetching csrf token', { endpoint }, err) - } - } -} diff --git a/services/web/test/smoke/src/support/Errors.mjs b/services/web/test/smoke/src/support/Errors.mjs deleted file mode 100644 index 577a1e4a0f..0000000000 --- a/services/web/test/smoke/src/support/Errors.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import OError from '@overleaf/o-error' - -export class SmokeTestFailure extends OError {} diff --git a/services/web/test/smoke/src/support/requestHelper.mjs b/services/web/test/smoke/src/support/requestHelper.mjs deleted file mode 100644 index 811e5a3a4e..0000000000 --- a/services/web/test/smoke/src/support/requestHelper.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import { Agent } from 'node:http' -import { createConnection } from 'node:net' -import { promisify } from 'node:util' -import OError from '@overleaf/o-error' -import request from 'request' -import Settings from '@overleaf/settings' - -// send requests to web router if this is the api process -const OWN_PORT = Settings.port || Settings.internal.web.port || 3000 -const PORT = (Settings.web && Settings.web.web_router_port) || OWN_PORT - -// like the curl option `--resolve DOMAIN:PORT:127.0.0.1` -class LocalhostAgent extends Agent { - createConnection(options, callback) { - return createConnection(PORT, '127.0.0.1', callback) - } -} - -// degrade the 'HttpOnly; Secure;' flags of the cookie -class InsecureCookieJar extends request.jar().constructor { - setCookie(...args) { - const cookie = super.setCookie(...args) - cookie.secure = false - cookie.httpOnly = false - return cookie - } -} - -export function requestFactory({ timeout }) { - return promisify( - request.defaults({ - agent: new LocalhostAgent(), - baseUrl: `http://smoke${Settings.cookieDomain}`, - headers: { - // emulate the header of a https proxy - // express wont emit a 'Secure;' cookie on a plain-text connection. - 'X-Forwarded-Proto': 'https', - }, - jar: new InsecureCookieJar(), - timeout, - }) - ) -} - -export function assertHasStatusCode(response, expected) { - const { statusCode: actual } = response - if (actual !== expected) { - throw new OError('unexpected response code', { - url: response.request.uri.href, - actual, - expected, - }) - } -} diff --git a/services/web/test/smoke/src/support/timeoutHelper.mjs b/services/web/test/smoke/src/support/timeoutHelper.mjs deleted file mode 100644 index 46f7010bdb..0000000000 --- a/services/web/test/smoke/src/support/timeoutHelper.mjs +++ /dev/null @@ -1,14 +0,0 @@ -export async function processWithTimeout({ work, timeout, message }) { - let workDeadLine - function checkInResults() { - clearTimeout(workDeadLine) - } - await Promise.race([ - new Promise((resolve, reject) => { - workDeadLine = setTimeout(() => { - reject(new Error(message)) - }, timeout) - }), - work.finally(checkInResults), - ]) -} diff --git a/services/web/test/smoke/tsconfig.json b/services/web/test/smoke/tsconfig.json deleted file mode 100644 index 126da92c92..0000000000 --- a/services/web/test/smoke/tsconfig.json +++ /dev/null @@ -1 +0,0 @@ -{ "extends": "../../tsconfig.backend.json" } diff --git a/services/web/tsconfig.backend.json b/services/web/tsconfig.backend.json index ba2843fe06..fa54dc4bdf 100644 --- a/services/web/tsconfig.backend.json +++ b/services/web/tsconfig.backend.json @@ -4,11 +4,9 @@ "app/src/**/*", "modules/*/app/src/**/*", "modules/*/test/acceptance/**/*", - "modules/*/test/smoke/**/*", "modules/*/test/unit/**/*", "scripts/**/*", "test/acceptance/**/*", - "test/smoke/**/*", "test/unit/**/*", "types/backend/**/*" ]