diff --git a/lib/cache/cacheProvider/factory.ts b/lib/cache/cacheProvider/factory.ts index e2f1e16..3e1097e 100644 --- a/lib/cache/cacheProvider/factory.ts +++ b/lib/cache/cacheProvider/factory.ts @@ -14,22 +14,14 @@ export class CacheProviderFactory implements Factory { public create() { switch (this.scope) { case 'class': - return this.classCacheProvider(); + return new ClassCacheProvider(this.cacheFactory); case 'instance': - return this.instanceCacheProvider(); + return new InstanceCacheProvider(this.cacheFactory); default: throw new Error(`@cache invalid scope option: ${this.scope}.`); } } - private classCacheProvider(): ClassCacheProvider { - return new ClassCacheProvider(this.cacheFactory); - } - - private instanceCacheProvider(): InstanceCacheProvider { - return new InstanceCacheProvider(this.cacheFactory); - } - } diff --git a/lib/cache/expirations/SlidingExpiration.ts b/lib/cache/expirations/SlidingExpiration.ts index 82b92fe..9f944d6 100644 --- a/lib/cache/expirations/SlidingExpiration.ts +++ b/lib/cache/expirations/SlidingExpiration.ts @@ -9,7 +9,11 @@ export class SlidingExpiration implements Expiration { ) { } public add(key: string, clearCallback: (key: string) => unknown): void { - this.expirations.has(key) ? this.update(key, clearCallback) : this.addKey(key, clearCallback); + if (this.expirations.has(key)) { + this.update(key, clearCallback); + } else { + this.addKey(key, clearCallback); + } } private addKey(key: string, clearCallback: (key: string) => unknown): void { diff --git a/lib/cache/expirations/factory.ts b/lib/cache/expirations/factory.ts index f6ec58a..9f5d9f2 100644 --- a/lib/cache/expirations/factory.ts +++ b/lib/cache/expirations/factory.ts @@ -13,22 +13,14 @@ export class ExpirationFactory implements Factory { public create(): Expiration { switch (this.expiration) { case 'absolute': - return this.absoluteExpirtation(); + return new AbsoluteExpiration(this.timeout); case 'sliding': - return this.slidingExpiration(); + return new SlidingExpiration(this.timeout); default: throw new Error(`@cache Expiration type is not supported: ${this.expiration}.`); } } - private absoluteExpirtation(): AbsoluteExpiration { - return new AbsoluteExpiration(this.timeout); - } - - private slidingExpiration(): SlidingExpiration { - return new SlidingExpiration(this.timeout); - } - } diff --git a/lib/cache/storages/factory.ts b/lib/cache/storages/factory.ts index e5c8bf7..b51a0c4 100644 --- a/lib/cache/storages/factory.ts +++ b/lib/cache/storages/factory.ts @@ -12,15 +12,11 @@ export class StorageFactory implements Factory { public create(): Storage { switch (this.storage) { case 'memory': - return this.memoryStorage(); + return new MemoryStorage(this.limit); default: throw new Error(`@cache Storage type is not supported: ${this.storage}.`); } } - private memoryStorage(): MemoryStorage { - return new MemoryStorage(this.limit); - } - } diff --git a/test/cache/cacheProvider/ClassCacheProvider.spec.ts b/test/cache/cacheProvider/ClassCacheProvider.spec.ts new file mode 100644 index 0000000..1976ff5 --- /dev/null +++ b/test/cache/cacheProvider/ClassCacheProvider.spec.ts @@ -0,0 +1,46 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { ClassCacheProvider } from '../../../lib/cache/cacheProvider/ClassCacheProvider'; +import { CacheFactory } from '../../../lib/cache/caches/factory'; + +describe('@cache ClassCacheProvider', () => { + + let cacheFactoryStub: sinon.SinonStubbedInstance; + let service: ClassCacheProvider; + + beforeEach(() => { + cacheFactoryStub = sinon.createStubInstance(CacheFactory); + + service = new ClassCacheProvider(cacheFactoryStub as any); + }); + + describe('constructor', () => { + + it('should create', () => expect(service).to.be.instanceOf(ClassCacheProvider)); + + it('should init cache property to null', () => expect(service['cache']).to.be.null); + + }); + + describe('get', () => { + + it('should call CacheFactory.create to create an instance at first call', () => { + const cacheInstance = {} as any; + cacheFactoryStub.create.returns(cacheInstance); + + expect(service.get()).to.be.equals(cacheInstance); + expect(cacheFactoryStub.create.calledOnce).to.be.true; + expect(service['cache']).to.be.equals(cacheInstance); + }); + + it('should return existent instance of cache if is not first call', () => { + const response = service['cache'] = {} as any; + + expect(cacheFactoryStub.create.called).to.be.false; + expect(service.get()).to.be.equals(response); + }); + + }); + +}); diff --git a/test/cache/cacheProvider/InstanceCacheProvider.spec.ts b/test/cache/cacheProvider/InstanceCacheProvider.spec.ts new file mode 100644 index 0000000..f415441 --- /dev/null +++ b/test/cache/cacheProvider/InstanceCacheProvider.spec.ts @@ -0,0 +1,51 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { InstanceCacheProvider } from '../../../lib/cache/cacheProvider/InstanceCacheProvider'; +import { CacheFactory } from '../../../lib/cache/caches/factory'; + +describe('@cache InstanceCacheProvider', () => { + + let cacheFactoryStub: sinon.SinonStubbedInstance; + let service: InstanceCacheProvider; + + beforeEach(() => { + cacheFactoryStub = sinon.createStubInstance(CacheFactory); + + service = new InstanceCacheProvider(cacheFactoryStub as any); + }); + + describe('constructor', () => { + + it('should create', () => expect(service).to.be.instanceOf(InstanceCacheProvider)); + + it('should init instancesCaches property', () => { + expect(service['instanceCaches']).to.be.instanceOf(WeakMap); + }); + + }); + + describe('get', () => { + + it('should create new cache instance if was called first time with this instance', () => { + const result = {} as any; + const instance = {} as any; + cacheFactoryStub.create.returns(result); + + expect(service.get(instance)).to.be.equals(result); + expect(service['instanceCaches'].get(instance)).to.be.equals(result); + expect(cacheFactoryStub.create.calledOnce).to.be.true; + }); + + it('should return already created cache for current isntance', () => { + const result = {} as any; + const instance = {} as any; + service['instanceCaches'].set(instance, result); + + expect(service.get(instance)).to.be.equals(result); + expect(cacheFactoryStub.create.called).to.be.false; + }); + + }); + +}); diff --git a/test/cache/cacheProvider/factory.spec.ts b/test/cache/cacheProvider/factory.spec.ts new file mode 100644 index 0000000..28887f9 --- /dev/null +++ b/test/cache/cacheProvider/factory.spec.ts @@ -0,0 +1,40 @@ +import { expect } from 'chai'; + +import { ClassCacheProvider } from '../../../lib/cache/cacheProvider/ClassCacheProvider'; +import { CacheProviderFactory } from '../../../lib/cache/cacheProvider/factory'; +import { InstanceCacheProvider } from '../../../lib/cache/cacheProvider/InstanceCacheProvider'; + +describe('@cache CacheProviderFactory', () => { + + describe('constructor', () => { + + it('should create', () => { + expect(new CacheProviderFactory('class', undefined)).to.be.instanceOf(CacheProviderFactory); + }); + + }); + + describe('create', () => { + + it('should create instanceof ClassCacheProvider if scope is "class"', () => { + const instance = new CacheProviderFactory('class', undefined); + expect(instance.create()).to.be.instanceOf(ClassCacheProvider); + }); + + it('should create instanceof InstanceCacheProvider if scope is "instance"', () => { + const instance = new CacheProviderFactory('instance', undefined); + expect(instance.create()).to.be.instanceOf(InstanceCacheProvider); + }); + + it('should throw error if scope options is not a valid one', () => { + const scope = '123' as any; + const message = `@cache invalid scope option: ${scope}.`; + + const instance = new CacheProviderFactory(scope, undefined); + + expect(() => instance.create()).to.throw(message); + }); + + }); + +}); diff --git a/test/cache/caches/Cache.spec.ts b/test/cache/caches/Cache.spec.ts new file mode 100644 index 0000000..e1cb28b --- /dev/null +++ b/test/cache/caches/Cache.spec.ts @@ -0,0 +1,148 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { Cache } from '../../../lib/cache/caches/Cache'; +import { AbsoluteExpiration } from '../../../lib/cache/expirations/AbsoluteExpiration'; +import { Expiration } from '../../../lib/cache/expirations/Expiration'; +import { MemoryStorage } from '../../../lib/cache/storages/MemoryStorage'; +import { Storage } from '../../../lib/cache/storages/Storage'; +import { HashService } from '../../../lib/utils/hash'; + +describe('@cache Cache', () => { + + let hashStub: sinon.SinonStubbedInstance; + let storageStub: sinon.SinonStubbedInstance; + let expirationStub: sinon.SinonStubbedInstance; + let service: Cache; + + beforeEach(() => { + hashStub = sinon.createStubInstance(HashService); + storageStub = sinon.createStubInstance(MemoryStorage); + expirationStub = sinon.createStubInstance(AbsoluteExpiration); + + service = new Cache(storageStub, expirationStub, hashStub); + }); + + describe('constructor', () => { + + it('should create', () => expect(service).to.be.instanceOf(Cache)); + + }); + + describe('set', () => { + + it('should call hash.calculate to obtain arguments hash', async () => { + await service.set(['key'], 'value'); + + expect(hashStub.calculate.calledOnce).to.be.true; + }); + + it('should call storage.set to store given hashed key and data', async () => { + const key = 'key'; + hashStub.calculate.returns(key); + + await service.set(['key'], 'value'); + + expect(storageStub.set.calledOnce).to.be.true; + expect(storageStub.set.calledWithExactly(key, 'value')).to.be.true; + }); + + it('should call expiration.add to make cache expirable', async () => { + const key = 'key'; + hashStub.calculate.returns(key); + + await service.set(['key'], 'value'); + + expect(expirationStub.add.calledOnce).to.be.true; + expect(expirationStub.add.calledWith(key, sinon.match.func)).to.be.true; + }); + + describe('function passed to expiration', () => { + + it('should call storage.delete', async () => { + const key = 'key'; + hashStub.calculate.returns(key); + + await service.set(['key'], 'value'); + + const callback = expirationStub.add.firstCall.args[1]; + + await callback(key); + + expect(storageStub.delete.calledOnce).to.be.true; + expect(storageStub.delete.calledWith(key)).to.be.true; + }); + + }); + + }); + + describe('has', () => { + + it('should call hash.calculate to obtain arguments hash', async () => { + await service.has(['key']); + + expect(hashStub.calculate.calledOnce).to.be.true; + }); + + it('should call storage has to check if key exists', async () => { + const key = 'key'; + hashStub.calculate.returns(key); + + await service.has(['key']); + + expect(storageStub.has.calledOnce).to.be.true; + expect(storageStub.has.calledWith(key)).to.be.true; + }); + + }); + + describe('get', () => { + + it('should call hash.calculate to obtain arguments hash', async () => { + await service.get(['key']); + + expect(hashStub.calculate.calledOnce).to.be.true; + }); + + it('should call expiration.add to update cache expiration', async () => { + const key = 'key'; + hashStub.calculate.returns(key); + + await service.get(['key']); + + expect(expirationStub.add.calledOnce).to.be.true; + expect(expirationStub.add.calledWith(key, sinon.match.func)).to.be.true; + }); + + it('should call storage.get to obtain cached value', async () => { + const key = 'key'; + hashStub.calculate.returns(key); + + await service.get(['key']); + + expect(storageStub.get.calledOnce).to.be.true; + expect(storageStub.get.calledWith(key)).to.be.true; + }); + + describe('function passed to expiration', () => { + + it('should call storage.delete', async () => { + const key = 'key'; + hashStub.calculate.returns(key); + + await service.get(['key']); + + const callback = expirationStub.add.firstCall.args[1]; + + await callback(key); + + expect(storageStub.delete.calledOnce).to.be.true; + expect(storageStub.delete.calledWith(key)).to.be.true; + }); + + }); + + }); + +}); diff --git a/test/cache/caches/factory.spec.ts b/test/cache/caches/factory.spec.ts new file mode 100644 index 0000000..034f75f --- /dev/null +++ b/test/cache/caches/factory.spec.ts @@ -0,0 +1,48 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { Cache } from '../../../lib/cache/caches/Cache'; +import { CacheFactory } from '../../../lib/cache/caches/factory'; +import { ExpirationFactory } from '../../../lib/cache/expirations/factory'; +import { StorageFactory } from '../../../lib/cache/storages/factory'; + +describe('@cache CacheFactory', () => { + + let expirationFactoryStub: sinon.SinonStubbedInstance; + let storageFactoryStub: sinon.SinonStubbedInstance; + let service: CacheFactory; + + beforeEach(() => { + expirationFactoryStub = sinon.createStubInstance(ExpirationFactory); + storageFactoryStub = sinon.createStubInstance(StorageFactory); + + service = new CacheFactory(undefined, expirationFactoryStub as any, storageFactoryStub as any); + }); + + describe('constructor', () => { + + it('should create', () => expect(service).to.be.instanceOf(CacheFactory)); + + }); + + describe('create', () => { + + it('should call expirationFactory.create to obtain expiration', () => { + service.create(); + + expect(expirationFactoryStub.create.calledOnce).to.be.true; + }); + + it('should call storageFactory.create to obtain storage', () => { + service.create(); + + expect(storageFactoryStub.create.calledOnce).to.be.true; + }); + + it('should return instance of Cahce', () => { + expect(service.create()).to.be.instanceOf(Cache); + }); + + }); + +}); diff --git a/test/cache/expirations/AbsoluteExpiration.spec.ts b/test/cache/expirations/AbsoluteExpiration.spec.ts new file mode 100644 index 0000000..8204577 --- /dev/null +++ b/test/cache/expirations/AbsoluteExpiration.spec.ts @@ -0,0 +1,93 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { AbsoluteExpiration } from '../../../lib/cache/expirations/AbsoluteExpiration'; +import { delay } from '../../utils'; + +describe('@cache AbsoluteExpiration', () => { + + const timeout = 10; + let service: AbsoluteExpiration; + + beforeEach(() => service = new AbsoluteExpiration(timeout)); + + describe('constructor', () => { + + it('should create', () => expect(service).to.be.instanceOf(AbsoluteExpiration)); + + it('should init expirations', () => { + expect(service['expirations']).to.be.instanceOf(Set); + }); + + }); + + describe('add', () => { + + describe('new key', () => { + + it('should add key to expirations', () => { + const size = service['expirations'].size; + + service.add('key', () => { }); + + expect(service['expirations'].size).to.be.equals(size + 1); + expect(service['expirations'].has('key')).to.be.true; + }); + + it('should remove key from expirations after timeout ms', async () => { + const size = service['expirations'].size; + + service.add('key', () => { }); + + await delay(timeout); + + expect(service['expirations'].size).to.be.equals(size); + expect(service['expirations'].has('key')).to.be.false; + }); + + it('should call clearCallback after timeout ms', async () => { + const spy = sinon.spy(); + + service.add('key', spy); + + await delay(timeout); + + expect(spy.calledOnce).to.be.true; + }); + + }); + + describe('existing key', () => { + + it('should not update expiration', async () => { + const spy = sinon.spy(); + service['expirations'].add('key'); + + service.add('key', spy); + + await delay(timeout); + + expect(spy.called).to.be.false; + }); + + it('should call initial callback', async () => { + const firstSpy = sinon.spy(); + const secondSpy = sinon.spy(); + + service.add('key', firstSpy); + + await delay(timeout / 2); + + service.add('key', secondSpy); + + await delay(timeout / 2); + + expect(firstSpy.calledOnce).to.be.true; + expect(secondSpy.called).to.be.false; + }); + + }); + + }); + +}); diff --git a/test/cache/expirations/SlidingExpiration.spec.ts b/test/cache/expirations/SlidingExpiration.spec.ts new file mode 100644 index 0000000..395ce25 --- /dev/null +++ b/test/cache/expirations/SlidingExpiration.spec.ts @@ -0,0 +1,102 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { SlidingExpiration } from '../../../lib/cache/expirations/SlidingExpiration'; +import { delay } from '../../utils'; + +describe('@cache SlidingExpiration', () => { + + const timeout = 10; + let service: SlidingExpiration; + + beforeEach(() => service = new SlidingExpiration(timeout)); + + describe('constructor', () => { + + it('should create', () => expect(service).to.be.instanceOf(SlidingExpiration)); + + it('should init expirations storage', () => { + expect(service['expirations']).to.be.instanceOf(Map); + }); + + }); + + describe('add', () => { + + describe('new key', () => { + + it('should add key in expirations', () => { + const size = service['expirations'].size; + + service.add('key', () => { }); + + expect(service['expirations'].size).to.be.equals(size + 1); + expect(service['expirations'].has('key')).to.be.true; + }); + + it('should remove key from expirations after timeout ms', async () => { + const size = service['expirations'].size; + + service.add('key', () => { }); + await delay(timeout); + + expect(service['expirations'].size).to.be.equals(size); + expect(service['expirations'].has('key')).to.be.false; + }); + + it('should call clearCallback after timeout ms', async () => { + const spy = sinon.spy(); + + service.add('key', spy); + await delay(timeout); + + expect(spy.calledOnce).to.be.true; + }); + + }); + + describe('existing key', () => { + + beforeEach(() => service['expirations'].set('key', setTimeout(() => { }, timeout) as any)); + + it('should update timer for key in expirations', () => { + const timer = service['expirations'].get('key'); + + service.add('key', () => { }); + + expect(service['expirations'].get('key')).not.to.be.equals(timer); + }); + + it('should remove existing expiration', async () => { + const expirationSpy = sinon.spy(); + service['expirations'].set('key', setTimeout(() => expirationSpy(), timeout) as any); + + service.add('key', () => { }); + + await delay(timeout); + + expect(expirationSpy.called).to.be.false; + }); + + it('should clear key in expirations after timeout ms', async () => { + service.add('key', () => { }); + + await delay(timeout); + + expect(service['expirations'].get('key')).to.be.undefined; + }); + + it('should call clearCallback after timeout ms', async () => { + const spy = sinon.spy(); + + service.add('key', spy); + await delay(timeout); + + expect(spy.calledOnce).to.be.true; + }); + + }); + + }); + +}); diff --git a/test/cache/expirations/factory.spec.ts b/test/cache/expirations/factory.spec.ts new file mode 100644 index 0000000..f34cb1e --- /dev/null +++ b/test/cache/expirations/factory.spec.ts @@ -0,0 +1,36 @@ +import { expect } from 'chai'; + +import { AbsoluteExpiration } from '../../../lib/cache/expirations/AbsoluteExpiration'; +import { ExpirationFactory } from '../../../lib/cache/expirations/factory'; +import { SlidingExpiration } from '../../../lib/cache/expirations/SlidingExpiration'; + +describe('@cache ExpirationFactory', () => { + + describe('constructor', () => { + + it('should create', () => { + expect(new ExpirationFactory(3, 'absolute')).to.be.instanceOf(ExpirationFactory); + }); + + }); + + describe('create', () => { + + it('should create instance of AbsoluteExpiration if expiration is "absolute"', () => { + expect(new ExpirationFactory(3, 'absolute').create()).to.be.instanceOf(AbsoluteExpiration); + }); + + it('should create instance of SlidingExpiration if expiration is "sliding"', () => { + expect(new ExpirationFactory(3, 'sliding').create()).to.be.instanceOf(SlidingExpiration); + }); + + it('should throw error if expiration parameter is not valid', () => { + const expiration = '123' as any; + const message = `@cache Expiration type is not supported: ${expiration}.`; + + expect(() => new ExpirationFactory(3, expiration).create()).to.throw(message); + }); + + }); + +}); diff --git a/test/cache/storages/MemoryStorage.spec.ts b/test/cache/storages/MemoryStorage.spec.ts new file mode 100644 index 0000000..1a1ab37 --- /dev/null +++ b/test/cache/storages/MemoryStorage.spec.ts @@ -0,0 +1,83 @@ +import { expect } from 'chai'; + +import { MemoryStorage } from '../../../lib/cache/storages/MemoryStorage'; + +describe('@cache MemoryStorage', () => { + + let service: MemoryStorage; + + beforeEach(() => service = new MemoryStorage()); + + describe('constructor', () => { + + it('should create', () => expect(service).to.be.instanceOf(MemoryStorage)); + + it('should init storage', () => expect(service['storage']).to.be.instanceOf(Map)); + + }); + + describe('set', () => { + + it('should remove oldest key if limit is reached', () => { + const service = new MemoryStorage(2); + service['storage'].set('a', 1).set('b', 2); + + service.set('c', 3); + + expect(service['storage'].size).to.be.equals(2); + expect(Array.from(service['storage'].keys())).to.be.deep.equals(['b', 'c']); + }); + + it('should set key and value to storage', () => { + const key = 'key'; + const value = 42; + service.set(key, value); + + expect(service['storage'].get(key)).to.be.equals(value); + }); + + it('should return self instance', async () => { + expect(await service.set('key', 'value')).to.be.equals(service); + }); + + }); + + describe('get', () => { + + it('should return value from storage', async () => { + service['storage'].set('key', 'value'); + expect(await service.get('key')).to.be.equals(service['storage'].get('key')); + }); + + }); + + describe('has', () => { + + it('should return true if key is in storage', async () => { + service['storage'].set('key', 'value'); + expect(await service.has('key')).to.be.true; + }); + + it('should retunr false if key is not in storage', async () => { + expect(await service.has('key')).to.be.false; + }); + + }); + + describe('delete', () => { + + it('should delete key from storage', async () => { + service['storage'].set('key', '123'); + + await service.delete('key'); + + expect(service['storage'].has('key')).to.be.false; + }); + + it('should return self instance', async () => { + expect(await service.delete('key')).to.be.equals(service); + }); + + }); + +}); diff --git a/test/cache/storages/factory.spec.ts b/test/cache/storages/factory.spec.ts new file mode 100644 index 0000000..cfa44fa --- /dev/null +++ b/test/cache/storages/factory.spec.ts @@ -0,0 +1,30 @@ +import { expect } from 'chai'; + +import { StorageFactory } from '../../../lib/cache/storages/factory'; +import { MemoryStorage } from '../../../lib/cache/storages/MemoryStorage'; + +describe('@cache StorageFactory', () => { + + describe('constructor', () => { + + it('should create', () => { + expect(new StorageFactory(3, 'memory')).to.be.instanceOf(StorageFactory); + }); + + }); + + describe('create', () => { + + it('should return instance of StorageFactory if storage is "memory"', () => { + expect(new StorageFactory(3, 'memory').create()).to.be.instanceOf(MemoryStorage); + }); + + it('should throw error if storate is not an valid storage', () => { + const storage = 'memory3'; + const message = `@cache Storage type is not supported: ${storage}.`; + expect(() => new StorageFactory(3, 'memory3' as any).create()).to.throw(message); + }); + + }); + +}); diff --git a/test/indext.ts b/test/indext.ts index 6dfdc7f..08b3dac 100644 --- a/test/indext.ts +++ b/test/indext.ts @@ -1,4 +1,8 @@ import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; +import * as sinonChai from 'sinon-chai'; +import * as chaiSpies from 'chai-spies'; chai.use(chaiAsPromised); +chai.use(sinonChai); +chai.use(chaiSpies);