import { expect } from 'chai' import sinon from 'sinon' import GithubApiProvider from './github-api-provider.js' describe('Github API provider', function () { const baseUrl = 'https://github-api.example.com' const reserveFraction = 0.333 let mockStandardToken, mockSearchToken, mockGraphqlToken, provider beforeEach(function () { provider = new GithubApiProvider({ baseUrl, reserveFraction }) mockStandardToken = { update: sinon.spy(), invalidate: sinon.spy() } sinon.stub(provider.standardTokens, 'next').returns(mockStandardToken) mockSearchToken = { update: sinon.spy(), invalidate: sinon.spy() } sinon.stub(provider.searchTokens, 'next').returns(mockSearchToken) mockGraphqlToken = { update: sinon.spy(), invalidate: sinon.spy() } sinon.stub(provider.graphqlTokens, 'next').returns(mockGraphqlToken) }) context('a search API request', function () { it('should obtain an appropriate token', async function () { const mockResponse = { res: { headers: {} } } const mockRequest = sinon.stub().resolves(mockResponse) await provider.fetch(mockRequest, '/search', {}) expect(provider.searchTokens.next).to.have.been.calledOnce expect(provider.standardTokens.next).not.to.have.been.called expect(provider.graphqlTokens.next).not.to.have.been.called }) }) context('a graphql API request', function () { it('should obtain an appropriate token', async function () { const mockResponse = { res: { headers: {} } } const mockRequest = sinon.stub().resolves(mockResponse) await provider.fetch(mockRequest, '/graphql', {}) expect(provider.searchTokens.next).not.to.have.been.called expect(provider.standardTokens.next).not.to.have.been.called expect(provider.graphqlTokens.next).to.have.been.calledOnce }) }) context('a core API request', function () { it('should obtain an appropriate token', async function () { const mockResponse = { res: { headers: {} } } const mockRequest = sinon.stub().resolves(mockResponse) await provider.fetch(mockRequest, '/repo', {}) expect(provider.searchTokens.next).not.to.have.been.called expect(provider.standardTokens.next).to.have.been.calledOnce expect(provider.graphqlTokens.next).not.to.have.been.called }) }) context('a valid V3 API response', function () { const rateLimit = 12500 const remaining = 7955 const nextReset = 123456789 const mockResponse = { res: { statusCode: 200, headers: { 'x-ratelimit-limit': rateLimit, 'x-ratelimit-remaining': remaining, 'x-ratelimit-reset': nextReset, }, buffer: Buffer.alloc(0), }, } const mockRequest = sinon.stub().resolves(mockResponse) it('should return the response', async function () { const res = await provider.fetch(mockRequest, '/repo', {}) expect(Object.is(res, mockResponse)).to.be.true }) it('should update the token with the expected values', async function () { await provider.fetch(mockRequest, '/foo', {}) const expectedUsesRemaining = remaining - Math.ceil(reserveFraction * rateLimit) expect(mockStandardToken.update).to.have.been.calledWith( expectedUsesRemaining, nextReset ) expect(mockStandardToken.invalidate).not.to.have.been.called }) }) context('a valid V4 API response', function () { const rateLimit = 12500 const remaining = 7955 const nextReset = 123456789 const mockResponse = { res: { statusCode: 200, headers: {}, body: `{ "data": { "rateLimit": { "limit": 12500, "cost": 1, "remaining": 7955, "resetAt": "1973-11-29T21:33:09Z" } } }`, }, } const mockRequest = sinon.stub().resolves(mockResponse) it('should return the response', async function () { const res = await provider.fetch(mockRequest, '/graphql', {}) expect(Object.is(res, mockResponse)).to.be.true }) it('should update the token with the expected values', async function () { await provider.fetch(mockRequest, '/graphql', {}) const expectedUsesRemaining = remaining - Math.ceil(reserveFraction * rateLimit) expect(mockGraphqlToken.update).to.have.been.calledWith( expectedUsesRemaining, nextReset ) expect(mockGraphqlToken.invalidate).not.to.have.been.called }) }) context('unauthorized API responses', function () { it('should invoke the callback and update the token with the expected values (unauthorized, v3)', async function () { const mockResponse = { res: { statusCode: 401, headers: {} } } const mockRequest = sinon.stub().resolves(mockResponse) await provider.fetch(mockRequest, '/foo', {}) expect(mockStandardToken.invalidate).to.have.been.calledOnce expect(mockStandardToken.update).not.to.have.been.called }) it('should invoke the callback and update the token with the expected values (unauthorized, v4)', async function () { const mockResponse = { res: { statusCode: 401, body: {} } } const mockRequest = sinon.stub().resolves(mockResponse) await provider.fetch(mockRequest, '/graphql', {}) expect(mockGraphqlToken.invalidate).to.have.been.calledOnce expect(mockGraphqlToken.update).not.to.have.been.called }) it('should invoke the callback and update the token with the expected values (suspended, v4)', async function () { const mockResponse = { res: { statusCode: 200, body: '{ "message": "Sorry. Your account was suspended." }', }, } const mockRequest = sinon.stub().resolves(mockResponse) await provider.fetch(mockRequest, '/graphql', {}) expect(mockGraphqlToken.invalidate).to.have.been.calledOnce expect(mockGraphqlToken.update).not.to.have.been.called }) }) context('a connection error', function () { it('should throw an exception', function () { const msg = 'connection timeout' const requestError = new Error(msg) const mockRequest = sinon.stub().rejects(requestError) return expect(provider.fetch(mockRequest, '/foo', {})).to.be.rejectedWith( Error, 'connection timeout' ) }) }) })