Test coverage for CacheManager and CacheClient

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-05-02 16:30:06 +02:00
parent 6e0be00e1d
commit c7fa03cd76
2 changed files with 295 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConcreteCacheClient } from './CacheClient';
import cacheManager from 'cache-manager';
describe('CacheClient', () => {
const pluginId = 'test';
let client: cacheManager.Cache;
const b64 = (k: string) => Buffer.from(k).toString('base64');
beforeEach(() => {
client = cacheManager.caching({ store: 'none', ttl: 0 });
client.get = jest.fn();
client.set = jest.fn();
client.del = jest.fn();
});
afterEach(() => jest.resetAllMocks());
describe('CacheClient.get', () => {
it('calls client with normalized key', async () => {
const sut = new ConcreteCacheClient({ client, pluginId });
const keyPartial = 'somekey';
await sut.get(keyPartial);
expect(client.get).toHaveBeenCalledWith(b64(`${pluginId}:${keyPartial}`));
});
it('calls client with normalized key (very long key)', async () => {
const sut = new ConcreteCacheClient({ client, pluginId });
const keyPartial = 'x'.repeat(251);
await sut.get(keyPartial);
const spy = client.get as jest.Mock;
const actualKey = spy.mock.calls[0][0];
expect(actualKey).not.toEqual(b64(`${pluginId}:${keyPartial}`));
expect(actualKey.length).toBeLessThan(250);
});
it('performs deserialization on returned data', async () => {
const expectedValue = { some: 'value' };
const sut = new ConcreteCacheClient({ client, pluginId });
client.get = jest.fn().mockResolvedValue(JSON.stringify(expectedValue));
const actualValue = await sut.get('someKey');
expect(actualValue).toMatchObject(expectedValue);
});
it('returns null on any underlying error', async () => {
const sut = new ConcreteCacheClient({ client, pluginId });
client.get = jest.fn().mockRejectedValue(undefined);
const actualValue = await sut.get('someKey');
expect(actualValue).toStrictEqual(null);
});
});
describe('CacheClient.set', () => {
it('calls client with normalized key', async () => {
const sut = new ConcreteCacheClient({ client, pluginId });
const keyPartial = 'somekey';
await sut.set(keyPartial, {});
const spy = client.set as jest.Mock;
const actualKey = spy.mock.calls[0][0];
expect(actualKey).toEqual(b64(`${pluginId}:${keyPartial}`));
});
it('performs serialization on given data', async () => {
const sut = new ConcreteCacheClient({ client, pluginId });
const expectedData = { some: 'value' };
await sut.set('someKey', expectedData);
const spy = client.set as jest.Mock;
const actualData = spy.mock.calls[0][1];
expect(actualData).toEqual(JSON.stringify(expectedData));
});
it('passes ttl to client when given', async () => {
const sut = new ConcreteCacheClient({ client, pluginId });
const expectedTtl = 3600;
await sut.set('someKey', {}, expectedTtl);
const spy = client.set as jest.Mock;
const actualOptions = spy.mock.calls[0][2];
expect(actualOptions.ttl).toEqual(expectedTtl);
});
it('does not throw errors on any client error', async () => {
const sut = new ConcreteCacheClient({ client, pluginId });
client.set = jest.fn().mockRejectedValue(undefined);
expect(async () => {
await sut.set('someKey', {});
}).not.toThrow();
});
});
describe('CacheClient.delete', () => {
it('calls client with normalized key', async () => {
const sut = new ConcreteCacheClient({ client, pluginId });
const keyPartial = 'somekey';
await sut.delete(keyPartial);
const spy = client.del as jest.Mock;
const actualKey = spy.mock.calls[0][0];
expect(actualKey).toEqual(b64(`${pluginId}:${keyPartial}`));
});
it('does not throw errors on any client error', async () => {
const sut = new ConcreteCacheClient({ client, pluginId });
client.del = jest.fn().mockRejectedValue(undefined);
expect(async () => {
await sut.delete('someKey');
}).not.toThrow();
});
});
});
+154
View File
@@ -0,0 +1,154 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import cacheManager from 'cache-manager';
import { ConcreteCacheClient } from './CacheClient';
import { CacheManager } from './CacheManager';
cacheManager.caching = jest.fn() as jest.Mock;
jest.mock('./CacheClient', () => {
return {
ConcreteCacheClient: jest.fn(),
};
});
describe('CacheManager', () => {
const defaultConfigOptions = {
backend: {
cache: {
store: 'memory',
},
},
};
const defaultConfig = () => new ConfigReader(defaultConfigOptions);
afterEach(() => jest.resetAllMocks());
describe('CacheManager.fromConfig', () => {
it('accesses the backend.cache key', () => {
const getOptionalConfig = jest.fn();
const config = defaultConfig();
config.getOptionalConfig = getOptionalConfig;
CacheManager.fromConfig(config);
expect(getOptionalConfig.mock.calls[0][0]).toEqual('backend.cache');
});
it('does not require the backend.cache key', () => {
const config = new ConfigReader({ backend: {} });
expect(() => {
CacheManager.fromConfig(config);
}).not.toThrowError();
});
});
describe('CacheManager.forPlugin', () => {
const manager = CacheManager.fromConfig(defaultConfig());
it('connects to a cache store scoped to the plugin', async () => {
const pluginId = 'test1';
const expectedTtl = 3600;
manager.forPlugin(pluginId).getClient(expectedTtl);
const client = ConcreteCacheClient as jest.Mock;
expect(client).toHaveBeenCalledTimes(1);
});
it('provides different plugins different cache clients', async () => {
const plugin1Id = 'test1';
const plugin2Id = 'test2';
const expectedTtl = 3600;
manager.forPlugin(plugin1Id).getClient(expectedTtl);
manager.forPlugin(plugin2Id).getClient(expectedTtl);
const cache = cacheManager.caching as jest.Mock;
const client = ConcreteCacheClient as jest.Mock;
expect(cache).toHaveBeenCalledTimes(2);
expect(client).toHaveBeenCalledTimes(2);
const plugin1CallArgs = client.mock.calls[0];
const plugin2CallArgs = client.mock.calls[1];
expect(plugin1CallArgs[0].pluginId).not.toEqual(
plugin2CallArgs[0].pluginId,
);
});
});
describe('CacheManager.forPlugin stores', () => {
it('returns none client when no cache is configured', () => {
const manager = CacheManager.fromConfig(
new ConfigReader({ backend: {} }),
);
const expectedTtl = 3600;
manager.forPlugin('test').getClient(expectedTtl);
const cache = cacheManager.caching as jest.Mock;
const mockCalls = cache.mock.calls.splice(-1);
const callArgs = mockCalls[0];
expect(callArgs[0]).toMatchObject({
store: 'none',
ttl: expectedTtl,
});
});
it('returns memory client when configured', () => {
const manager = CacheManager.fromConfig(defaultConfig());
const expectedTtl = 3600;
manager.forPlugin('test').getClient(expectedTtl);
const cache = cacheManager.caching as jest.Mock;
const mockCalls = cache.mock.calls.splice(-1);
const callArgs = mockCalls[0];
expect(callArgs[0]).toMatchObject({
store: 'memory',
ttl: expectedTtl,
});
});
it('returns a memcache client when configured', () => {
const expectedHost = '127.0.0.1:11211';
const expectedTimeout = 1000;
const manager = CacheManager.fromConfig(
new ConfigReader({
backend: {
cache: {
store: 'memcache',
connection: {
hosts: [expectedHost],
netTimeout: expectedTimeout,
},
},
},
}),
);
const expectedTtl = 3600;
manager.forPlugin('test').getClient(expectedTtl);
const cache = cacheManager.caching as jest.Mock;
const mockCalls = cache.mock.calls.splice(-1);
const callArgs = mockCalls[0];
expect(callArgs[0]).toMatchObject({
options: {
hosts: [expectedHost],
netTimeout: expectedTimeout,
},
ttl: expectedTtl,
});
});
});
});