node_modules

This commit is contained in:
softprops 2019-10-20 19:16:50 -04:00
parent 0e414c630a
commit 78c309ef59
555 changed files with 103819 additions and 1 deletions

View file

@ -0,0 +1,170 @@
const expect = require('chai').expect
const Octokit = require('./octokit')
describe('Events', function () {
it('Should support non-limit 403s', async function () {
const octokit = new Octokit({ throttle: { onAbuseLimit: () => 1, onRateLimit: () => 1 } })
let caught = false
await octokit.request('GET /route1', {
request: {
responses: [{ status: 201, headers: {}, data: {} }]
}
})
try {
await octokit.request('GET /route2', {
request: {
responses: [{ status: 403, headers: {}, data: {} }]
}
})
} catch (error) {
expect(error.message).to.equal('Test failed request (403)')
caught = true
}
expect(caught).to.equal(true)
expect(octokit.__requestLog).to.deep.equal([
'START GET /route1',
'END GET /route1',
'START GET /route2'
])
})
describe('\'abuse-limit\'', function () {
it('Should detect abuse limit and broadcast event', async function () {
let eventCount = 0
const octokit = new Octokit({
throttle: {
onAbuseLimit: (retryAfter, options) => {
expect(retryAfter).to.equal(60)
expect(options).to.include({ method: 'GET', url: '/route2' })
expect(options.request.retryCount).to.equal(0)
eventCount++
},
onRateLimit: () => 1
}
})
await octokit.request('GET /route1', {
request: {
responses: [{ status: 201, headers: {}, data: {} }]
}
})
try {
await octokit.request('GET /route2', {
request: {
responses: [{ status: 403, headers: { 'retry-after': '60' }, data: { message: 'You have been rate limited to prevent abuse' } }]
}
})
throw new Error('Should not reach this point')
} catch (error) {
expect(error.status).to.equal(403)
}
expect(eventCount).to.equal(1)
})
it('Should ensure retryAfter is a minimum of 5s', async function () {
let eventCount = 0
const octokit = new Octokit({
throttle: {
onAbuseLimit: (retryAfter, options) => {
expect(retryAfter).to.equal(5)
expect(options).to.include({ method: 'GET', url: '/route2' })
expect(options.request.retryCount).to.equal(0)
eventCount++
},
onRateLimit: () => 1
}
})
await octokit.request('GET /route1', {
request: {
responses: [{ status: 201, headers: {}, data: {} }]
}
})
try {
await octokit.request('GET /route2', {
request: {
responses: [{ status: 403, headers: { 'retry-after': '2' }, data: { message: 'You have been rate limited to prevent abuse' } }]
}
})
throw new Error('Should not reach this point')
} catch (error) {
expect(error.status).to.equal(403)
}
expect(eventCount).to.equal(1)
})
it('Should broadcast retryAfter of 5s even when the header is missing', async function () {
let eventCount = 0
const octokit = new Octokit({
throttle: {
onAbuseLimit: (retryAfter, options) => {
expect(retryAfter).to.equal(5)
expect(options).to.include({ method: 'GET', url: '/route2' })
expect(options.request.retryCount).to.equal(0)
eventCount++
},
onRateLimit: () => 1
}
})
await octokit.request('GET /route1', {
request: {
responses: [{ status: 201, headers: {}, data: {} }]
}
})
try {
await octokit.request('GET /route2', {
request: {
responses: [{ status: 403, headers: {}, data: { message: 'You have been rate limited to prevent abuse' } }]
}
})
throw new Error('Should not reach this point')
} catch (error) {
expect(error.status).to.equal(403)
}
expect(eventCount).to.equal(1)
})
})
describe('\'rate-limit\'', function () {
it('Should detect rate limit exceeded and broadcast event', async function () {
let eventCount = 0
const octokit = new Octokit({
throttle: {
onRateLimit: (retryAfter, options) => {
expect(retryAfter).to.be.closeTo(30, 1)
expect(options).to.include({ method: 'GET', url: '/route2' })
expect(options.request.retryCount).to.equal(0)
eventCount++
},
onAbuseLimit: () => 1
}
})
const t0 = Date.now()
await octokit.request('GET /route1', {
request: {
responses: [{ status: 201, headers: {}, data: {} }]
}
})
try {
await octokit.request('GET /route2', {
request: {
responses: [{ status: 403, headers: { 'x-ratelimit-remaining': '0', 'x-ratelimit-reset': `${Math.round(t0 / 1000) + 30}` }, data: {} }]
}
})
throw new Error('Should not reach this point')
} catch (error) {
expect(error.status).to.equal(403)
}
expect(eventCount).to.equal(1)
})
})
})

View file

@ -0,0 +1,291 @@
const Bottleneck = require('bottleneck')
const expect = require('chai').expect
const Octokit = require('./octokit')
describe('General', function () {
it('Should be possible to disable the plugin', async function () {
const octokit = new Octokit({ throttle: { enabled: false } })
const req1 = octokit.request('GET /route1', {
request: {
responses: [{ status: 201, headers: {}, data: {} }]
}
})
const req2 = octokit.request('GET /route2', {
request: {
responses: [{ status: 202, headers: {}, data: {} }]
}
})
const req3 = octokit.request('GET /route3', {
request: {
responses: [{ status: 203, headers: {}, data: {} }]
}
})
await Promise.all([req1, req2, req3])
expect(octokit.__requestLog).to.deep.equal([
'START GET /route1',
'START GET /route2',
'START GET /route3',
'END GET /route1',
'END GET /route2',
'END GET /route3'
])
})
it('Should require the user to pass both limit handlers', function () {
const message = 'You must pass the onAbuseLimit and onRateLimit error handlers'
expect(() => new Octokit()).to.throw(message)
expect(() => new Octokit({ throttle: {} })).to.throw(message)
expect(() => new Octokit({ throttle: { onAbuseLimit: 5, onRateLimit: 5 } })).to.throw(message)
expect(() => new Octokit({ throttle: { onAbuseLimit: 5, onRateLimit: () => 1 } })).to.throw(message)
expect(() => new Octokit({ throttle: { onAbuseLimit: () => 1 } })).to.throw(message)
expect(() => new Octokit({ throttle: { onRateLimit: () => 1 } })).to.throw(message)
expect(() => new Octokit({ throttle: { onAbuseLimit: () => 1, onRateLimit: () => 1 } })).to.not.throw()
})
})
describe('Github API best practices', function () {
it('Should linearize requests', async function () {
const octokit = new Octokit({ throttle: { onAbuseLimit: () => 1, onRateLimit: () => 1 } })
const req1 = octokit.request('GET /route1', {
request: {
responses: [{ status: 201, headers: {}, data: {} }]
}
})
const req2 = octokit.request('GET /route2', {
request: {
responses: [{ status: 202, headers: {}, data: {} }]
}
})
const req3 = octokit.request('GET /route3', {
request: {
responses: [{ status: 203, headers: {}, data: {} }]
}
})
await Promise.all([req1, req2, req3])
expect(octokit.__requestLog).to.deep.equal([
'START GET /route1',
'END GET /route1',
'START GET /route2',
'END GET /route2',
'START GET /route3',
'END GET /route3'
])
})
it('Should maintain 1000ms between mutating or GraphQL requests', async function () {
const octokit = new Octokit({
throttle: {
write: new Bottleneck.Group({ minTime: 50 }),
onAbuseLimit: () => 1,
onRateLimit: () => 1
}
})
const req1 = octokit.request('POST /route1', {
request: {
responses: [{ status: 201, headers: {}, data: {} }]
}
})
const req2 = octokit.request('GET /route2', {
request: {
responses: [{ status: 202, headers: {}, data: {} }]
}
})
const req3 = octokit.request('POST /route3', {
request: {
responses: [{ status: 203, headers: {}, data: {} }]
}
})
const req4 = octokit.request('POST /graphql', {
request: {
responses: [{ status: 200, headers: {}, data: {} }]
}
})
await Promise.all([req1, req2, req3, req4])
expect(octokit.__requestLog).to.deep.equal([
'START GET /route2',
'END GET /route2',
'START POST /route1',
'END POST /route1',
'START POST /route3',
'END POST /route3',
'START POST /graphql',
'END POST /graphql'
])
expect(octokit.__requestTimings[4] - octokit.__requestTimings[0]).to.be.closeTo(50, 20)
expect(octokit.__requestTimings[6] - octokit.__requestTimings[4]).to.be.closeTo(50, 20)
})
it('Should maintain 3000ms between requests that trigger notifications', async function () {
const octokit = new Octokit({
throttle: {
write: new Bottleneck.Group({ minTime: 50 }),
notifications: new Bottleneck.Group({ minTime: 100 }),
onAbuseLimit: () => 1,
onRateLimit: () => 1
}
})
const req1 = octokit.request('POST /orgs/:org/invitations', {
request: {
responses: [{ status: 201, headers: {}, data: {} }]
}
})
const req2 = octokit.request('POST /route2', {
request: {
responses: [{ status: 202, headers: {}, data: {} }]
}
})
const req3 = octokit.request('POST /repos/:owner/:repo/commits/:sha/comments', {
request: {
responses: [{ status: 302, headers: {}, data: {} }]
}
})
await Promise.all([req1, req2, req3])
expect(octokit.__requestLog).to.deep.equal([
'START POST /orgs/:org/invitations',
'END POST /orgs/:org/invitations',
'START POST /route2',
'END POST /route2',
'START POST /repos/:owner/:repo/commits/:sha/comments',
'END POST /repos/:owner/:repo/commits/:sha/comments'
])
expect(octokit.__requestTimings[5] - octokit.__requestTimings[0]).to.be.closeTo(100, 20)
})
it('Should match custom routes when checking notification triggers', function () {
const plugin = require('../../lib')
expect(plugin.triggersNotification('/abc/def')).to.equal(false)
expect(plugin.triggersNotification('/orgs/abc/invitation')).to.equal(false)
expect(plugin.triggersNotification('/repos/abc/releases')).to.equal(false)
expect(plugin.triggersNotification('/repos/abc/def/pulls/5')).to.equal(false)
expect(plugin.triggersNotification('/repos/abc/def/pulls')).to.equal(true)
expect(plugin.triggersNotification('/repos/abc/def/pulls/5/comments')).to.equal(true)
expect(plugin.triggersNotification('/repos/foo/bar/issues')).to.equal(true)
expect(plugin.triggersNotification('/repos/:owner/:repo/pulls')).to.equal(true)
expect(plugin.triggersNotification('/repos/:owner/:repo/pulls/5/comments')).to.equal(true)
expect(plugin.triggersNotification('/repos/:foo/:bar/issues')).to.equal(true)
})
it('Should maintain 2000ms between search requests', async function () {
const octokit = new Octokit({
throttle: {
search: new Bottleneck.Group({ minTime: 50 }),
onAbuseLimit: () => 1,
onRateLimit: () => 1
}
})
const req1 = octokit.request('GET /search/route1', {
request: {
responses: [{ status: 201, headers: {}, data: {} }]
}
})
const req2 = octokit.request('GET /route2', {
request: {
responses: [{ status: 202, headers: {}, data: {} }]
}
})
const req3 = octokit.request('GET /search/route3', {
request: {
responses: [{ status: 203, headers: {}, data: {} }]
}
})
await Promise.all([req1, req2, req3])
expect(octokit.__requestLog).to.deep.equal([
'START GET /route2',
'END GET /route2',
'START GET /search/route1',
'END GET /search/route1',
'START GET /search/route3',
'END GET /search/route3'
])
expect(octokit.__requestTimings[4] - octokit.__requestTimings[2]).to.be.closeTo(50, 20)
})
it('Should optimize throughput rather than maintain ordering', async function () {
const octokit = new Octokit({
throttle: {
write: new Bottleneck.Group({ minTime: 50 }),
notifications: new Bottleneck.Group({ minTime: 150 }),
onAbuseLimit: () => 1,
onRateLimit: () => 1
}
})
const req1 = octokit.request('POST /orgs/abc/invitations', {
request: {
responses: [{ status: 200, headers: {}, data: {} }]
}
})
const req2 = octokit.request('GET /route2', {
request: {
responses: [{ status: 200, headers: {}, data: {} }]
}
})
const req3 = octokit.request('GET /route3', {
request: {
responses: [{ status: 200, headers: {}, data: {} }]
}
})
const req4 = octokit.request('POST /route4', {
request: {
responses: [{ status: 200, headers: {}, data: {} }]
}
})
const req5 = octokit.request('POST /repos/abc/def/commits/12345/comments', {
request: {
responses: [{ status: 200, headers: {}, data: {} }]
}
})
const req6 = octokit.request('PATCH /orgs/abc/invitations', {
request: {
responses: [{ status: 200, headers: {}, data: {} }]
}
})
await Promise.all([req1, req2, req3, req4, req5, req6])
await octokit.request('GET /route6', {
request: {
responses: [{ status: 200, headers: {}, data: {} }]
}
})
expect(octokit.__requestLog).to.deep.equal([
'START GET /route2',
'END GET /route2',
'START GET /route3',
'END GET /route3',
'START POST /orgs/abc/invitations',
'END POST /orgs/abc/invitations',
'START POST /route4',
'END POST /route4',
'START POST /repos/abc/def/commits/12345/comments',
'END POST /repos/abc/def/commits/12345/comments',
'START PATCH /orgs/abc/invitations',
'END PATCH /orgs/abc/invitations',
'START GET /route6',
'END GET /route6'
])
expect(octokit.__requestTimings[2] - octokit.__requestTimings[0]).to.be.closeTo(0, 20)
expect(octokit.__requestTimings[4] - octokit.__requestTimings[2]).to.be.closeTo(0, 20)
expect(octokit.__requestTimings[6] - octokit.__requestTimings[4]).to.be.closeTo(50, 20)
expect(octokit.__requestTimings[8] - octokit.__requestTimings[6]).to.be.closeTo(100, 20)
expect(octokit.__requestTimings[10] - octokit.__requestTimings[8]).to.be.closeTo(150, 20)
expect(octokit.__requestTimings[12] - octokit.__requestTimings[10]).to.be.closeTo(0, 30)
})
})

View file

@ -0,0 +1,28 @@
const Octokit = require('@octokit/rest')
const HttpError = require('@octokit/request/lib/http-error')
const throttlingPlugin = require('../..')
module.exports = Octokit
.plugin((octokit) => {
octokit.__t0 = Date.now()
octokit.__requestLog = []
octokit.__requestTimings = []
octokit.hook.wrap('request', async (request, options) => {
octokit.__requestLog.push(`START ${options.method} ${options.url}`)
octokit.__requestTimings.push(Date.now() - octokit.__t0)
await new Promise(resolve => setTimeout(resolve, 0))
const res = options.request.responses.shift()
if (res.status >= 400) {
const message = res.data.message != null ? res.data.message : `Test failed request (${res.status})`
const error = new HttpError(message, res.status, res.headers, options)
throw error
} else {
octokit.__requestLog.push(`END ${options.method} ${options.url}`)
octokit.__requestTimings.push(Date.now() - octokit.__t0)
return res
}
})
})
.plugin(throttlingPlugin)

View file

@ -0,0 +1,193 @@
const Bottleneck = require('bottleneck')
const expect = require('chai').expect
const Octokit = require('./octokit')
describe('Retry', function () {
describe('REST', function () {
it('Should retry \'abuse-limit\' and succeed', async function () {
let eventCount = 0
const octokit = new Octokit({
throttle: {
minimumAbuseRetryAfter: 0,
retryAfterBaseValue: 50,
onAbuseLimit: (retryAfter, options) => {
expect(options).to.include({ method: 'GET', url: '/route' })
expect(options.request.retryCount).to.equal(eventCount)
expect(retryAfter).to.equal(eventCount + 1)
eventCount++
return true
},
onRateLimit: () => 1
}
})
const res = await octokit.request('GET /route', {
request: {
responses: [
{ status: 403, headers: { 'retry-after': '1' }, data: { message: 'You have been rate limited to prevent abuse' } },
{ status: 200, headers: {}, data: { message: 'Success!' } }
]
}
})
expect(res.status).to.equal(200)
expect(res.data).to.include({ message: 'Success!' })
expect(eventCount).to.equal(1)
expect(octokit.__requestLog).to.deep.equal([
'START GET /route',
'START GET /route',
'END GET /route'
])
expect(octokit.__requestTimings[1] - octokit.__requestTimings[0]).to.be.closeTo(50, 20)
})
it('Should retry \'abuse-limit\' twice and fail', async function () {
let eventCount = 0
const octokit = new Octokit({
throttle: {
minimumAbuseRetryAfter: 0,
retryAfterBaseValue: 50,
onAbuseLimit: (retryAfter, options) => {
expect(options).to.include({ method: 'GET', url: '/route' })
expect(options.request.retryCount).to.equal(eventCount)
expect(retryAfter).to.equal(eventCount + 1)
eventCount++
return true
},
onRateLimit: () => 1
}
})
const message = 'You have been rate limited to prevent abuse'
try {
await octokit.request('GET /route', {
request: {
responses: [
{ status: 403, headers: { 'retry-after': '1' }, data: { message } },
{ status: 403, headers: { 'retry-after': '2' }, data: { message } },
{ status: 404, headers: { 'retry-after': '3' }, data: { message: 'Nope!' } }
]
}
})
throw new Error('Should not reach this point')
} catch (error) {
expect(error.status).to.equal(404)
expect(error.message).to.equal('Nope!')
}
expect(eventCount).to.equal(2)
expect(octokit.__requestLog).to.deep.equal([
'START GET /route',
'START GET /route',
'START GET /route'
])
expect(octokit.__requestTimings[1] - octokit.__requestTimings[0]).to.be.closeTo(50, 20)
expect(octokit.__requestTimings[2] - octokit.__requestTimings[1]).to.be.closeTo(100, 20)
})
it('Should retry \'rate-limit\' and succeed', async function () {
let eventCount = 0
const octokit = new Octokit({
throttle: {
onRateLimit: (retryAfter, options) => {
expect(options).to.include({ method: 'GET', url: '/route' })
expect(options.request.retryCount).to.equal(eventCount)
expect(retryAfter).to.equal(0)
eventCount++
return true
},
onAbuseLimit: () => 1
}
})
const res = await octokit.request('GET /route', {
request: {
responses: [
{ status: 403, headers: { 'x-ratelimit-remaining': '0', 'x-ratelimit-reset': `123` }, data: {} },
{ status: 202, headers: {}, data: { message: 'Yay!' } }
]
}
})
expect(res.status).to.equal(202)
expect(res.data).to.include({ message: 'Yay!' })
expect(eventCount).to.equal(1)
expect(octokit.__requestLog).to.deep.equal([
'START GET /route',
'START GET /route',
'END GET /route'
])
expect(octokit.__requestTimings[1] - octokit.__requestTimings[0]).to.be.closeTo(0, 20)
})
})
describe('GraphQL', function () {
it('Should retry \'rate-limit\' and succeed', async function () {
let eventCount = 0
const octokit = new Octokit({
throttle: {
write: new Bottleneck.Group({ minTime: 50 }),
onRateLimit: (retryAfter, options) => {
expect(options).to.include({ method: 'POST', url: '/graphql' })
expect(options.request.retryCount).to.equal(eventCount)
expect(retryAfter).to.equal(0)
eventCount++
return true
},
onAbuseLimit: () => 1
}
})
const res = await octokit.request('POST /graphql', {
request: {
responses: [
{ status: 200, headers: { 'x-ratelimit-remaining': '0', 'x-ratelimit-reset': `123` }, data: { errors: [{ type: 'RATE_LIMITED' }] } },
{ status: 200, headers: {}, data: { message: 'Yay!' } }
]
}
})
expect(res.status).to.equal(200)
expect(res.data).to.include({ message: 'Yay!' })
expect(eventCount).to.equal(1)
expect(octokit.__requestLog).to.deep.equal([
'START POST /graphql',
'END POST /graphql',
'START POST /graphql',
'END POST /graphql'
])
expect(octokit.__requestTimings[2] - octokit.__requestTimings[0]).to.be.closeTo(50, 20)
})
it('Should ignore other error types', async function () {
let eventCount = 0
const octokit = new Octokit({
throttle: {
write: new Bottleneck.Group({ minTime: 50 }),
onRateLimit: (retryAfter, options) => {
eventCount++
return true
},
onAbuseLimit: () => 1
}
})
const res = await octokit.request('POST /graphql', {
request: {
responses: [
{ status: 200, headers: { 'x-ratelimit-remaining': '0', 'x-ratelimit-reset': `123` }, data: { errors: [{ type: 'HELLO_WORLD' }] } },
{ status: 200, headers: {}, data: { message: 'Yay!' } }
]
}
})
expect(res.status).to.equal(200)
expect(res.data).to.deep.equal({ errors: [ { type: 'HELLO_WORLD' } ] })
expect(eventCount).to.equal(0)
expect(octokit.__requestLog).to.deep.equal([
'START POST /graphql',
'END POST /graphql'
])
})
})
})

View file

@ -0,0 +1,14 @@
const { iterate } = require('leakage')
const Octokit = require('@octokit/rest')
.plugin(require('..'))
const result = iterate(() => {
Octokit({
throttle: {
onAbuseLimit: () => {},
onRateLimit: () => {}
}
})
})
result.printSummary()