Merge pull request #3427 from overleaf/jpa-rewite-smoke-tests

[SmokeTests] rewrite

GitOrigin-RevId: eda39db6b339d997f5669cb9bfca2aefe7d96699
This commit is contained in:
Jakob Ackermann 2020-12-09 10:12:45 +00:00 committed by Copybot
parent e8e2264d7d
commit cb9d207ba0
16 changed files with 470 additions and 314 deletions

View file

@ -1,60 +1,17 @@
/* eslint-disable
handle-callback-err,
max-len,
no-path-concat,
no-unused-vars,
node/no-deprecated-api,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let HealthCheckController
const Mocha = require('mocha')
const Base = require('mocha/lib/reporters/base')
const RedisWrapper = require('../../infrastructure/RedisWrapper')
const rclient = RedisWrapper.client('health_check')
const settings = require('settings-sharelatex')
const logger = require('logger-sharelatex')
const domain = require('domain')
const UserGetter = require('../User/UserGetter')
const {
SmokeTestFailure,
runSmokeTests
} = require('./../../../../test/smoke/src/SmokeTests')
module.exports = HealthCheckController = {
module.exports = {
check(req, res, next) {
if (next == null) {
next = function(error) {}
}
const d = domain.create()
d.on('error', error => logger.err({ err: error }, 'error in mocha'))
return d.run(function() {
const mocha = new Mocha({ reporter: Reporter(res), timeout: 10000 })
mocha.addFile('test/smoke/src/SmokeTests.js')
return mocha.run(function() {
// TODO: combine this with the smoke-test-sharelatex module
// we need to clean up all references to the smokeTest module
// so it can be garbage collected. The only reference should
// be in its parent, when it is loaded by mocha.addFile.
const path = require.resolve(
__dirname + '/../../../../test/smoke/src/SmokeTests.js'
)
const smokeTestModule = require.cache[path]
if (smokeTestModule != null) {
let idx
const { parent } = smokeTestModule
while ((idx = parent.children.indexOf(smokeTestModule)) !== -1) {
parent.children.splice(idx, 1)
}
} else {
logger.warn({ path }, 'smokeTestModule not defined')
}
// remove the smokeTest from the module cache
return delete require.cache[path]
})
})
// detach from express for cleaner stack traces
setTimeout(() => runSmokeTestsDetached(req, res).catch(next))
},
checkActiveHandles(req, res, next) {
@ -129,38 +86,34 @@ module.exports = HealthCheckController = {
}
}
var Reporter = res =>
function(runner) {
Base.call(this, runner)
const tests = []
const passes = []
const failures = []
runner.on('test end', test => tests.push(test))
runner.on('pass', test => passes.push(test))
runner.on('fail', test => failures.push(test))
return runner.on('end', () => {
const clean = test => ({
title: test.fullTitle(),
duration: test.duration,
err: test.err,
timedOut: test.timedOut
})
const results = {
stats: this.stats,
failures: failures.map(clean),
passes: passes.map(clean)
}
res.contentType('application/json')
if (failures.length > 0) {
logger.err({ failures }, 'health check failed')
return res.status(500).send(JSON.stringify(results, null, 2))
} else {
return res.status(200).send(JSON.stringify(results, null, 2))
}
})
function prettyJSON(blob) {
return JSON.stringify(blob, null, 2) + '\n'
}
async function runSmokeTestsDetached(req, res) {
function isAborted() {
return req.aborted
}
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.contentType('application/json')
res.status(status).send(prettyJSON(response))
}

View file

@ -508,6 +508,7 @@ module.exports = settings =
password: process.env['SMOKE_TEST_PASSWORD']
projectId: process.env['SMOKE_TEST_PROJECT_ID']
rateLimitSubject: process.env['SMOKE_TEST_RATE_LIMIT_SUBJECT'] or "127.0.0.1"
stepTimeout: parseInt(process.env['SMOKE_TEST_STEP_TIMEOUT'] or "10000", 10)
appName: process.env['APP_NAME'] or "ShareLaTeX (Community Edition)"

View file

@ -3836,9 +3836,9 @@
}
},
"@overleaf/o-error": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-3.1.0.tgz",
"integrity": "sha512-TWJ80ozJ1LeugGTJyGQSPEuTkZ9LqZD7/ndLE6azKa03SU/mKV/FINcfk8atpVil8iv1hHQwzYZc35klplpMpQ=="
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@overleaf/o-error/-/o-error-3.2.0.tgz",
"integrity": "sha512-H2HjuzoRBKNDy3pEZnVt+Pa6gPZPEENALXzvJy99ijOE5z6iirUkp2EMP/NTPfvyfUPz32cY04Jn10jjVowerg=="
},
"@overleaf/redis-wrapper": {
"version": "2.0.0",
@ -9825,6 +9825,7 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
@ -11991,7 +11992,8 @@
"browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
"dev": true
},
"browserify-aes": {
"version": "1.2.0",
@ -15074,7 +15076,8 @@
"diff": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"dev": true
},
"diffie-hellman": {
"version": "5.0.3",
@ -17578,6 +17581,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
"integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==",
"dev": true,
"requires": {
"is-buffer": "~2.0.3"
},
@ -17585,7 +17589,8 @@
"is-buffer": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
"dev": true
}
}
},
@ -19178,7 +19183,8 @@
"growl": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA=="
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
"dev": true
},
"gtoken": {
"version": "5.1.0",
@ -19593,7 +19599,8 @@
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"helmet": {
"version": "3.22.0",
@ -20997,7 +21004,8 @@
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"isobject": {
"version": "4.0.0",
@ -21566,6 +21574,7 @@
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@ -22949,6 +22958,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
"integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
"dev": true,
"requires": {
"chalk": "^2.0.1"
},
@ -22957,6 +22967,7 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@ -22965,6 +22976,7 @@
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@ -22974,12 +22986,14 @@
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true
},
"supports-color": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
"integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
@ -23942,6 +23956,7 @@
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz",
"integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==",
"dev": true,
"requires": {
"ansi-colors": "3.2.3",
"browser-stdout": "1.3.1",
@ -23971,17 +23986,20 @@
"ansi-colors": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
"integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw=="
"integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
"dev": true
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
@ -23990,6 +24008,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
"locate-path": "^3.0.0"
}
@ -23998,6 +24017,7 @@
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -24011,6 +24031,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
@ -24020,6 +24041,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -24027,12 +24049,14 @@
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q=="
"integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
@ -24040,12 +24064,14 @@
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
"p-limit": "^2.0.0"
}
@ -24054,6 +24080,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
"integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
@ -24062,6 +24089,7 @@
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
@ -24570,6 +24598,7 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz",
"integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==",
"dev": true,
"requires": {
"object.getownpropertydescriptors": "^2.0.3",
"semver": "^5.7.0"
@ -24578,7 +24607,8 @@
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
}
},
@ -33024,7 +33054,8 @@
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"srcset": {
"version": "2.0.1",
@ -37709,6 +37740,7 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
@ -38149,6 +38181,7 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
"integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
"dev": true,
"requires": {
"flat": "^4.1.0",
"lodash": "^4.17.15",

View file

@ -44,7 +44,7 @@
"@babel/preset-env": "^7.9.5",
"@babel/preset-react": "^7.9.4",
"@overleaf/metrics": "^3.4.1",
"@overleaf/o-error": "^3.1.0",
"@overleaf/o-error": "^3.2.0",
"@overleaf/redis-wrapper": "^2.0.0",
"@pollyjs/adapter-node-http": "^4.2.1",
"@pollyjs/core": "^4.2.1",
@ -100,7 +100,6 @@
"method-override": "^2.3.3",
"minimist": "1.2.5",
"mmmagic": "^0.5.2",
"mocha": "^6.2.2",
"moment": "^2.24.0",
"mongodb": "^3.6.0",
"mongoose": "^5.10.7",
@ -209,6 +208,7 @@
"less-plugin-autoprefix": "^2.0.0",
"mini-css-extract-plugin": "^0.8.0",
"mkdirp": "0.5.1",
"mocha": "^6.2.2",
"mock-fs": "^4.11.0",
"node-fetch": "^2.6.1",
"nodemon": "^1.14.3",

View file

@ -19,11 +19,20 @@ describe('HealthCheckController', function() {
})
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 }
}
@ -36,13 +45,30 @@ describe('HealthCheckController', function() {
})
})
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.equal('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 } = await performSmokeTestRequest()
const { response, body } = await performSmokeTestRequest()
expect(body.error).to.equal('run.101_loadEditor failed')
expect(response.statusCode).to.equal(500)
})
})
@ -52,8 +78,9 @@ describe('HealthCheckController', function() {
Settings.smokeTest.password = 'foo-bar'
})
it('should respond with a 500 with mismatching password', async function() {
const { response } = await performSmokeTestRequest()
const { response, body } = await performSmokeTestRequest()
expect(body.error).to.equal('run.002_login failed')
expect(response.statusCode).to.equal(500)
})
})

View file

@ -0,0 +1,39 @@
# 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.

View file

@ -1,215 +1,95 @@
/* eslint-disable
max-len,
no-unused-vars,
no-useless-escape,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const child = require('child_process')
let fs = require('fs')
const assert = require('assert')
const chai = require('chai')
if (Object.prototype.should == null) {
chai.should()
}
const { expect } = chai
const fs = require('fs')
const Path = require('path')
const Settings = require('settings-sharelatex')
let ownPort = Settings.internal.web.port || Settings.port || 3000
const port = (Settings.web && Settings.web.web_router_port) || ownPort // send requests to web router if this is the api process
const cookeFilePath = `/tmp/smoke-test-cookie-${ownPort}-to-${port}.txt`
const buildUrl = path =>
` -b ${cookeFilePath} --resolve 'smoke${
Settings.cookieDomain
}:${port}:127.0.0.1' http://smoke${Settings.cookieDomain}:${port}/${path}`
const logger = require('logger-sharelatex')
const LoginRateLimiter = require('../../../app/src/Features/Security/LoginRateLimiter.js')
const RateLimiter = require('../../../app/src/infrastructure/RateLimiter.js')
const { getCsrfTokenForFactory } = require('./support/Csrf')
const { SmokeTestFailure } = require('./support/Errors')
const {
requestFactory,
assertHasStatusCode
} = require('./support/requestHelper')
const { processWithTimeout } = require('./support/timeoutHelper')
// Change cookie to be non secure so curl will send it
const convertCookieFile = function(callback) {
fs = require('fs')
return fs.readFile(cookeFilePath, 'utf8', (err, data) => {
if (err) {
return callback(err)
}
const firstTrue = data.indexOf('TRUE')
const secondTrue = data.indexOf('TRUE', firstTrue + 4)
const result =
data.slice(0, secondTrue) + 'FALSE' + data.slice(secondTrue + 4)
return fs.writeFile(cookeFilePath, result, 'utf8', err => {
if (err) {
return callback(err)
}
return callback()
})
const STEP_TIMEOUT = Settings.smokeTest.stepTimeout
const PATH_STEPS = Path.join(__dirname, './steps')
const STEPS = fs
.readdirSync(PATH_STEPS)
.sort()
.map(name => {
const step = require(Path.join(PATH_STEPS, name))
step.name = Path.basename(name, '.js')
return 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
})
}
}
describe('Opening', function() {
before(function(done) {
logger.log('smoke test: setup')
LoginRateLimiter.recordSuccessfulLogin(Settings.smokeTest.user, err => {
if (err != null) {
logger.err({ err }, 'smoke test: error recoring successful login')
return done(err)
}
return RateLimiter.clearRateLimit(
'open-project',
`${Settings.smokeTest.projectId}:${Settings.smokeTest.userId}`,
err => {
if (err != null) {
logger.err(
{ err },
'smoke test: error clearing open-project rate limit'
)
return done(err)
}
return RateLimiter.clearRateLimit(
'overleaf-login',
Settings.smokeTest.rateLimitSubject,
err => {
if (err != null) {
logger.err(
{ err },
'smoke test: error clearing overleaf-login rate limit'
)
return done(err)
}
return done()
}
)
}
)
})
})
before(function(done) {
logger.log('smoke test: hitting dev/csrf')
let command = `\
curl -H "X-Forwarded-Proto: https" -c ${cookeFilePath} ${buildUrl('dev/csrf')}\
`
child.exec(command, (err, stdout, stderr) => {
if (err != null) {
done(err)
}
const csrf = stdout
logger.log('smoke test: converting cookie file 1')
return convertCookieFile(err => {
if (err != null) {
return done(err)
}
logger.log('smoke test: hitting /login with csrf')
command = `\
curl -c ${cookeFilePath} -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d '{"_csrf":"${csrf}", "email":"${
Settings.smokeTest.user
}", "password":"${Settings.smokeTest.password}"}' ${buildUrl('login')}\
`
return child.exec(command, err => {
if (err != null) {
return done(err)
}
logger.log('smoke test: finishing setup')
return convertCookieFile(done)
})
})
})
})
after(function(done) {
logger.log('smoke test: converting cookie file 2')
convertCookieFile(err => {
if (err != null) {
return done(err)
}
logger.log('smoke test: cleaning up')
let command = `\
curl -H "X-Forwarded-Proto: https" -c ${cookeFilePath} ${buildUrl('dev/csrf')}\
`
return child.exec(command, (err, stdout, stderr) => {
if (err != null) {
done(err)
}
const csrf = stdout
logger.log('smoke test: converting cookie file 3')
return convertCookieFile(err => {
if (err != null) {
return done(err)
}
command = `\
curl -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d '{"_csrf":"${csrf}"}' -c ${cookeFilePath} ${buildUrl(
'logout'
)}\
`
return child.exec(command, (err, stdout, stderr) => {
if (err != null) {
return done(err)
}
return fs.unlink(cookeFilePath, done)
})
})
})
})
})
it('a project', function(done) {
logger.log('smoke test: Checking can load a project')
this.timeout(4000)
const command = `\
curl -H "X-Forwarded-Proto: https" -v ${buildUrl(
`project/${Settings.smokeTest.projectId}`
)}\
`
return child.exec(command, (error, stdout, stderr) => {
expect(error, 'smoke test: error in getting project').to.not.exist
const statusCodeMatch = !!stderr.match('200 OK')
expect(
statusCodeMatch,
'smoke test: response code is not 200 getting project'
).to.equal(true)
// Check that the project id is present in the javascript that loads up the project
const match = !!stdout.match(
`window.project_id = \"${Settings.smokeTest.projectId}\"`
)
expect(
match,
'smoke test: project page html does not have project_id'
).to.equal(true)
return done()
})
})
it('the project list', function(done) {
logger.log('smoke test: Checking can load project list')
this.timeout(4000)
const command = `\
curl -H "X-Forwarded-Proto: https" -v ${buildUrl('project')}\
`
return child.exec(command, (error, stdout, stderr) => {
expect(error, 'smoke test: error returned in getting project list').to.not
.exist
expect(
!!stderr.match('200 OK'),
'smoke test: response code is not 200 getting project list'
).to.equal(true)
expect(
!!stdout.match(
'<title>Your Projects - .*, Online LaTeX Editor</title>'
),
'smoke test: body does not have correct title'
).to.equal(true)
expect(
!!stdout.match('ProjectPageController'),
'smoke test: body does not have correct angular controller'
).to.equal(true)
return done()
})
})
})
module.exports = { runSmokeTests, SmokeTestFailure }

View file

@ -0,0 +1,7 @@
async function run({ getCsrfTokenFor }) {
const loginCsrfToken = await getCsrfTokenFor('/login')
return { loginCsrfToken }
}
module.exports = { run }

View file

@ -0,0 +1,36 @@
const OError = require('@overleaf/o-error')
const Settings = require('settings-sharelatex')
const RateLimiter = require('../../../../app/src/infrastructure/RateLimiter')
async function clearRateLimit(endpointName, subject) {
try {
await RateLimiter.promises.clearRateLimit(endpointName, subject)
} catch (err) {
throw new OError(
'error clearing rate limit',
{ endpointName, subject },
err
)
}
}
async function clearLoginRateLimit() {
await clearRateLimit('login', Settings.smokeTest.user)
}
async function clearOpenProjectRateLimit() {
await clearRateLimit(
'open-project',
`${Settings.smokeTest.projectId}:${Settings.smokeTest.userId}`
)
}
async function run({ processWithTimeout, timeout }) {
await processWithTimeout({
work: Promise.all([clearLoginRateLimit(), clearOpenProjectRateLimit()]),
timeout,
message: 'cleanupRateLimits timed out'
})
}
module.exports = { run }

View file

@ -0,0 +1,35 @@
const Settings = require('settings-sharelatex')
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('/logout')
const response = await request('/logout', {
method: 'POST',
headers: {
'X-CSRF-Token': logoutCsrfToken
}
})
assertHasStatusCode(response, 302)
}
module.exports = { cleanup, run }

View file

@ -0,0 +1,17 @@
const ANGULAR_PROJECT_CONTROLLER_REGEX = /controller="ProjectPageController"/
const TITLE_REGEX = /<title>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')
}
if (!ANGULAR_PROJECT_CONTROLLER_REGEX.test(response.body)) {
throw new Error('body does not have correct angular controller')
}
}
module.exports = { run }

View file

@ -0,0 +1,16 @@
const Settings = require('settings-sharelatex')
async function run({ assertHasStatusCode, request }) {
const response = await request(`/project/${Settings.smokeTest.projectId}`)
assertHasStatusCode(response, 200)
const PROJECT_ID_REGEX = new RegExp(
`window.project_id = "${Settings.smokeTest.projectId}"`
)
if (!PROJECT_ID_REGEX.test(response.body)) {
throw new Error('project page html does not have project_id')
}
}
module.exports = { run }

View file

@ -0,0 +1,27 @@
const OError = require('@overleaf/o-error')
const { assertHasStatusCode } = require('./requestHelper')
const CSRF_REGEX = /window.csrfToken = "(.+?)"/
function _parseCsrf(body) {
const match = CSRF_REGEX.exec(body)
if (!match) {
throw new Error('Cannot find csrfToken in HTML')
}
return match[1]
}
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 on ${endpoint}`, {}, err)
}
}
}
module.exports = {
getCsrfTokenForFactory
}

View file

@ -0,0 +1,7 @@
const OError = require('@overleaf/o-error')
class SmokeTestFailure extends OError {}
module.exports = {
SmokeTestFailure
}

View file

@ -0,0 +1,60 @@
const { Agent } = require('http')
const { createConnection } = require('net')
const { promisify } = require('util')
const OError = require('@overleaf/o-error')
const request = require('request')
const Settings = require('settings-sharelatex')
// 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
}
}
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
})
)
}
function assertHasStatusCode(response, expected) {
const { statusCode: actual } = response
if (actual !== expected) {
throw new OError('unexpected response code', {
url: response.request.uri.href,
actual,
expected
})
}
}
module.exports = {
assertHasStatusCode,
requestFactory
}

View file

@ -0,0 +1,18 @@
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)
])
}
module.exports = {
processWithTimeout
}