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 = - /