Merge pull request #3966 from overleaf/jpa-acceptance-tests-rate-limiter

[misc] replace RateLimiter unit tests with acceptance tests on /metrics

GitOrigin-RevId: c4ef502b38bb4b300c6b9812e1c97858e93a38fe
This commit is contained in:
Miguel Serrano
2021-05-06 02:09:22 +00:00
committed by Copybot
parent 6c831e9a02
commit 219f9ec39e
3 changed files with 161 additions and 199 deletions
@@ -2,11 +2,12 @@
handle-callback-err
*/
const { assert, expect } = require('chai')
const { expect } = require('chai')
const async = require('async')
const metrics = require('./helpers/metrics')
const User = require('./helpers/User')
const UserPromises = require('./helpers/User').promises
const redis = require('./helpers/redis')
const _ = require('lodash')
const Features = require('../../../app/src/infrastructure/Features')
// Currently this is testing registration via the 'public-registration' module,
@@ -64,55 +65,155 @@ const tryLoginThroughRegistrationForm = function (
describe('Registration', function () {
describe('LoginRateLimit', function () {
let userA
beforeEach(function () {
this.user = new User()
this.badEmail = 'bademail@example.com'
this.badPassword = 'badpassword'
userA = new UserPromises()
})
function loginRateLimited(line) {
return line.includes('rate_limit_hit') && line.includes('login')
}
async function getLoginRateLimitHitMetricValue() {
return await metrics.promises.getMetric(loginRateLimited)
}
let beforeCount
beforeEach('get baseline metric value', async function () {
beforeCount = await getLoginRateLimitHitMetricValue()
})
beforeEach('setup csrf token', async function () {
await userA.getCsrfToken()
})
it('should rate limit login attempts after 10 within two minutes', function (done) {
this.user.request.get('/login', (err, res, body) => {
async.timesSeries(
15,
(n, cb) => {
this.user.getCsrfToken(error => {
if (error != null) {
return cb(error)
}
this.user.request.post(
{
url: '/login',
json: {
email: this.badEmail,
password: this.badPassword,
},
},
(err, response, body) => {
const message = body && body.message && body.message.text
return cb(null, message)
}
)
})
},
(err, results) => {
// ten incorrect-credentials messages, then five rate-limit messages
expect(results.length).to.equal(15)
assert.deepEqual(
results,
_.concat(
_.fill(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
'Your email or password is incorrect. Please try again'
),
_.fill(
[1, 2, 3, 4, 5],
describe('pushing an account just below the rate limit', function () {
async function doLoginAttempts(user, n, pushInto) {
while (n--) {
const { body } = await user.doRequest('POST', {
url: '/login',
json: {
email: user.email,
password: 'invalid-password',
},
})
const message = body && body.message && body.message.text
pushInto.push(message)
}
}
let results = []
beforeEach('do 9 login attempts', async function () {
results = []
await doLoginAttempts(userA, 9, results)
})
it('should not record any rate limited requests', async function () {
const afterCount = await getLoginRateLimitHitMetricValue()
expect(afterCount).to.equal(beforeCount)
})
it('should produce the correct responses so far', function () {
expect(results.length).to.equal(9)
expect(results).to.deep.equal(
Array(9).fill('Your email or password is incorrect. Please try again')
)
})
describe('pushing the account past the limit', function () {
beforeEach('do 6 login attempts', async function () {
await doLoginAttempts(userA, 6, results)
})
it('should record 5 rate limited requests', async function () {
const afterCount = await getLoginRateLimitHitMetricValue()
expect(afterCount).to.equal(beforeCount + 5)
})
it('should produce the correct responses', function () {
expect(results.length).to.equal(15)
expect(results).to.deep.equal(
Array(10)
.fill('Your email or password is incorrect. Please try again')
.concat(
Array(5).fill(
'This account has had too many login requests. Please wait 2 minutes before trying to log in again'
)
)
)
})
describe('logging in with another user', function () {
let userB
beforeEach(function () {
userB = new UserPromises()
})
beforeEach('update baseline metric value', async function () {
beforeCount = await getLoginRateLimitHitMetricValue()
})
beforeEach('setup csrf token', async function () {
await userB.getCsrfToken()
})
let messages = []
beforeEach('do bad login', async function () {
messages = []
await doLoginAttempts(userB, 1, messages)
})
it('should not rate limit their request', function () {
expect(messages).to.deep.equal([
'Your email or password is incorrect. Please try again',
])
})
it('should not record any further rate limited requests', async function () {
const afterCount = await getLoginRateLimitHitMetricValue()
expect(afterCount).to.equal(beforeCount)
})
})
})
describe('performing a valid login for clearing the limit', function () {
beforeEach('do login', async function () {
await userA.login()
})
it('should log the user in', async function () {
const { response } = await userA.doRequest('GET', '/project')
expect(response.statusCode).to.equal(200)
})
it('should not record any rate limited requests', async function () {
const afterCount = await getLoginRateLimitHitMetricValue()
expect(afterCount).to.equal(beforeCount)
})
describe('logging out and performing more invalid login requests', function () {
beforeEach('logout', async function () {
await userA.logout()
})
beforeEach('fetch new csrf token', async function () {
await userA.getCsrfToken()
})
let results = []
beforeEach('do 9 login attempts', async function () {
results = []
await doLoginAttempts(userA, 9, results)
})
it('should not record any rate limited requests yet', async function () {
const afterCount = await getLoginRateLimitHitMetricValue()
expect(afterCount).to.equal(beforeCount)
})
it('should not emit any rate limited responses yet', function () {
expect(results.length).to.equal(9)
expect(results).to.deep.equal(
Array(9).fill(
'Your email or password is incorrect. Please try again'
)
)
return done()
}
)
})
})
})
})
})
@@ -0,0 +1,16 @@
const { callbackify } = require('util')
const request = require('./request')
async function getMetric(matcher) {
const { body } = await request.promises.request('/metrics')
const found = body.split('\n').find(matcher)
if (!found) return 0
return parseInt(found.split(' ')[1], 0)
}
module.exports = {
getMetric: callbackify(getMetric),
promises: {
getMetric,
},
}