diff options
-rw-r--r-- | app/assets/javascripts/lib/utils/ajax_cache.js | 18 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/cache.js | 19 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/users_cache.js | 28 | ||||
-rw-r--r-- | spec/javascripts/lib/utils/cache_spec.js | 65 | ||||
-rw-r--r-- | spec/javascripts/lib/utils/users_cache_spec.js | 136 |
5 files changed, 252 insertions, 14 deletions
diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js index cf030d613df..f1fe95e12e8 100644 --- a/app/assets/javascripts/lib/utils/ajax_cache.js +++ b/app/assets/javascripts/lib/utils/ajax_cache.js @@ -1,21 +1,11 @@ -class AjaxCache { +import Cache from './cache'; + +class AjaxCache extends Cache { constructor() { - this.internalStorage = { }; + super(); this.pendingRequests = { }; } - get(endpoint) { - return this.internalStorage[endpoint]; - } - - hasData(endpoint) { - return Object.prototype.hasOwnProperty.call(this.internalStorage, endpoint); - } - - remove(endpoint) { - delete this.internalStorage[endpoint]; - } - retrieve(endpoint) { if (this.hasData(endpoint)) { return Promise.resolve(this.get(endpoint)); diff --git a/app/assets/javascripts/lib/utils/cache.js b/app/assets/javascripts/lib/utils/cache.js new file mode 100644 index 00000000000..3141f1eeafc --- /dev/null +++ b/app/assets/javascripts/lib/utils/cache.js @@ -0,0 +1,19 @@ +class Cache { + constructor() { + this.internalStorage = { }; + } + + get(key) { + return this.internalStorage[key]; + } + + hasData(key) { + return Object.prototype.hasOwnProperty.call(this.internalStorage, key); + } + + remove(key) { + delete this.internalStorage[key]; + } +} + +export default Cache; diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js new file mode 100644 index 00000000000..88f8a622c00 --- /dev/null +++ b/app/assets/javascripts/lib/utils/users_cache.js @@ -0,0 +1,28 @@ +import Api from '../../api'; +import Cache from './cache'; + +class UsersCache extends Cache { + retrieve(username) { + if (this.hasData(username)) { + return Promise.resolve(this.get(username)); + } + + return Api.users('', { username }) + .then((users) => { + if (!users.length) { + throw new Error(`User "${username}" could not be found!`); + } + + if (users.length > 1) { + throw new Error(`Expected username "${username}" to be unique!`); + } + + const user = users[0]; + this.internalStorage[username] = user; + return user; + }); + // missing catch is intentional, error handling depends on use case + } +} + +export default new UsersCache(); diff --git a/spec/javascripts/lib/utils/cache_spec.js b/spec/javascripts/lib/utils/cache_spec.js new file mode 100644 index 00000000000..2fe02a7592c --- /dev/null +++ b/spec/javascripts/lib/utils/cache_spec.js @@ -0,0 +1,65 @@ +import Cache from '~/lib/utils/cache'; + +describe('Cache', () => { + const dummyKey = 'just some key'; + const dummyValue = 'more than a value'; + let cache; + + beforeEach(() => { + cache = new Cache(); + }); + + describe('get', () => { + it('return cached data', () => { + cache.internalStorage[dummyKey] = dummyValue; + + expect(cache.get(dummyKey)).toBe(dummyValue); + }); + + it('returns undefined for missing data', () => { + expect(cache.internalStorage[dummyKey]).toBe(undefined); + expect(cache.get(dummyKey)).toBe(undefined); + }); + }); + + describe('hasData', () => { + it('return true for cached data', () => { + cache.internalStorage[dummyKey] = dummyValue; + + expect(cache.hasData(dummyKey)).toBe(true); + }); + + it('returns false for missing data', () => { + expect(cache.internalStorage[dummyKey]).toBe(undefined); + expect(cache.hasData(dummyKey)).toBe(false); + }); + }); + + describe('remove', () => { + it('removes data from cache', () => { + cache.internalStorage[dummyKey] = dummyValue; + + cache.remove(dummyKey); + + expect(cache.internalStorage[dummyKey]).toBe(undefined); + }); + + it('does nothing for missing data', () => { + expect(cache.internalStorage[dummyKey]).toBe(undefined); + + cache.remove(dummyKey); + + expect(cache.internalStorage[dummyKey]).toBe(undefined); + }); + + it('does not remove wrong data', () => { + cache.internalStorage[dummyKey] = dummyValue; + cache.internalStorage[dummyKey + dummyKey] = dummyValue + dummyValue; + + cache.remove(dummyKey); + + expect(cache.internalStorage[dummyKey]).toBe(undefined); + expect(cache.internalStorage[dummyKey + dummyKey]).toBe(dummyValue + dummyValue); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/users_cache_spec.js b/spec/javascripts/lib/utils/users_cache_spec.js new file mode 100644 index 00000000000..ec6ea35952b --- /dev/null +++ b/spec/javascripts/lib/utils/users_cache_spec.js @@ -0,0 +1,136 @@ +import Api from '~/api'; +import UsersCache from '~/lib/utils/users_cache'; + +describe('UsersCache', () => { + const dummyUsername = 'win'; + const dummyUser = 'has a farm'; + + beforeEach(() => { + UsersCache.internalStorage = { }; + }); + + describe('get', () => { + it('returns undefined for empty cache', () => { + expect(UsersCache.internalStorage).toEqual({ }); + + const user = UsersCache.get(dummyUsername); + + expect(user).toBe(undefined); + }); + + it('returns undefined for missing user', () => { + UsersCache.internalStorage['no body'] = 'no data'; + + const user = UsersCache.get(dummyUsername); + + expect(user).toBe(undefined); + }); + + it('returns matching user', () => { + UsersCache.internalStorage[dummyUsername] = dummyUser; + + const user = UsersCache.get(dummyUsername); + + expect(user).toBe(dummyUser); + }); + }); + + describe('hasData', () => { + it('returns false for empty cache', () => { + expect(UsersCache.internalStorage).toEqual({ }); + + expect(UsersCache.hasData(dummyUsername)).toBe(false); + }); + + it('returns false for missing user', () => { + UsersCache.internalStorage['no body'] = 'no data'; + + expect(UsersCache.hasData(dummyUsername)).toBe(false); + }); + + it('returns true for matching user', () => { + UsersCache.internalStorage[dummyUsername] = dummyUser; + + expect(UsersCache.hasData(dummyUsername)).toBe(true); + }); + }); + + describe('remove', () => { + it('does nothing if cache is empty', () => { + expect(UsersCache.internalStorage).toEqual({ }); + + UsersCache.remove(dummyUsername); + + expect(UsersCache.internalStorage).toEqual({ }); + }); + + it('does nothing if cache contains no matching data', () => { + UsersCache.internalStorage['no body'] = 'no data'; + + UsersCache.remove(dummyUsername); + + expect(UsersCache.internalStorage['no body']).toBe('no data'); + }); + + it('removes matching data', () => { + UsersCache.internalStorage[dummyUsername] = dummyUser; + + UsersCache.remove(dummyUsername); + + expect(UsersCache.internalStorage).toEqual({ }); + }); + }); + + describe('retrieve', () => { + let apiSpy; + + beforeEach(() => { + spyOn(Api, 'users').and.callFake((query, options) => apiSpy(query, options)); + }); + + it('stores and returns data from API call if cache is empty', (done) => { + apiSpy = (query, options) => { + expect(query).toBe(''); + expect(options).toEqual({ username: dummyUsername }); + return Promise.resolve([dummyUser]); + }; + + UsersCache.retrieve(dummyUsername) + .then((user) => { + expect(user).toBe(dummyUser); + expect(UsersCache.internalStorage[dummyUsername]).toBe(dummyUser); + }) + .then(done) + .catch(done.fail); + }); + + it('returns undefined if Ajax call fails and cache is empty', (done) => { + const dummyError = new Error('server exploded'); + apiSpy = (query, options) => { + expect(query).toBe(''); + expect(options).toEqual({ username: dummyUsername }); + return Promise.reject(dummyError); + }; + + UsersCache.retrieve(dummyUsername) + .then(user => fail(`Received unexpected user: ${JSON.stringify(user)}`)) + .catch((error) => { + expect(error).toBe(dummyError); + }) + .then(done) + .catch(done.fail); + }); + + it('makes no Ajax call if matching data exists', (done) => { + UsersCache.internalStorage[dummyUsername] = dummyUser; + apiSpy = () => fail(new Error('expected no Ajax call!')); + + UsersCache.retrieve(dummyUsername) + .then((user) => { + expect(user).toBe(dummyUser); + }) + .then(done) + .catch(done.fail); + }); + }); +}); |