// Imports import {EventEmitter} from 'events'; import {ClientRequest, IncomingMessage} from 'http'; import * as https from 'https'; import {GithubApi} from '../../lib/common/github-api'; // Tests describe('GithubApi', () => { let api: GithubApi; beforeEach(() => api = new GithubApi('repo/slug', '12345')); describe('constructor()', () => { it('should throw if \'repoSlug\' is missing or empty', () => { expect(() => new GithubApi('', '12345')).toThrowError('Missing or empty required parameter \'repoSlug\'!'); }); it('should log a warning if \'githubToken\' is not defined or empty', () => { const warningMessage = 'No GitHub access-token specified. Requests will be unauthenticated.'; const consoleWarnSpy = spyOn(console, 'warn'); /* tslint:disable: no-unused-new */ new GithubApi('repo/slug'); new GithubApi('repo/slug', ''); /* tslint:enable: no-unused-new */ expect(consoleWarnSpy).toHaveBeenCalledTimes(2); expect(consoleWarnSpy.calls.argsFor(0)[0]).toBe(warningMessage); expect(consoleWarnSpy.calls.argsFor(1)[0]).toBe(warningMessage); }); }); describe('get()', () => { let apiBuildPathSpy: jasmine.Spy; let apiRequestSpy: jasmine.Spy; beforeEach(() => { apiBuildPathSpy = spyOn(api as any, 'buildPath'); apiRequestSpy = spyOn(api as any, 'request'); }); it('should call \'buildPath()\' with the pathname and params', () => { api.get('/foo', {bar: 'baz'}); expect(apiBuildPathSpy).toHaveBeenCalled(); expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]); }); it('should call \'request()\' with the correct method', () => { api.get('/foo'); expect(apiRequestSpy).toHaveBeenCalled(); expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('get'); }); it('should call \'request()\' with the correct path', () => { apiBuildPathSpy.and.returnValue('/foo/bar'); api.get('foo'); expect(apiRequestSpy).toHaveBeenCalled(); expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar'); }); it('should not pass data to \'request()\'', () => { (api.get as Function)('foo', {}, {}); expect(apiRequestSpy).toHaveBeenCalled(); expect(apiRequestSpy.calls.argsFor(0)[2]).toBeUndefined(); }); }); describe('post()', () => { let apiBuildPathSpy: jasmine.Spy; let apiRequestSpy: jasmine.Spy; beforeEach(() => { apiBuildPathSpy = spyOn(api as any, 'buildPath'); apiRequestSpy = spyOn(api as any, 'request'); }); it('should call \'buildPath()\' with the pathname and params', () => { api.post('/foo', {bar: 'baz'}); expect(apiBuildPathSpy).toHaveBeenCalled(); expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]); }); it('should call \'request()\' with the correct method', () => { api.post('/foo'); expect(apiRequestSpy).toHaveBeenCalled(); expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('post'); }); it('should call \'request()\' with the correct path', () => { apiBuildPathSpy.and.returnValue('/foo/bar'); api.post('/foo'); expect(apiRequestSpy).toHaveBeenCalled(); expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar'); }); it('should pass the data to \'request()\'', () => { api.post('/foo', {}, {bar: 'baz'}); expect(apiRequestSpy).toHaveBeenCalled(); expect(apiRequestSpy.calls.argsFor(0)[2]).toEqual({bar: 'baz'}); }); }); // Protected methods describe('buildPath()', () => { it('should return the pathname if no params', () => { expect((api as any).buildPath('/foo')).toBe('/foo'); expect((api as any).buildPath('/foo', undefined)).toBe('/foo'); expect((api as any).buildPath('/foo', null)).toBe('/foo'); }); it('should append the params to the pathname', () => { expect((api as any).buildPath('/foo', {bar: 'baz'})).toBe('/foo?bar=baz'); }); it('should join the params with \'&\'', () => { expect((api as any).buildPath('/foo', {bar: 1, baz: 2})).toBe('/foo?bar=1&baz=2'); }); it('should ignore undefined/null params', () => { expect((api as any).buildPath('/foo', {bar: undefined, baz: null})).toBe('/foo'); }); it('should encode param values as URI components', () => { expect((api as any).buildPath('/foo', {bar: 'b a&z'})).toBe('/foo?bar=b%20a%26z'); }); }); describe('request()', () => { let httpsRequestSpy: jasmine.Spy; let latestRequest: ClientRequest; beforeEach(() => { const originalRequest = https.request; httpsRequestSpy = spyOn(https, 'request').and.callFake((...args: any[]) => { latestRequest = originalRequest.apply(https, args); spyOn(latestRequest, 'on').and.callThrough(); spyOn(latestRequest, 'end'); return latestRequest; }); }); it('should return a promise', () => { expect((api as any).request()).toEqual(jasmine.any(Promise)); }); it('should call \'https.request()\' with the correct options', () => { (api as any).request('method', 'path'); expect(httpsRequestSpy).toHaveBeenCalled(); expect(httpsRequestSpy.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({ headers: jasmine.objectContaining({ 'User-Agent': `Node/${process.versions.node}`, }), host: 'api.github.com', method: 'method', path: 'path', })); }); it('should call specify an \'Authorization\' header if \'githubToken\' is present', () => { (api as any).request('method', 'path'); expect(httpsRequestSpy).toHaveBeenCalled(); expect(httpsRequestSpy.calls.argsFor(0)[0].headers).toEqual(jasmine.objectContaining({ Authorization: 'token 12345', })); }); it('should reject on request error', done => { (api as any).request('method', 'path').catch((err: any) => { expect(err).toBe('Test'); done(); }); latestRequest.emit('error', 'Test'); }); it('should send the request (i.e. call \'end()\')', () => { (api as any).request('method', 'path'); expect(latestRequest.end).toHaveBeenCalled(); }); it('should \'JSON.stringify\' and send the data along with the request', () => { (api as any).request('method', 'path'); expect(latestRequest.end).toHaveBeenCalledWith(null); (api as any).request('method', 'path', {key: 'value'}); expect(latestRequest.end).toHaveBeenCalledWith('{"key":"value"}'); }); describe('onResponse', () => { let promise: Promise; let respond: (statusCode: number) => IncomingMessage; beforeEach(() => { promise = (api as any).request('method', 'path'); respond = (statusCode: number) => { const mockResponse = new EventEmitter() as IncomingMessage; mockResponse.statusCode = statusCode; const onResponse = httpsRequestSpy.calls.argsFor(0)[1]; onResponse(mockResponse); return mockResponse; }; }); it('should reject on response error', done => { promise.catch(err => { expect(err).toBe('Test'); done(); }); const res = respond(200); res.emit('error', 'Test'); }); it('should reject if returned statusCode is <200', done => { promise.catch(err => { expect(err).toContain('failed'); expect(err).toContain('status: 199'); done(); }); const res = respond(199); res.emit('end'); }); it('should reject if returned statusCode is >=400', done => { promise.catch(err => { expect(err).toContain('failed'); expect(err).toContain('status: 400'); done(); }); const res = respond(400); res.emit('end'); }); it('should include the response text in the rejection message', done => { promise.catch(err => { expect(err).toContain('Test'); done(); }); const res = respond(500); res.emit('data', 'Test'); res.emit('end'); }); it('should resolve if returned statusCode is <=200 <400', done => { promise.then(done); const res = respond(200); res.emit('data', '{}'); res.emit('end'); }); it('should resolve with the response text \'JSON.parsed\'', done => { promise.then(data => { expect(data).toEqual({foo: 'bar'}); done(); }); const res = respond(300); res.emit('data', '{"foo":"bar"}'); res.emit('end'); }); it('should collect and concatenate the whole response text', done => { promise.then(data => { expect(data).toEqual({foo: 'bar', baz: 'qux'}); done(); }); const res = respond(300); res.emit('data', '{"foo":'); res.emit('data', '"bar","baz"'); res.emit('data', ':"qux"}'); res.emit('end'); }); it('should reject if the response text is malformed JSON', done => { promise.catch(err => { expect(err).toEqual(jasmine.any(SyntaxError)); done(); }); const res = respond(300); res.emit('data', '}'); res.emit('end'); }); }); }); });