Got some more unit tests nailed down

This commit is contained in:
Chris Troutner
2021-03-07 15:35:44 -08:00
parent 8a28652c4d
commit 8383818c51
2 changed files with 219 additions and 19 deletions
+134 -12
View File
@@ -87,7 +87,9 @@ class RateLimits {
// Exit if the user has already authenticated with Basic Authentication.
if (req.locals.proLimit) {
console.log('req.locals.proLimit = true; Using Basic Authentication instead of rate limits')
console.log(
'req.locals.proLimit = true; Using Basic Authentication instead of rate limits'
)
return next()
}
@@ -109,16 +111,42 @@ class RateLimits {
// If this is an internal call that originated from a user using
// Basic Authentication, then skip rate-limits.
return next()
//
} else if (req.body.usrObj.jwtToken) {
// Internal call originated from a user using a JWT token.
console.log(
'Internal call originated from a user using a JWT token'
)
// } else if (req.body.usrObj.jwtToken) {
// // Internal call originated from a user using a JWT token.
// console.log(
// 'Internal call originated from a user using a JWT token'
// )
//
// const hasExceededRateLimit = await _this.trackRateLimits(
// req,
// res,
// req.body.usrObj.jwtToken
// )
// if (!hasExceededRateLimit) {
// return next()
// } else {
// return hasExceededRateLimit
// }
// //
// } else {
// // Internal call originates from an anonymous user.
// console.log('Internal call originates from an anonymous user')
// }
} else {
// Internal call originates from an anonymous user.
console.log('Internal call originates from an anonymous user')
// Determine if user has exceeded their rate limits. Pass in the
// JWT token if one exists.
const hasExceededRateLimit = await _this.trackRateLimits(
req,
res,
req.body.usrObj.jwtToken
)
if (!hasExceededRateLimit) {
return next()
} else {
return hasExceededRateLimit
}
}
}
}
@@ -126,9 +154,77 @@ class RateLimits {
console.error('Error in route-ratelimit2.js/applyRateLimits(): ', err)
}
// By default, move to the next middleware.
next()
}
// A wrapper for Redis-based rate limiter. This function is called by
// applyRateLimits(), after it's determined the key to use.
// Will return false if the user has not exceeded the rate limit. Otherwise
// it will return the res object that should be returned by the middleware.
async trackRateLimits (req, res, jwtToken) {
// Anonymous rate limits are used by default.
let pointsToConsume = 50
let key = req.ip
try {
// Decode the JWT token if it exists
if (jwtToken) {
const decoded = _this.decodeJwtToken(jwtToken)
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
key = decoded.id
pointsToConsume = decoded.pointsToConsume
}
console.log(`rate limit key: ${key}`)
// This function will throw an error if the user exceeds the rate limit.
// The 429 error response is handled by the catch().
await _this.rateLimiter.consume(key, pointsToConsume)
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
// Signal that the user has not exceeded their rate limits.
return false
} catch (err) {
console.log('err: ', err)
const rateLimit = Math.floor(1000 / pointsToConsume)
res.locals.rateLimitTriggered = true
// console.log('res.locals: ', res.locals)
// Rate limited was triggered
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({
error: `Too many requests. Your limits are currently ${rateLimit} requests per minute. Increase rate limits at https://fullstack.cash`
})
}
}
// Attempts to decode a JWT token. Returns default values if it fails.
decodeJwtToken (jwtToken) {
// Default values, in case there is an error.
let decoded = {
id: '123.456.789.10',
email: 'test@bchtest.net',
apiLevel: 10,
rateLimit: 3,
pointsToConsume: 50,
duration: 30,
iat: 1615157087,
exp: 1617749087
}
try {
decoded = _this.jwt.verify(jwtToken, _this.config.apiTokenSecret)
} catch (err) {
console.error('Error in route-ratelimit2.js/decodeJwtTokens()')
}
return decoded
}
// Returns a boolean if the origin of the request matches a domain in the
// whitelist.
isInWhitelist (req) {
@@ -139,15 +235,14 @@ class RateLimits {
const origin = req.get('origin')
console.log(`origin: ${origin}`)
// If the origin is not determinable, return false.
if (!origin) return false
// console.log(`WHITELIST_DOMAINS: ${JSON.stringify(WHITELIST_DOMAINS, null, 2)}`)
for (let i = 0; i < WHITELIST_DOMAINS.length; i++) {
const thisDomain = WHITELIST_DOMAINS[i]
// if (origin.toString().indexOf(thisDomain) > -1) {
// return true
// }
if (origin.includes(thisDomain)) return true
}
@@ -196,6 +291,33 @@ class RateLimits {
async wipeRedis () {
await redisClient.flushdb()
}
// Generates a JWT token for testing purposes. This is not used in production.
// This function mirrors the kind of JWT token that would be generated by
// jwt-bch-api.
generateJwtToken (payload) {
try {
const jwtOptions = {
expiresIn: '30 days'
}
const jwtPayload = {
id: payload.id,
pointsToConsume: payload.pointsToConsume
}
const token = _this.jwt.sign(
jwtPayload,
_this.config.apiTokenSecret,
jwtOptions
)
return token
} catch (err) {
console.error('Error in generateJwtToken()')
throw err
}
}
}
module.exports = RateLimits
+85 -7
View File
@@ -100,6 +100,7 @@ describe('#rate-routelimit2', () => {
it('should return false when origin is not in the whitelist', () => {
req.origin = 'blah.com'
req.get = sandbox.stub().returns(req.origin)
const result = uut.isInWhitelist(req)
@@ -110,12 +111,89 @@ describe('#rate-routelimit2', () => {
next()
})
// it('should return true when origin is in the whitelist', () => {
// req.origin = 'message.fullstack.cash'
//
// const result = uut.isInWhitelist(req)
//
// assert.equal(result, true)
// })
it('should return true when origin is in the whitelist', () => {
req.origin = 'message.fullstack.cash'
req.get = sandbox.stub().returns(req.origin)
const result = uut.isInWhitelist(req)
assert.equal(result, true)
})
})
describe('#trackRateLimits', () => {
it('should apply anonymous rate limits if no JWT token is provided', async () => {
req.ip = '127.0.0.1'
const result = await uut.trackRateLimits(req, res)
// console.log(`result: `, result)
// console.log('res.locals.pointsToConsume: ', res.locals.pointsToConsume)
assert.equal(result, false, 'Rate limits not exceeded')
assert.equal(
res.locals.pointsToConsume,
50,
'Anonymous rate limits applied'
)
})
it('should apply 100 RPM rate limits when JWT token is provided', async () => {
// Generate a new JWT token for the test.
const jwtPayload = {
id: '5dade3f5739e6c0ff034b9a1',
pointsToConsume: 10
}
const jwtToken = uut.generateJwtToken(jwtPayload)
const result = await uut.trackRateLimits(req, res, jwtToken)
// console.log(`result: `, result)
// console.log('res.locals.pointsToConsume: ', res.locals.pointsToConsume)
assert.equal(result, false, 'Rate limits not exceeded')
assert.equal(res.locals.pointsToConsume, 10, '100 RPM limits applied')
})
})
describe('#applyRateLimits', () => {
it('should skip rate limits if basic auth token is used', async () => {
req.locals.proLimit = true
// console.log('next.callCount: ', next.callCount)
const startCallCount = next.callCount
await uut.applyRateLimits(req, res, next)
// console.log('next.callCount: ', next.callCount)
const endCallCount = next.callCount
assert.isAbove(
endCallCount,
startCallCount,
'Expecting next to be called'
)
})
it('should skip rate limits if internal call passes basic auth token', async () => {
req.ip = '127.0.0.1'
req.body.usrObj = {
proLimit: true
}
// console.log('next.callCount: ', next.callCount)
const startCallCount = next.callCount
await uut.applyRateLimits(req, res, next)
// console.log('next.callCount: ', next.callCount)
const endCallCount = next.callCount
assert.isAbove(
endCallCount,
startCallCount,
'Expecting next to be called'
)
})
})
})