diff --git a/.travis.yml b/.travis.yml index c4180ab..a93caa0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,4 @@ script: after_success: npm run coverage node_js: - - "8" + - "8.4.0" diff --git a/index.js b/index.js index 2ea1092..d3a84b8 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ const Promise = require('bluebird'); const request = Promise.promisify(require('request')); +const { isURL } = require('validator'); const { UnexpectedStatusCodeError, TrembitaError } = require('./error'); @@ -28,16 +29,48 @@ const Trembita = class Trembita { return this.raw(options) }; - if (!options) { throw new TrembitaError('missing options'); } - if (!options.endpoint) { throw new TrembitaError('missing endpoint'); } + Trembita._validateOptions(options); + Trembita._validateEndpoint(options.endpoint); this.endpoint = options.endpoint; - this.log = options.log || console; + this.log = options.log || console; // TODO: add more loggers this.client = request.defaults({ baseUrl: this.endpoint, json: true, }); } + + /** + * Options validator + * @method _validateOptions + * @param {Object} options object comes from plugin, includes required endpoint + * @return {TrembitaError} errors: missing options, options is not an object + */ + static _validateOptions (options) { + if (!options) { throw new TrembitaError('missing options'); } + if (!isObject(options)) {throw new TrembitaError('options is not an object');} + + function isObject(value) { + const type = typeof value + return value !== null && (type === 'object' || type === 'function') + } + } + + /** + * Endpoint validator + * @method _validateEndpoint + * @param {String} endpoint API + * @return {TrembitaError} errors: missing endpoint, endpoint is not string, endpoint is not valid url + */ + static _validateEndpoint (endpoint) { + if (!endpoint) { throw new TrembitaError('missing endpoint'); } + if (typeof endpoint !== 'string') { throw new TrembitaError('endpoint is not string'); } + if (!isURL(endpoint, { + protocols: ['http', 'https'], + require_protocol: true, // eslint-disable-line camelcase + require_host: true, // eslint-disable-line camelcase + })) { throw new TrembitaError('endpoint is not valid url') } + } }; module.exports = Trembita; diff --git a/package.json b/package.json index 35c2bfd..e84f035 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "license": "MIT", "dependencies": { "bluebird": "^3.0.6", - "request": "^2.67.0" + "request": "^2.67.0", + "validator": "^9.2.0" }, "devDependencies": { "chai": "^4.1.2", diff --git a/test/index.js b/test/index.js index 74cd6a5..41c8ad9 100644 --- a/test/index.js +++ b/test/index.js @@ -5,148 +5,190 @@ const helpers = require('./helpers'); const Trembita = require('../'); const { UnexpectedStatusCodeError } = require('../error'); + + describe('Trembita:', () => { - let scope; + let scope, expectedBody, trembita; const clientOptions = { endpoint: 'https://example.com/api', log: helpers.log }; - before(() => { return tbita = new Trembita(clientOptions); }); beforeEach(() => { - // set up an HTTP mock - return scope = nock(clientOptions.endpoint); + expectedBody = { + page: 2, + per_page: 3, + total: 12, + total_pages: 4, + data: [{ + id: 4, + first_name: 'fName', + last_name: 'lName' + }, + { + id: 5, + first_name: 'fName', + last_name: 'lName' + }, + { + id: 6, + first_name: 'fName', + last_name: 'lName' + }] + }; + + trembita = new Trembita(clientOptions); + scope = nock(clientOptions.endpoint); }); afterEach(() => { return scope.done(); }); - it( - 'should be created with six properties: raw, request, endpoint, log, client', () => { - expect(tbita).to.have.property('raw'); - expect(tbita).to.have.property('request'); - expect(tbita).to.have.property('endpoint'); - expect(tbita).to.have.property('log'); - expect(tbita).to.have.property('client'); - }); + describe('constructor', () => { + it( + 'should be created with six properties: raw, request, endpoint, log, client', () => { + expect(trembita).to.have.property('raw'); + expect(trembita).to.have.property('request'); + expect(trembita).to.have.property('endpoint'); + expect(trembita).to.have.property('log'); + expect(trembita).to.have.property('client'); + }); - it('should fail if options are not provided', () => { - expect(() => new Trembita()).to.throw('missing options') - }) + it('should fail if options are not provided', () => { + expect(() => new Trembita()).to.throw('missing options') + }) - it('should fail if endpoint is not provided', () => { - expect(() => new Trembita({})).to.throw('missing endpoint') - }) + it('should fail if options are not provided', () => { + expect(() => new Trembita(1)).to.throw('options is not an object') + }) - it('should provide default logger logger is not provided', () => { - const tbita = new Trembita({ - endpoint: 'https://example.com/api', + it('should fail if endpoint is not provided', () => { + expect(() => new Trembita({})).to.throw('missing endpoint') }) - expect(tbita).to.have.property('log'); - }) - it('should return status code 200 and resourse', () => { - scope - .get('/users?page=2') - .replyWithFile(200, __dirname + - '/responses/get-users-page-2.json') - - return tbita - .request({ - url: '/users', - qs: { - page: 2 - }, - expectedCodes: [200] + it('should fail if endpoint is not string', () => { + expect(() => new Trembita({ + endpoint: 1 + })).to.throw('endpoint is not string') + }) + + it('should fail if endpoint is not valid url', () => { + expect(() => new Trembita({ + endpoint: '!url' + })).to.throw('endpoint is not valid url') + }) + + it('should fail if endpoint is not supported', () => { + expect(() => new Trembita({ + endpoint: 'ftp://example.com' + })).to.throw('endpoint is not valid url') + }) + + it('should fail if protocol is missing', () => { + expect(() => new Trembita({ + endpoint: 'example.com' + })).to.throw('endpoint is not valid url') + }) + + it('should fail if host is missing', () => { + expect(() => new Trembita({ + endpoint: 'http://' + })).to.throw('endpoint is not valid url') + }) + + it('should provide default logger logger is not provided', () => { + const trembita = new Trembita({ + endpoint: 'https://example.com/api', }) - .then(res => { - expect(res).to.deep.equal({ - page: 2, - per_page: 3, - total: 12, - total_pages: 4, - data: [{ - id: 4, - first_name: 'fName', - last_name: 'lName' - }, - { - id: 5, - first_name: 'fName', - last_name: 'lName' - }, - { - id: 6, - first_name: 'fName', - last_name: 'lName' - }] + expect(trembita).to.have.property('log'); + }) + }) + + describe('trembita.client', () => { + it('should return status code 200 and resourse', () => { + scope + .get('/users?page=2') + .replyWithFile(200, __dirname + + '/responses/get-users-page-2.json') + + return trembita + .client({ + url: '/users', + qs: { + page: 2 + }, + expectedCodes: [200] }) - }); - }); + .then(res => { + expect(res.statusCode).to.be.equal(200) + expect(res.body).to.deep.equal(expectedBody) + }); + }); + }) - it('should return status code 200 and resourse if expectedCodes arent provided', () => { - scope - .get('/users?page=2') - .replyWithFile(200, __dirname + - '/responses/get-users-page-2.json') - - return tbita - .request({ - url: '/users', - qs: { - page: 2 - }, - // expectedCodes: [200] - }) - .then(res => { - expect(res).to.deep.equal({ - page: 2, - per_page: 3, - total: 12, - total_pages: 4, - data: [{ - id: 4, - first_name: 'fName', - last_name: 'lName' - }, - { - id: 5, - first_name: 'fName', - last_name: 'lName' - }, - { - id: 6, - first_name: 'fName', - last_name: 'lName' - }] + describe('trembita.request', () => { + it('should return status code 200 and resourse', () => { + scope + .get('/users?page=2') + .replyWithFile(200, __dirname + + '/responses/get-users-page-2.json') + + return trembita + .request({ + url: '/users', + qs: { + page: 2 + }, + expectedCodes: [200] }) - }); - }); + .then(res => { + expect(res).to.deep.equal(expectedBody) + }); + }); - it('should return 404 status code', () => { - scope - .get('/profiles') - .reply(404) + it('should return status code 200 and resourse if expectedCodes arent provided', () => { + scope + .get('/users?page=2') + .replyWithFile(200, __dirname + + '/responses/get-users-page-2.json') - return tbita - .request({ - url: '/profiles', - expectedCodes: [404] - }) - }); + return trembita + .request({ + url: '/users', + qs: { + page: 2 + } + }) + .then(res => { + expect(res).to.deep.equal(expectedBody) + }); + }); - it('should return error related to unexpected status code', () => { - scope - .get('/profiles/1') - .reply(404) + it('should return 404 status code', () => { + scope + .get('/profiles') + .reply(404) - return tbita - .request({ - url: '/profiles/1', - expectedCodes: [200] - }) - .catch(UnexpectedStatusCodeError, (err) => { - const message = `Unexpected status code: 404, Body: undefined, Options: { url: \'/profiles/1\', expectedCodes: [ 200 ] }` - expect(err.message).to.equal(message) - expect(err.toJSON()).to.deep.equal({ message }) - }) - }); + return trembita + .request({ + url: '/profiles', + expectedCodes: [404] + }) + }); + + it('should return error related to unexpected status code', () => { + scope + .get('/profiles/1') + .reply(404) + + return trembita + .request({ + url: '/profiles/1', + expectedCodes: [200] + }) + .catch(UnexpectedStatusCodeError, (err) => { + const message = `Unexpected status code: 404, Body: undefined, Options: { url: \'/profiles/1\', expectedCodes: [ 200 ] }` + expect(err.message).to.equal(message) + expect(err.toJSON()).to.deep.equal({ message }) + }) + }); + }) }); diff --git a/yarn.lock b/yarn.lock index 0cfb807..c432aa9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2472,6 +2472,10 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" +validator@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-9.2.0.tgz#ad216eed5f37cac31a6fe00ceab1f6b88bded03e" + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"