From 8d582336e2625c9d6213be49d68fb69f1edf6303 Mon Sep 17 00:00:00 2001 From: Aleksey Kvak Date: Tue, 28 Jan 2020 15:39:53 +0300 Subject: [PATCH] fix: mset does not add cache key prefix. Integration tests with real Redis instance was added. --- .travis.yml | 4 + integration/jest.config.js | 13 ++ integration/tests/redis.spec.ts | 213 +++++++++++++++++++++++++++++++ package-lock.json | 12 ++ package.json | 3 + src/adapters/redis/index.ts | 6 +- src/adapters/redis/redis.spec.ts | 2 +- 7 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 integration/jest.config.js create mode 100644 integration/tests/redis.spec.ts diff --git a/.travis.yml b/.travis.yml index 3414f75..0b73895 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,12 @@ node_js: - '10' - '8' +services: + - redis-server + script: - npm run test:ci + - npm run test:integration - npm run build - npm run check diff --git a/integration/jest.config.js b/integration/jest.config.js new file mode 100644 index 0000000..8665e8d --- /dev/null +++ b/integration/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + globals: { + 'ts-jest': { + tsConfig: '/../tsconfig.json', + diagnostics: true, + }, + }, + testMatch: ['/tests/**/*.spec.ts'], + testEnvironment: 'jsdom', + transform: { + '^.+\\.ts$': 'ts-jest', + }, +}; diff --git a/integration/tests/redis.spec.ts b/integration/tests/redis.spec.ts new file mode 100644 index 0000000..101e5fe --- /dev/null +++ b/integration/tests/redis.spec.ts @@ -0,0 +1,213 @@ +import faker from 'faker'; +import Redis, { Redis as RedisType } from 'ioredis'; +import RedisStorageAdapter, { CACHE_PREFIX } from '../../src/adapters/redis'; +import { ConnectionStatus } from '../../src/connection-status'; + +let redis: RedisType; +let adapter: RedisStorageAdapter; + +function delay(duration: number): Promise { + return new Promise((resolve) => setTimeout(resolve, duration)); +} + +const expireTimeout = 5; + +describe('Redis adapter', () => { + beforeAll(() => { + redis = new Redis(); + adapter = new RedisStorageAdapter(redis, { lockExpireTimeout: expireTimeout }); + }); + + afterAll(() => { + redis.disconnect(); + }); + + it('Sets connection status to "connected" if redis executes some command', async () => { + await redis.get(''); + expect(adapter.getConnectionStatus()).toEqual(ConnectionStatus.CONNECTED); + }); + + describe('set', () => { + it('set returns true if operation is successful', async () => { + const key = faker.random.uuid(); + const value = faker.random.uuid(); + + await expect(adapter.set(key, value)).resolves.toEqual(true); + await expect(adapter.get(key)).resolves.toEqual(value); + }); + + it('set adds cache prefix', async () => { + const key = faker.random.uuid(); + const value = faker.random.uuid(); + + await adapter.set(key, value); + + await expect(redis.get(`${CACHE_PREFIX}:${key}`)).resolves.toEqual(value); + }); + + it('set calls set with cache prefix and PX mode when expires set', async () => { + const key = faker.random.uuid(); + const value = faker.random.uuid(); + + await adapter.set(key, value, expireTimeout); + await delay(expireTimeout); + await expect(redis.get(`${CACHE_PREFIX}:${key}`)).resolves.toBeNull(); + }); + }); + + describe('get', () => { + it('get returns value', async () => { + const key = faker.random.uuid(); + const value = faker.random.uuid(); + + await expect(adapter.set(key, value)); + await expect(adapter.get(key)).resolves.toEqual(value); + }); + + it('get returns null if key does not set', async () => { + const key = faker.random.uuid(); + await expect(adapter.get(key)).resolves.toBeNull(); + }); + + it('get adds cache prefix', async () => { + const key = faker.random.uuid(); + const value = faker.random.uuid(); + await redis.set(`${CACHE_PREFIX}:${key}`, value); + + await expect(adapter.get(key)).resolves.toEqual(value); + }); + }); + + describe('del', () => { + it('del calls del with cache prefix', async () => { + const key = faker.random.uuid(); + const value = faker.random.uuid(); + + await redis.set(`${CACHE_PREFIX}:${key}`, value); + await adapter.del(key); + + await expect(redis.get(`${CACHE_PREFIX}:${key}`)).resolves.toBeNull(); + }); + + it('del does nothing if key does not exist', async () => { + const key = faker.random.uuid(); + const keyWithPrefix = `${CACHE_PREFIX}:${key}`; + + await expect(redis.get(keyWithPrefix)).resolves.toBeNull(); + await adapter.del(key); + await expect(redis.get(keyWithPrefix)).resolves.toBeNull(); + }); + }); + + describe('acquireLock', () => { + it('acquireLock returns true if lock is successful', async () => { + const key = faker.random.uuid(); + const lockResult = await adapter.acquireLock(key); + + expect(lockResult).toEqual(true); + }); + + it('acquireLock calls set with generated key name and in NX mode', async () => { + const key = faker.random.uuid(); + const lockResult = await adapter.acquireLock(key); + + expect(lockResult).toEqual(true); + await delay(expireTimeout); + + await (expect(redis.get(`${key}_lock`))).resolves.toBeNull(); + }); + }); + + describe('releaseLock', () => { + it('releaseLock returns false if lock does not exist', async () => { + const key = faker.random.uuid(); + const releaseLockResult = await adapter.releaseLock(key); + expect(releaseLockResult).toEqual(false); + }); + + it('releaseLock delete lock record with appropriate key, and returns true on success', async () => { + const key = faker.random.uuid(); + + await redis.set(`${key}_lock`, ''); + const releaseLockResult = await adapter.releaseLock(key); + expect(releaseLockResult).toEqual(true); + }); + + it('releaseLock delete lock record set by acquireLock', async () => { + const key = faker.random.uuid(); + + await adapter.acquireLock(key); + + await expect(adapter.releaseLock(key)).resolves.toEqual(true); + }); + }); + + describe('isLockExists', () => { + it('isLockExists returns true if lock exists', async () => { + const key = faker.random.uuid(); + + await adapter.acquireLock(key); + await expect(adapter.isLockExists(key)).resolves.toEqual(true); + }); + + it('isLockExists returns false if lock does not exist', async () => { + const key = faker.random.uuid(); + + await expect(adapter.isLockExists(key)).resolves.toEqual(false); + }); + }); + + describe('mset', () => { + it('mset sets values', async () => { + const values = new Map([ + [faker.random.word(), faker.random.uuid()], + [faker.random.word(), faker.random.uuid()] + ]); + await adapter.mset(values); + + for (const [key, value] of values.entries()) { + await expect(redis.get(`${CACHE_PREFIX}:${key}`)).resolves.toEqual(value); + } + }); + + it('mset throws error on empty values', async () => { + await expect(adapter.mset(new Map())).rejects.toThrowError('ERR wrong number of arguments for \'mset\' command'); + }); + }); + + describe('mget', () => { + it('mget gets values', async () => { + const values = new Map([ + [faker.random.word(), faker.random.uuid()], + [faker.random.word(), faker.random.uuid()] + ]); + + for (const [key, value] of values.entries()) { + await redis.set(`${CACHE_PREFIX}:${key}`, value); + } + + const result = await adapter.mget(Array.from(values.keys())); + + expect(result).toEqual(Array.from(values.values())); + }); + + it('mget returns null for non-existing keys', async () => { + const values = new Map([ + [faker.random.word(), faker.random.uuid()], + [faker.random.word(), faker.random.uuid()] + ]); + + for (const [key, value] of values.entries()) { + await redis.set(`${CACHE_PREFIX}:${key}`, value); + } + + const keys = Array.from(values.keys()); + const nonExistingKey = faker.random.word(); + keys.push(nonExistingKey); + + const result = await adapter.mget(keys); + + expect(result).toEqual([...Array.from(values.values()), null]); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 88823e4..c296477 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1007,6 +1007,12 @@ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", "dev": true }, + "@types/faker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/faker/-/faker-4.1.9.tgz", + "integrity": "sha512-4ZFqA3CEXB6MgT8sDV8E5LhW+O9ndONsHeQXMbEwfOsjoQ4UXKqTJKru+BjDBUfobYEpQz1WYF9/uzQsvbY2wA==", + "dev": true + }, "@types/glob": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", @@ -2799,6 +2805,12 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "dev": true }, + "faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=", + "dev": true + }, "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", diff --git a/package.json b/package.json index 3b730bb..9b50ff6 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "test": "npm run test:unit", "test:unit": "jest --coverage --verbose --passWithNoTests", "test:ci": "npm run test:unit -- --coverageReporters=text-lcov | coveralls", + "test:integration": "jest --config ./integration/jest.config.js --forceExit --detectOpenHandles", "test:unit:watch": "jest --watch", "prepublishOnly": "npm run check && npm run build", "semantic-release": "semantic-release" @@ -43,12 +44,14 @@ "devDependencies": { "@semantic-release/changelog": "^3.0.4", "@semantic-release/git": "^7.0.16", + "@types/faker": "^4.1.9", "@types/ioredis": "^4.14.4", "@types/jest": "^24.0.15", "@types/lodash": "^4.14.149", "@types/node": "^8", "coveralls": "^3.0.4", "cz-conventional-changelog": "^3.0.2", + "faker": "^4.1.0", "ioredis": "^4.14.1", "jest": "^24.8.0", "semantic-release": "^15.13.24", diff --git a/src/adapters/redis/index.ts b/src/adapters/redis/index.ts index 8fb6a7b..4cf4b33 100644 --- a/src/adapters/redis/index.ts +++ b/src/adapters/redis/index.ts @@ -83,7 +83,11 @@ export class RedisStorageAdapter implements StorageAdapter { * Set multiple values to redis storage */ public async mset(values: Map): Promise { - await withTimeout(this.redisInstance.mset(values), this.options.operationTimeout); + const data = new Map(); + for (const [key, value] of values.entries()) { + data.set(`${CACHE_PREFIX}:${key}`, value); + } + await withTimeout(this.redisInstance.mset(data), this.options.operationTimeout); } /** diff --git a/src/adapters/redis/redis.spec.ts b/src/adapters/redis/redis.spec.ts index 6ad9512..e04b69d 100644 --- a/src/adapters/redis/redis.spec.ts +++ b/src/adapters/redis/redis.spec.ts @@ -166,7 +166,7 @@ describe('Redis adapter', () => { await adapter.mset(values); expect((mock as any).mset).toHaveBeenCalledTimes(1); - expect((mock as any).mset).toHaveBeenCalledWith(values); + expect((mock as any).mset).toHaveBeenCalledWith(new Map([['cache:some1', 'value1'], ['cache:some2', 'value2']])); }); it('mget gets values', async () => {