Skip to content

Commit

Permalink
Merge pull request #15 from wearereasonablepeople/oleg-koval-gh#14
Browse files Browse the repository at this point in the history
fix: add options & endpoint validations, more tests
  • Loading branch information
syllerim authored Jan 19, 2018
2 parents ad9563d + cba804c commit 826ea14
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 126 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ script:
after_success: npm run coverage

node_js:
- "8"
- "8.4.0"
39 changes: 36 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const Promise = require('bluebird');
const request = Promise.promisify(require('request'));
const { isURL } = require('validator');

const { UnexpectedStatusCodeError, TrembitaError } = require('./error');

Expand Down Expand Up @@ -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;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
284 changes: 163 additions & 121 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
})
});
})
});
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"

[email protected]:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
Expand Down

0 comments on commit 826ea14

Please sign in to comment.