Test coverage for CacheManager and CacheClient
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user