Skip to content
This repository has been archived by the owner on Nov 5, 2019. It is now read-only.

Commit

Permalink
Merge branch 'cache'
Browse files Browse the repository at this point in the history
  • Loading branch information
danielbayerlein committed Mar 16, 2017
2 parents f00c0d1 + 5d5be59 commit bf30ff3
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 37 deletions.
111 changes: 87 additions & 24 deletions __tests__/github.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint global-require: 0 */

describe('github.js', () => {
beforeEach(() => {
jest.resetAllMocks();
Expand All @@ -7,15 +9,25 @@ describe('github.js', () => {
describe('search', () => {
let got;
let github;
let cache;

const mockResult = require('../__mocks__/result.json').items.map(repository => ({
title: repository.full_name,
value: repository.html_url,
subtitle: repository.description,
}));

beforeEach(() => {
jest.mock('got');
got = require('got'); // eslint-disable-line global-require
github = require('../src/github'); // eslint-disable-line global-require
console.error = jest.fn(); // eslint-disable-line no-console
got = require('got');

jest.mock('cache-conf');
cache = { get: jest.fn(), isExpired: jest.fn(), set: jest.fn() };
require('cache-conf').mockImplementation(() => cache);

github = require('../src/github');

got.mockImplementation(() => new Promise(resolve => resolve({
// eslint-disable-next-line global-require
body: require('../__mocks__/result.json'),
})));
});
Expand All @@ -41,35 +53,35 @@ describe('github.js', () => {

test('returns an array', () => (
github.search('honeycomb')
.then((packages) => {
expect(packages).toBeInstanceOf(Array);
.then((repositories) => {
expect(repositories).toBeInstanceOf(Array);
})
));

test('returns the expected title', () => (
github.search('honeycomb')
.then((packages) => {
expect(packages[0].title).toBe('altamiracorp/honeycomb');
.then((repositories) => {
expect(repositories[0].title).toBe('altamiracorp/honeycomb');
})
));

test('returns the expected value', () => (
github.search('honeycomb')
.then((packages) => {
expect(packages[0].value).toBe(
.then((repositories) => {
expect(repositories[0].value).toBe(
'https://github.com/altamiracorp/honeycomb',
);
})
));

test('returns the expected subtitle', () => (
github.search('honeycomb')
.then((packages) => {
expect(packages[0].subtitle).toBe('MySql storage engine for the cloud');
.then((repositories) => {
expect(repositories[0].subtitle).toBe('MySql storage engine for the cloud');
})
));

test('call console.error with an error message', () => {
test('returns the expected error', () => {
const body = `
{
"message": "Validation Failed",
Expand All @@ -88,40 +100,91 @@ describe('github.js', () => {
response: { body },
})));

return github.search('honeycomb')
.catch((repositories) => {
expect(repositories.response.body).toBe(body);
});
});

test('call cache.get with the expected arguments', () => (
github.search('honeycomb')
.then(() => {
expect(cache.get).toBeCalledWith(
'zazu-github.honeycomb',
{ ignoreMaxAge: true },
);
})
));

test('call cache.set with the expected arguments', () => (
github.search('honeycomb')
.then(() => {
expect(cache.set).toBeCalledWith(
'zazu-github.honeycomb',
mockResult,
{ maxAge: 3600000 },
);
})
));

test('call cache.isExpired with the expected argument', () => {
cache.get = jest.fn(() => mockResult);

return github.search('honeycomb')
.then(() => {
// eslint-disable-next-line no-console
expect(console.error).toHaveBeenCalledWith(body);
expect(cache.isExpired).toBeCalledWith('zazu-github.honeycomb');
});
});

test('returns the cache result', () => {
cache.isExpired = jest.fn(() => false);
cache.get = jest.fn(() => mockResult);

return github.search('honeycomb')
.then((repositories) => {
expect(repositories).toEqual(mockResult);
});
});

test('returns the cache result when an error occurs', () => {
cache.isExpired = jest.fn(() => true);
cache.get = jest.fn(() => mockResult);
got.mockImplementation(() => new Promise((resolve, reject) => reject()));

return github.search('honeycomb')
.then((repositories) => {
expect(repositories).toEqual(mockResult);
});
});
});

describe('integration', () => {
// eslint-disable-next-line global-require
jest.mock('cache-conf');

const github = require('../src/github');
const searchResult = github.search('honeycomb');

test('returns an array', () => (
searchResult.then((packages) => {
expect(packages).toBeInstanceOf(Array);
searchResult.then((repositories) => {
expect(repositories).toBeInstanceOf(Array);
})
));

test('returns an object with a title', () => (
searchResult.then((packages) => {
expect(packages[0].title).toBeDefined();
searchResult.then((repositories) => {
expect(repositories[0].title).toBeDefined();
})
));

test('returns an object with a value', () => (
searchResult.then((packages) => {
expect(packages[0].value).toBeDefined();
searchResult.then((repositories) => {
expect(repositories[0].value).toBeDefined();
})
));

test('returns an object with a subtitle', () => (
searchResult.then((packages) => {
expect(packages[0].subtitle).toBeDefined();
searchResult.then((repositories) => {
expect(repositories[0].subtitle).toBeDefined();
})
));
});
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@
},
"homepage": "https://github.com/danielbayerlein/zazu-github#readme",
"devDependencies": {
"eslint": "^3.17.0",
"eslint": "^3.17.1",
"eslint-config-airbnb-base": "^11.1.1",
"eslint-formatter-pretty": "^1.1.0",
"eslint-plugin-import": "^2.2.0",
"husky": "^0.13.2",
"jest": "^19.0.2"
},
"dependencies": {
"cache-conf": "^0.5.0",
"got": "^6.7.1"
},
"jest": {
Expand Down
48 changes: 39 additions & 9 deletions src/github.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
const got = require('got');
const CacheConf = require('cache-conf');

const URL = 'https://api.github.com/search/repositories';
const RESULT_ITEMS = 10;

const CACHE_CONF = {
key: 'zazu-github', // cache key prefix
maxAge: 3600000, // 1 hour
};

const cache = new CacheConf();

/**
* Fetch the URL, cache the result and return it.
* Returns the cache result if it is valid.
*
* @param {string} query Search query
* @return {Promise} Returns a promise that is fulfilled with the JSON result
*/
module.exports.search = (query) => {
const options = {
json: true,
Expand All @@ -15,17 +30,32 @@ module.exports.search = (query) => {
},
};

return got(URL, options)
.then(response => (
response.body.items.map(repository => (
{
const cacheKey = `${CACHE_CONF.key}.${query}`;
const cachedResponse = cache.get(cacheKey, { ignoreMaxAge: true });

if (cachedResponse && !cache.isExpired(cacheKey)) {
return Promise.resolve(cachedResponse);
}

return new Promise((resolve, reject) => (
got(URL, options)
.then((response) => {
const data = response.body.items.map(repository => ({
title: repository.full_name,
value: repository.html_url,
subtitle: repository.description,
}));

cache.set(cacheKey, data, { maxAge: CACHE_CONF.maxAge });

resolve(data);
})
.catch((error) => {
if (cachedResponse) {
resolve(cachedResponse);
}
))
))
.catch((error) => {
console.error(error.response.body); // eslint-disable-line no-console
});

reject(error);
})
));
};
36 changes: 33 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,13 @@ builtin-modules@^1.0.0, builtin-modules@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"

cache-conf@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/cache-conf/-/cache-conf-0.5.0.tgz#681c42ed1771ead2bc23632a75d3cd8dc7945c59"
dependencies:
conf "^0.12.0"
pkg-up "^1.0.0"

caller-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
Expand Down Expand Up @@ -476,6 +483,15 @@ concat-stream@^1.4.6:
readable-stream "^2.2.2"
typedarray "^0.0.6"

conf@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/conf/-/conf-0.12.0.tgz#8498c599e2487fec703505d181c113875b8c310c"
dependencies:
dot-prop "^4.1.0"
env-paths "^1.0.0"
mkdirp "^0.5.1"
pkg-up "^1.0.0"

contains-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
Expand Down Expand Up @@ -589,6 +605,12 @@ [email protected], doctrine@^1.2.2:
esutils "^2.0.2"
isarray "^1.0.0"

dot-prop@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.1.1.tgz#a8493f0b7b5eeec82525b5c7587fa7de7ca859c1"
dependencies:
is-obj "^1.0.0"

duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
Expand All @@ -599,6 +621,10 @@ ecc-jsbn@~0.1.1:
dependencies:
jsbn "~0.1.0"

env-paths@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"

"errno@>=0.1.1 <0.2.0-0":
version "0.1.4"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
Expand Down Expand Up @@ -731,9 +757,9 @@ eslint-plugin-import@^2.2.0:
minimatch "^3.0.3"
pkg-up "^1.0.0"

eslint@^3.17.0:
version "3.17.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.17.0.tgz#e2704b09c5bae9fb49ee8bafeea3832c7257d498"
eslint@^3.17.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.17.1.tgz#b80ae12d9c406d858406fccda627afce33ea10ea"
dependencies:
babel-code-frame "^6.16.0"
chalk "^1.1.3"
Expand Down Expand Up @@ -1265,6 +1291,10 @@ is-number@^2.0.2, is-number@^2.1.0:
dependencies:
kind-of "^3.0.2"

is-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"

is-path-cwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
Expand Down

0 comments on commit bf30ff3

Please sign in to comment.