├── .github
└── workflows
│ └── ci-plugin.yml
├── .gitignore
├── API.md
├── LICENSE.md
├── README.md
├── docker-compose.yml
├── lib
├── index.d.ts
└── index.js
├── package.json
└── test
├── cluster.js
├── esm.js
├── index.js
├── mock.js
└── types.ts
/.github/workflows/ci-plugin.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | jobs:
11 | test:
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | os: [ubuntu, macos]
16 | node: ["*", "16", "14"]
17 | runs-on: ${{ matrix.os }}-latest
18 | name: ${{ matrix.os }} node@${{ matrix.node }}
19 | steps:
20 | - uses: actions/checkout@v2
21 | - uses: actions/setup-node@v2
22 | with:
23 | node-version: ${{ matrix.node }}
24 | check-latest: ${{ matrix.node == '*' }}
25 | - name: Install
26 | run: npm install
27 | - name: Start docker
28 | if: matrix.os == 'macos'
29 | run: brew install docker docker-compose && colima start
30 | - name: Start containers
31 | run: docker-compose up -d
32 | - name: Sleep for 5s
33 | if: matrix.os != 'macos'
34 | run: sleep 5s;
35 | - name: Sleep for 5s
36 | if: matrix.os == 'macos'
37 | run: sleep 5;
38 | - name: test
39 | run: npm test
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/package-lock.json
3 |
4 | coverage.*
5 |
6 | **/.DS_Store
7 | **/._*
8 |
9 | **/*.pem
10 |
11 | **/.vs
12 | **/.vscode
13 | **/.idea
14 |
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 |
2 | ### `new CatboxRedis.Engine(options)`
3 |
4 | The connection can be specified with one (and only one) of:
5 |
6 | - `client` - a custom Redis client instance where `client` must:
7 | - be manually started and stopped,
8 | - be compatible with the **ioredis** module API, and
9 | - expose the `status` property that must be set to `'ready'` when connected.
10 |
11 | - `url` - a Redis server URL.
12 |
13 | - `socket` - a unix socket string.
14 |
15 | - `cluster` - an array of `{ host, port }` pairs.
16 |
17 | Or:
18 |
19 | - `host` - a Redis server hostname. Defaults to `'127.0.0.1'` if no other connection method specified from the above.
20 | - `port` - a Redis server port or unix domain socket path. Defaults to `6379` if no other connection method specified from the above.
21 |
22 | **catbox** options:
23 |
24 | - `partition` - a string used to prefix all item keys with. Defaults to `''`.
25 |
26 | Other supported Redis options:
27 |
28 | - `password` - the Redis authentication password when required.
29 | - `db` - a Redis database name or number.
30 | - `sentinels` - an array of `{ host, port }` sentinel address pairs.
31 | - `sentinelName` - the name of the sentinel master (when `sentinels` is specified).
32 | - `tls` - an object representing TLS config options for **ioredis**.
33 |
34 | The plugin also accepts other `redis` options not mentioned above.
35 |
36 |
37 | ### Usage
38 |
39 | Sample catbox cache initialization:
40 |
41 | ```js
42 | const Catbox = require('@hapi/catbox');
43 | const { Engine: CatboxRedis } = require('@hapi/catbox-redis');
44 |
45 |
46 | const cache = new Catbox.Client(CatboxRedis, {
47 | partition : 'my_cached_data',
48 | host: 'redis-cluster.domain.com',
49 | port: 6379,
50 | db: 0,
51 | tls: {},
52 | });
53 | ```
54 |
55 | When used in a hapi server (hapi version 18 or newer):
56 |
57 | ```js
58 | const Hapi = require('hapi')
59 | const { Engine: CatboxRedis } = require('@hapi/catbox-redis');
60 |
61 | const server = new Hapi.Server({
62 | cache : [
63 | {
64 | name: 'my_cache',
65 | provider: {
66 | constructor: CatboxRedis,
67 | options: {
68 | partition : 'my_cached_data',
69 | host: 'redis-cluster.domain.com',
70 | port: 6379,
71 | db: 0,
72 | tls: {},
73 | }
74 | }
75 | }
76 | ]
77 | });
78 | ```
79 |
80 |
81 | ### Tests
82 |
83 | The test suite expects:
84 | - a redis server to be running on port 6379
85 | - a redis server listenning to port 6378 and requiring a password: 'secret'
86 | - a redis cluster contains nodes running on ports 7000 to 7005
87 |
88 | See [docker-compose.yml](./docker-compose.yml)
89 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2022, Project contributors
2 | Copyright (c) 2012-2020, Sideway Inc
3 | Copyright (c) 2012-2014, Walmart.
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 | * The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission.
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS OFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # @hapi/catbox-redis
4 |
5 | #### Redis adapter for [catbox](https://github.com/hapijs/catbox).
6 |
7 | **catbox-redis** is part of the **hapi** ecosystem and was designed to work seamlessly with the [hapi web framework](https://hapi.dev) and its other components (but works great on its own or with other frameworks). If you are using a different web framework and find this module useful, check out [hapi](https://hapi.dev) – they work even better together.
8 |
9 | ### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support
10 |
11 | ## Useful resources
12 |
13 | - [Documentation and API](https://hapi.dev/family/catbox-redis/)
14 | - [Version status](https://hapi.dev/resources/status/#catbox-redis) (builds, dependencies, node versions, licenses, eol)
15 | - [Changelog](https://hapi.dev/family/catbox-redis/changelog/)
16 | - [Project policies](https://hapi.dev/policies/)
17 | - [Free and commercial support options](https://hapi.dev/support/)
18 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.0'
2 | services:
3 | redis_basic:
4 | container_name: redis_basic
5 | image: redis:6-alpine
6 | ports:
7 | - 6379:6379
8 |
9 | redis_with_password:
10 | container_name: redis_with_password
11 | image: redis:6-alpine
12 | command: redis-server --requirepass secret
13 | ports:
14 | - 6378:6379
15 |
16 | redis_cluster:
17 | container_name: redis_cluster
18 | image: grokzen/redis-cluster:6.2.8
19 | environment:
20 | IP: '0.0.0.0'
21 | CLUSTER_ONLY: 'true'
22 | ports:
23 | - 7000:7000
24 | - 7001:7001
25 | - 7002:7002
26 | - 7003:7003
27 | - 7004:7004
28 | - 7005:7005
29 |
--------------------------------------------------------------------------------
/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for @hapi/catbox-redis 7.0
2 | // Project: https://github.com/hapijs/catbox-redis
3 | // Definitions by: Simon Schick
4 | // Silas Rech
5 | // Danilo Alonso
6 | // TypeScript Version: 5
7 |
8 | import { EnginePrototype, ClientOptions, Client } from '@hapi/catbox';
9 | import Redis, { Cluster } from 'ioredis';
10 |
11 | export interface CatboxRedisOptions extends ClientOptions {
12 |
13 | /**
14 | * Raw client.
15 | */
16 | client?: Redis | Cluster | undefined;
17 | /**
18 | * the Redis server URL (if url is provided, host, port, and socket are ignored)
19 | */
20 | url?: string | undefined;
21 | /**
22 | * the Redis server hostname.
23 | * Defaults to '127.0.0.1'.
24 | */
25 | host?: string | undefined;
26 | /**
27 | * the Redis server port or unix domain socket path.
28 | * Defaults to 6379.
29 | */
30 | port?: number | undefined;
31 | /**
32 | * the unix socket string to connect to (if socket is provided, host and port are ignored)
33 | */
34 | socket?: string | undefined;
35 | /**
36 | * the Redis authentication password when required.
37 | */
38 | password?: string | undefined;
39 | /**
40 | * the Redis database.
41 | */
42 | database?: string | undefined;
43 | /**
44 | * an array of redis sentinel addresses to connect to.
45 | */
46 | sentinels?: Array<{
47 | host: string;
48 | }> | undefined;
49 | /**
50 | * the name of the sentinel master.
51 | * (Only needed when sentinels is specified)
52 | */
53 | sentinelName?: string | undefined;
54 | }
55 |
56 | export class Engine extends Client {
57 |
58 | constructor (opts: CatboxRedisOptions);
59 | }
60 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Bourne = require('@hapi/bourne');
4 | const Hoek = require('@hapi/hoek');
5 | const IoRedis = require('ioredis');
6 | const Joi = require('joi');
7 |
8 |
9 | const internals = {
10 | schema: {
11 | partition: Joi.string().default(''),
12 | host: Joi.object({
13 | host: Joi.string().default('127.0.0.1'),
14 | port: Joi.number().integer().positive().default(6379)
15 | })
16 | }
17 | };
18 |
19 |
20 | internals.schema.cluster = Joi.array()
21 | .items(internals.schema.host)
22 | .min(1);
23 |
24 |
25 | internals.schema.common = Joi.object({
26 |
27 | partition: internals.schema.partition,
28 |
29 | // Redis options
30 |
31 | db: [Joi.string(), Joi.number()],
32 |
33 | password: Joi.string().allow(''),
34 | tls: Joi.object(),
35 | sentinels: internals.schema.cluster,
36 | name: Joi.string()
37 | })
38 | .rename('database', 'db')
39 | .rename('sentinelName', 'name')
40 | .without('db', 'database')
41 | .with('name', 'sentinels')
42 | .unknown();
43 |
44 |
45 | internals.schema.options = Joi.alternatives([
46 | Joi.object({
47 | client: Joi.object().required(),
48 | partition: internals.schema.partition
49 | })
50 | .unknown(),
51 |
52 | internals.schema.common.keys({
53 | url: Joi.string().uri(),
54 | socket: Joi.string(),
55 | cluster: internals.schema.cluster
56 | })
57 | .xor('url', 'socket', 'cluster'),
58 |
59 | internals.schema.common.concat(internals.schema.host)
60 | ]);
61 |
62 |
63 | exports.Engine = class CatboxRedis {
64 |
65 | constructor(options = {}) {
66 |
67 | this.settings = Joi.attempt(options, internals.schema.options);
68 | }
69 |
70 | async start() {
71 |
72 | // Skip if already started
73 |
74 | if (this.client) {
75 | return;
76 | }
77 |
78 | // Externally managed clients
79 |
80 | if (this.settings.client) {
81 | this.client = this.settings.client;
82 | return;
83 | }
84 |
85 | // Normalize Redis options
86 |
87 | const redisOptions = Hoek.clone(this.settings);
88 | redisOptions.lazyConnect = !this.settings.cluster;
89 |
90 | for (const key of ['client', 'cluster', 'partition', 'socket', 'url']) {
91 | delete redisOptions[key];
92 | }
93 |
94 | // Cluster
95 |
96 | if (this.settings.cluster) {
97 | return new Promise((resolve, reject) => {
98 |
99 | this.client = new IoRedis.Cluster(this.settings.cluster, redisOptions);
100 | this.client.once('ready', resolve);
101 | this.client.on('error', reject);
102 | });
103 | }
104 |
105 | // Single connection
106 |
107 | const client = this._connection(redisOptions);
108 |
109 | client.on('error', () => {
110 |
111 | if (!this.client) { // Failed to connect
112 | client.disconnect();
113 | }
114 | });
115 |
116 | await client.connect();
117 | this.client = client;
118 | }
119 |
120 | _connection(options) {
121 |
122 | if (this.settings.url) {
123 | return new IoRedis(this.settings.url, options);
124 | }
125 |
126 | if (this.settings.socket) {
127 | return new IoRedis(this.settings.socket, options);
128 | }
129 |
130 | return new IoRedis(options);
131 | }
132 |
133 | async stop() {
134 |
135 | if (!this.client) {
136 | return;
137 | }
138 |
139 | try {
140 | if (!this.settings.client) {
141 | this.client.removeAllListeners();
142 | await this.client.disconnect();
143 | }
144 | }
145 | finally {
146 | this.client = null;
147 | }
148 | }
149 |
150 | isReady() {
151 |
152 | return this.client?.status === 'ready';
153 | }
154 |
155 | validateSegmentName(name) {
156 |
157 | if (!name) {
158 | return new Error('Empty string');
159 | }
160 |
161 | if (name.indexOf('\0') !== -1) {
162 | return new Error('Includes null character');
163 | }
164 |
165 | return null;
166 | }
167 |
168 | async get(key) {
169 |
170 | if (!this.client) {
171 | throw Error('Connection not started');
172 | }
173 |
174 | const result = await this.client.get(this.generateKey(key));
175 | if (!result) {
176 | return null;
177 | }
178 |
179 | try {
180 | var envelope = Bourne.parse(result);
181 | }
182 | catch (ignoreErr) { } // Handled by validation below
183 |
184 | if (!envelope) {
185 | throw Error('Bad envelope content');
186 | }
187 |
188 | if (!envelope.stored ||
189 | !envelope.hasOwnProperty('item')) {
190 |
191 | throw Error('Incorrect envelope structure');
192 | }
193 |
194 | return envelope;
195 | }
196 |
197 | set(key, value, ttl) {
198 |
199 | if (!this.client) {
200 | throw Error('Connection not started');
201 | }
202 |
203 | const envelope = {
204 | item: value,
205 | stored: Date.now(),
206 | ttl
207 | };
208 |
209 | const cacheKey = this.generateKey(key);
210 | const stringifiedEnvelope = JSON.stringify(envelope);
211 |
212 | return this.client.psetex(cacheKey, ttl, stringifiedEnvelope);
213 | }
214 |
215 | drop(key) {
216 |
217 | if (!this.client) {
218 | throw Error('Connection not started');
219 | }
220 |
221 | return this.client.del(this.generateKey(key));
222 | }
223 |
224 | generateKey({ id, segment }) {
225 |
226 | const parts = [];
227 |
228 | if (this.settings.partition) {
229 | parts.push(encodeURIComponent(this.settings.partition));
230 | }
231 |
232 | parts.push(encodeURIComponent(segment));
233 | parts.push(encodeURIComponent(id));
234 |
235 | return parts.join(':');
236 | }
237 | };
238 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hapi/catbox-redis",
3 | "description": "Redis adapter for catbox",
4 | "version": "7.0.2",
5 | "repository": "git://github.com/hapijs/catbox-redis",
6 | "engines": {
7 | "node": ">=14.0.0"
8 | },
9 | "main": "lib/index.js",
10 | "types": "lib/index.d.ts",
11 | "files": [
12 | "lib"
13 | ],
14 | "keywords": [
15 | "cache",
16 | "catbox",
17 | "redis"
18 | ],
19 | "eslintConfig": {
20 | "extends": [
21 | "plugin:@hapi/module"
22 | ]
23 | },
24 | "dependencies": {
25 | "@hapi/bourne": "^3.0.0",
26 | "@hapi/hoek": "^11.0.2",
27 | "ioredis": "^5.0.0",
28 | "joi": "^17.7.1"
29 | },
30 | "devDependencies": {
31 | "@hapi/catbox": "^12.1.1",
32 | "@hapi/code": "^9.0.3",
33 | "@hapi/eslint-plugin": "^6.0.0",
34 | "@hapi/lab": "^25.1.2",
35 | "@types/node": "^16.18.39",
36 | "redis-parser": "^3.0.0",
37 | "typescript": "^5.1.6"
38 | },
39 | "scripts": {
40 | "test": "lab -t 100 -a @hapi/code -L -m 15000 -Y",
41 | "test-cov-html": "lab -r html -o coverage.html -a @hapi/code -L"
42 | },
43 | "license": "BSD-3-Clause"
44 | }
45 |
--------------------------------------------------------------------------------
/test/cluster.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Hoek = require('@hapi/hoek');
4 | const Catbox = require('@hapi/catbox');
5 | const { Engine: CatboxRedis } = require('..');
6 |
7 | const Code = require('@hapi/code');
8 | const Lab = require('@hapi/lab');
9 |
10 |
11 | const internals = {};
12 |
13 |
14 | const { describe, it } = exports.lab = Lab.script();
15 | const expect = Code.expect;
16 |
17 |
18 | describe('Redis Cluster', () => {
19 |
20 | it('creates connection to multiple nodes', async () => {
21 |
22 | const connection = new CatboxRedis({
23 | cluster: [
24 | {
25 | host: '127.0.0.1',
26 | port: 7000
27 | },
28 | {
29 | host: '127.0.0.1',
30 | port: 7001
31 | },
32 | {
33 | host: '127.0.0.1',
34 | port: 7002
35 | }
36 | ]
37 | });
38 |
39 | await connection.start();
40 | expect(connection.isReady()).to.equal(true);
41 | });
42 |
43 | it('closes the connection', async () => {
44 |
45 | const connection = new CatboxRedis({
46 | cluster: [
47 | {
48 | host: '127.0.0.1',
49 | port: 7000
50 | }
51 | ]
52 | });
53 |
54 | const client = new Catbox.Client(connection);
55 | await client.start();
56 | expect(client.isReady()).to.equal(true);
57 | await client.stop();
58 | expect(client.isReady()).to.equal(false);
59 | });
60 |
61 | it('gets an item after setting it', async () => {
62 |
63 | const connection = new CatboxRedis({
64 | cluster: [
65 | {
66 | host: '127.0.0.1',
67 | port: 7000
68 | }
69 | ]
70 | });
71 |
72 | const client = new Catbox.Client(connection);
73 | await client.start();
74 |
75 | const key = { id: 'x', segment: 'test' };
76 | await client.set(key, '123', 5000);
77 |
78 | const result = await client.get(key);
79 | expect(result.item).to.equal('123');
80 | });
81 |
82 | it('fails setting an item circular references', async () => {
83 |
84 | const connection = new CatboxRedis({
85 | cluster: [
86 | {
87 | host: '127.0.0.1',
88 | port: 7000
89 | }
90 | ]
91 | });
92 |
93 | const client = new Catbox.Client(connection);
94 | await client.start();
95 | const key = { id: 'x', segment: 'test' };
96 | const value = { a: 1 };
97 | value.b = value;
98 |
99 | await expect(client.set(key, value, 10)).to.reject(/Converting circular structure to JSON/);
100 | });
101 |
102 | it('ignored starting a connection twice chained', async () => {
103 |
104 | const connection = new CatboxRedis({
105 | cluster: [
106 | {
107 | host: '127.0.0.1',
108 | port: 7000
109 | }
110 | ]
111 | });
112 |
113 | const client = new Catbox.Client(connection);
114 |
115 | await client.start();
116 | expect(client.isReady()).to.equal(true);
117 |
118 | await client.start();
119 | expect(client.isReady()).to.equal(true);
120 | });
121 |
122 | it('returns not found on get when using null key', async () => {
123 |
124 | const connection = new CatboxRedis({
125 | cluster: [
126 | {
127 | host: '127.0.0.1',
128 | port: 7000
129 | }
130 | ]
131 | });
132 |
133 | const client = new Catbox.Client(connection);
134 | await client.start();
135 |
136 | const result = await client.get(null);
137 |
138 | expect(result).to.equal(null);
139 | });
140 |
141 | it('returns not found on get when item expired', async () => {
142 |
143 | const connection = new CatboxRedis({
144 | cluster: [
145 | {
146 | host: '127.0.0.1',
147 | port: 7000
148 | }
149 | ]
150 | });
151 |
152 | const client = new Catbox.Client(connection);
153 | await client.start();
154 |
155 | const key = { id: 'x', segment: 'test' };
156 | await client.set(key, 'x', 10);
157 |
158 | await Hoek.wait(20);
159 |
160 | const result = await client.get(key);
161 | expect(result).to.equal(null);
162 | });
163 |
164 | it('returns error on set when using null key', async () => {
165 |
166 | const connection = new CatboxRedis({
167 | cluster: [
168 | {
169 | host: '127.0.0.1',
170 | port: 7000
171 | }
172 | ]
173 | });
174 |
175 | const client = new Catbox.Client(connection);
176 | await client.start();
177 |
178 | await expect(client.set(null, {}, 1000)).to.reject();
179 | });
180 |
181 | it('returns error on get when using invalid key', async () => {
182 |
183 | const connection = new CatboxRedis({
184 | cluster: [
185 | {
186 | host: '127.0.0.1',
187 | port: 7000
188 | }
189 | ]
190 | });
191 |
192 | const client = new Catbox.Client(connection);
193 | await client.start();
194 |
195 | await expect(client.get({})).to.reject();
196 | });
197 |
198 | it('returns error on drop when using invalid key', async () => {
199 |
200 | const connection = new CatboxRedis({
201 | cluster: [
202 | {
203 | host: '127.0.0.1',
204 | port: 7000
205 | }
206 | ]
207 | });
208 |
209 | const client = new Catbox.Client(connection);
210 | await client.start();
211 |
212 | await expect(client.drop({})).to.reject();
213 | });
214 |
215 | it('returns error on set when using invalid key', async () => {
216 |
217 | const connection = new CatboxRedis({
218 | cluster: [
219 | {
220 | host: '127.0.0.1',
221 | port: 7000
222 | }
223 | ]
224 | });
225 |
226 | const client = new Catbox.Client(connection);
227 | await client.start();
228 |
229 | await expect(client.set({}, {}, 1000)).to.reject();
230 | });
231 |
232 | it('ignores set when using non-positive ttl value', async () => {
233 |
234 | const connection = new CatboxRedis({
235 | cluster: [
236 | {
237 | host: '127.0.0.1',
238 | port: 7000
239 | }
240 | ]
241 | });
242 |
243 | const client = new Catbox.Client(connection);
244 | await client.start();
245 | const key = { id: 'x', segment: 'test' };
246 | await client.set(key, 'y', 0);
247 | });
248 |
249 | it('returns error on drop when using null key', async () => {
250 |
251 | const connection = new CatboxRedis({
252 | cluster: [
253 | {
254 | host: '127.0.0.1',
255 | port: 7000
256 | }
257 | ]
258 | });
259 |
260 | const client = new Catbox.Client(connection);
261 | await client.start();
262 |
263 | await expect(client.drop(null)).to.reject();
264 | });
265 |
266 | it('returns error on get when stopped', async () => {
267 |
268 | const connection = new CatboxRedis({
269 | cluster: [
270 | {
271 | host: '127.0.0.1',
272 | port: 7000
273 | }
274 | ]
275 | });
276 |
277 | const client = new Catbox.Client(connection);
278 | await client.stop();
279 |
280 | const key = { id: 'x', segment: 'test' };
281 | await expect(client.connection.get(key)).to.reject('Connection not started');
282 | });
283 |
284 | it('returns error on set when stopped', async () => {
285 |
286 | const connection = new CatboxRedis({
287 | cluster: [
288 | {
289 | host: '127.0.0.1',
290 | port: 7000
291 | }
292 | ]
293 | });
294 |
295 | const client = new Catbox.Client(connection);
296 | await client.stop();
297 |
298 | const key = { id: 'x', segment: 'test' };
299 | expect(() => client.connection.set(key, 'y', 1)).to.throw('Connection not started');
300 | });
301 |
302 | it('returns error on drop when stopped', async () => {
303 |
304 | const connection = new CatboxRedis({
305 | cluster: [
306 | {
307 | host: '127.0.0.1',
308 | port: 7000
309 | }
310 | ]
311 | });
312 |
313 | const client = new Catbox.Client(connection);
314 | await client.stop();
315 |
316 | const key = { id: 'x', segment: 'test' };
317 |
318 | try {
319 | await client.connection.drop(key);
320 | }
321 | catch (err) {
322 | expect(err.message).to.equal('Connection not started');
323 | }
324 | });
325 |
326 | it('returns error on missing segment name', () => {
327 |
328 | const config = {
329 | expiresIn: 50000
330 | };
331 |
332 | const fn = () => {
333 |
334 | const connection = new CatboxRedis({
335 | cluster: [
336 | {
337 | host: '127.0.0.1',
338 | port: 7000
339 | }
340 | ]
341 | });
342 |
343 | const client = new Catbox.Client(connection);
344 | new Catbox.Policy(config, client, '');
345 | };
346 |
347 | expect(fn).to.throw();
348 | });
349 |
350 | it('returns error on bad segment name', () => {
351 |
352 | const config = {
353 | expiresIn: 50000
354 | };
355 |
356 | const fn = () => {
357 |
358 | const connection = new CatboxRedis({
359 | cluster: [
360 | {
361 | host: '127.0.0.1',
362 | port: 7000
363 | }
364 | ]
365 | });
366 |
367 | const client = new Catbox.Client(connection);
368 | new Catbox.Policy(config, client, 'a\0b');
369 | };
370 |
371 | expect(fn).to.throw();
372 | });
373 |
374 | it('returns error when cache item dropped while stopped', async () => {
375 |
376 | const connection = new CatboxRedis({
377 | cluster: [
378 | {
379 | host: '127.0.0.1',
380 | port: 7000
381 | }
382 | ]
383 | });
384 |
385 | const client = new Catbox.Client(connection);
386 | await client.stop();
387 |
388 | await expect(client.drop('a')).to.reject();
389 | });
390 |
391 | describe('start()', () => {
392 |
393 | it('sets client to when the connection succeeds', async () => {
394 |
395 | const redisCluster = new CatboxRedis({
396 | cluster: [
397 | {
398 | host: '127.0.0.1',
399 | port: 7000
400 | }
401 | ]
402 | });
403 |
404 | await redisCluster.start();
405 | expect(redisCluster.client).to.exist();
406 | });
407 |
408 | it('returns an error when connection fails', async () => {
409 |
410 | const redisCluster = new CatboxRedis({
411 | cluster: [
412 | {
413 | host: '127.0.0.20',
414 | port: 27000
415 | }
416 | ]
417 | });
418 |
419 | await expect(redisCluster.start()).to.reject();
420 | });
421 |
422 | it('sends select command when database is provided', async () => {
423 |
424 | const redisCluster = new CatboxRedis({
425 | cluster: [
426 | {
427 | host: '127.0.0.1',
428 | port: 7000
429 | }
430 | ],
431 | database: 1
432 | });
433 |
434 | await redisCluster.start();
435 | expect(redisCluster.client).to.exist();
436 | });
437 | });
438 |
439 | describe('', () => {
440 |
441 | it('does not stops the client on error post connection', async () => {
442 |
443 | const redisCluster = new CatboxRedis({
444 | cluster: [
445 | {
446 | host: '127.0.0.1',
447 | port: 7000
448 | }
449 | ]
450 | });
451 |
452 | await redisCluster.start();
453 | expect(redisCluster.client).to.exist();
454 |
455 | redisCluster.client.emit('error', new Error('injected'));
456 | expect(redisCluster.client).to.exist();
457 | });
458 | });
459 |
460 | describe('isReady()', () => {
461 |
462 | it('returns true when when connected', async () => {
463 |
464 | const redisCluster = new CatboxRedis({
465 | cluster: [
466 | {
467 | host: '127.0.0.1',
468 | port: 7000
469 | }
470 | ]
471 | });
472 |
473 | await redisCluster.start();
474 | expect(redisCluster.client).to.exist();
475 | expect(redisCluster.isReady()).to.equal(true);
476 | await redisCluster.stop();
477 | });
478 |
479 | it('returns false when stopped', async () => {
480 |
481 | const redisCluster = new CatboxRedis({
482 | cluster: [
483 | {
484 | host: '127.0.0.1',
485 | port: 7000
486 | }
487 | ]
488 | });
489 |
490 | await redisCluster.start();
491 | expect(redisCluster.client).to.exist();
492 | expect(redisCluster.isReady()).to.equal(true);
493 | await redisCluster.stop();
494 | expect(redisCluster.isReady()).to.equal(false);
495 | });
496 | });
497 |
498 | describe('validateSegmentName()', () => {
499 |
500 | it('returns an error when the name is empty', () => {
501 |
502 | const redisCluster = new CatboxRedis({
503 | cluster: [
504 | {
505 | host: '127.0.0.1',
506 | port: 7000
507 | }
508 | ]
509 | });
510 |
511 | const result = redisCluster.validateSegmentName('');
512 |
513 | expect(result).to.be.an.error();
514 | expect(result.message).to.equal('Empty string');
515 | });
516 |
517 | it('returns an error when the name has a null character', () => {
518 |
519 | const redisCluster = new CatboxRedis({
520 | cluster: [
521 | {
522 | host: '127.0.0.1',
523 | port: 7000
524 | }
525 | ]
526 | });
527 |
528 | const result = redisCluster.validateSegmentName('\0test');
529 |
530 | expect(result).to.be.an.error();
531 | });
532 |
533 | it('returns null when there aren\'t any errors', () => {
534 |
535 | const redisCluster = new CatboxRedis({
536 | cluster: [
537 | {
538 | host: '127.0.0.1',
539 | port: 7000
540 | }
541 | ]
542 | });
543 |
544 | const result = redisCluster.validateSegmentName('valid');
545 |
546 | expect(result).to.not.be.an.error();
547 | expect(result).to.equal(null);
548 | });
549 | });
550 |
551 | describe('get()', () => {
552 |
553 | it('returns a promise that rejects when the connection is closed', async () => {
554 |
555 | const redisCluster = new CatboxRedis({
556 | cluster: [
557 | {
558 | host: '127.0.0.1',
559 | port: 7000
560 | }
561 | ]
562 | });
563 |
564 | try {
565 | await redisCluster.get('test');
566 | }
567 | catch (err) {
568 | expect(err.message).to.equal('Connection not started');
569 | }
570 | });
571 |
572 | it('returns a promise that rejects when there is an error returned from getting an item', async () => {
573 |
574 | const redisCluster = new CatboxRedis({
575 | cluster: [
576 | {
577 | host: '127.0.0.1',
578 | port: 7000
579 | }
580 | ]
581 | });
582 |
583 | redisCluster.client = {
584 | get: function (item) {
585 |
586 | return Promise.reject(Error());
587 | }
588 | };
589 |
590 | await expect(redisCluster.get('test')).to.reject();
591 | });
592 |
593 | it('returns a promise that rejects when there is an error parsing the result', async () => {
594 |
595 | const redisCluster = new CatboxRedis({
596 | cluster: [
597 | {
598 | host: '127.0.0.1',
599 | port: 7000
600 | }
601 | ]
602 | });
603 |
604 | redisCluster.client = {
605 |
606 | get: function (item) {
607 |
608 | return Promise.resolve('test');
609 | }
610 | };
611 |
612 | await expect(redisCluster.get('test')).to.reject('Bad envelope content');
613 | });
614 |
615 | it('returns a promise that rejects when there is an error with the envelope structure (stored)', async () => {
616 |
617 | const redisCluster = new CatboxRedis({
618 | cluster: [
619 | {
620 | host: '127.0.0.1',
621 | port: 7000
622 | }
623 | ]
624 | });
625 |
626 | redisCluster.client = {
627 | get: function (item) {
628 |
629 | return Promise.resolve('{ "item": "false" }');
630 | }
631 | };
632 |
633 | await expect(redisCluster.get('test')).to.reject('Incorrect envelope structure');
634 | });
635 |
636 | it('returns a promise that rejects when there is an error with the envelope structure (item)', async () => {
637 |
638 | const redisCluster = new CatboxRedis({
639 | cluster: [
640 | {
641 | host: '127.0.0.1',
642 | port: 7000
643 | }
644 | ]
645 | });
646 |
647 | redisCluster.client = {
648 | get: function (item) {
649 |
650 | return Promise.resolve('{ "stored": "123" }');
651 | }
652 | };
653 |
654 | await expect(redisCluster.get('test')).to.reject('Incorrect envelope structure');
655 | });
656 |
657 | it('is able to retrieve an object thats stored when connection is started', async () => {
658 |
659 | const key = {
660 | id: 'test',
661 | segment: 'test'
662 | };
663 |
664 | const redisCluster = new CatboxRedis({
665 | cluster: [
666 | {
667 | host: '127.0.0.1',
668 | port: 7000
669 | }
670 | ],
671 | partition: 'wwwtest'
672 | });
673 |
674 | await redisCluster.start();
675 | await redisCluster.set(key, 'myvalue', 200);
676 | const result = await redisCluster.get(key);
677 | expect(result.item).to.equal('myvalue');
678 | });
679 |
680 | it('returns null when unable to find the item', async () => {
681 |
682 | const key = {
683 | id: 'notfound',
684 | segment: 'notfound'
685 | };
686 |
687 | const redisCluster = new CatboxRedis({
688 | cluster: [
689 | {
690 | host: '127.0.0.1',
691 | port: 7000
692 | }
693 | ],
694 | partition: 'wwwtest'
695 | });
696 |
697 | await redisCluster.start();
698 | const result = await redisCluster.get(key);
699 | expect(result).to.not.exist();
700 | });
701 |
702 | it('can store and retrieve falsy values such as int 0', async () => {
703 |
704 | const key = {
705 | id: 'test',
706 | segment: 'test'
707 | };
708 |
709 | const redisCluster = new CatboxRedis({
710 | cluster: [
711 | {
712 | host: '127.0.0.1',
713 | port: 7000
714 | }
715 | ],
716 | partition: 'wwwtest'
717 | });
718 |
719 | await redisCluster.start();
720 | await redisCluster.set(key, 0, 200);
721 | const result = await redisCluster.get(key);
722 | expect(result.item).to.equal(0);
723 | });
724 |
725 | it('can store and retrieve falsy values such as boolean false', async () => {
726 |
727 | const key = {
728 | id: 'test',
729 | segment: 'test'
730 | };
731 |
732 | const redisCluster = new CatboxRedis({
733 | cluster: [
734 | {
735 | host: '127.0.0.1',
736 | port: 7000
737 | }
738 | ],
739 | partition: 'wwwtest'
740 | });
741 |
742 | await redisCluster.start();
743 | await redisCluster.set(key, false, 200);
744 | const result = await redisCluster.get(key);
745 | expect(result.item).to.equal(false);
746 | });
747 | });
748 |
749 | describe('set()', () => {
750 |
751 | it('returns a promise that rejects when the connection is closed', async () => {
752 |
753 | const redisCluster = new CatboxRedis({
754 | cluster: [
755 | {
756 | host: '127.0.0.1',
757 | port: 7000
758 | }
759 | ]
760 | });
761 |
762 | try {
763 | await redisCluster.set('test1', 'test1', 3600);
764 | }
765 | catch (err) {
766 | expect(err.message).to.equal('Connection not started');
767 | }
768 | });
769 |
770 | it('returns a promise that rejects when there is an error returned from setting an item', async () => {
771 |
772 | const redisCluster = new CatboxRedis({
773 | cluster: [
774 | {
775 | host: '127.0.0.1',
776 | port: 7000
777 | }
778 | ]
779 | });
780 |
781 | redisCluster.client = {
782 | psetex: function (key, ttl, value) {
783 |
784 | return Promise.reject(Error());
785 | }
786 | };
787 |
788 | await expect(redisCluster.set('test', 'test', 3600)).to.reject();
789 | });
790 | });
791 |
792 | describe('drop()', () => {
793 |
794 | it('returns a promise that rejects when the connection is closed', async () => {
795 |
796 | const redisCluster = new CatboxRedis({
797 | cluster: [
798 | {
799 | host: '127.0.0.1',
800 | port: 7000
801 | }
802 | ]
803 | });
804 |
805 | try {
806 | await redisCluster.drop('test2');
807 | }
808 | catch (err) {
809 | expect(err.message).to.equal('Connection not started');
810 | }
811 | });
812 |
813 | it('deletes the item from redis', async () => {
814 |
815 | const redisCluster = new CatboxRedis({
816 | cluster: [
817 | {
818 | host: '127.0.0.1',
819 | port: 7000
820 | }
821 | ]
822 | });
823 |
824 | redisCluster.client = {
825 | del: function (key) {
826 |
827 | return Promise.resolve(null);
828 | }
829 | };
830 |
831 | await redisCluster.drop('test');
832 | });
833 | });
834 |
835 | describe('generateKey()', () => {
836 |
837 | it('generates the storage key from a given catbox key', () => {
838 |
839 | const redisCluster = new CatboxRedis({
840 | cluster: [
841 | {
842 | host: '127.0.0.1',
843 | port: 7000
844 | }
845 | ],
846 | partition: 'foo'
847 | });
848 |
849 | const key = {
850 | id: 'bar',
851 | segment: 'baz'
852 | };
853 |
854 | expect(redisCluster.generateKey(key)).to.equal('foo:baz:bar');
855 | });
856 |
857 | it('generates the storage key from a given catbox key without partition', () => {
858 |
859 | const redisCluster = new CatboxRedis({
860 | cluster: [
861 | {
862 | host: '127.0.0.1',
863 | port: 7000
864 | }
865 | ]
866 | });
867 |
868 | const key = {
869 | id: 'bar',
870 | segment: 'baz'
871 | };
872 |
873 | expect(redisCluster.generateKey(key)).to.equal('baz:bar');
874 | });
875 | });
876 |
877 | describe('stop()', () => {
878 |
879 | it('sets the client to null', async () => {
880 |
881 | const redisCluster = new CatboxRedis({
882 | cluster: [
883 | {
884 | host: '127.0.0.1',
885 | port: 7000
886 | }
887 | ]
888 | });
889 |
890 | await redisCluster.start();
891 | expect(redisCluster.client).to.exist();
892 | await redisCluster.stop();
893 | expect(redisCluster.client).to.not.exist();
894 | });
895 | });
896 | });
897 |
--------------------------------------------------------------------------------
/test/esm.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Code = require('@hapi/code');
4 | const Lab = require('@hapi/lab');
5 |
6 |
7 | const { before, describe, it } = exports.lab = Lab.script();
8 | const expect = Code.expect;
9 |
10 |
11 | describe('import()', () => {
12 |
13 | let CatboxRedis;
14 |
15 | before(async () => {
16 |
17 | CatboxRedis = await import('../lib/index.js');
18 | });
19 |
20 | it('exposes all methods and classes as named imports', () => {
21 |
22 | expect(Object.keys(CatboxRedis)).to.equal([
23 | 'Engine',
24 | 'default'
25 | ]);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Net = require('net');
4 |
5 | const IoRedis = require('ioredis');
6 | const Catbox = require('@hapi/catbox');
7 | const Hoek = require('@hapi/hoek');
8 | const Mock = require('./mock');
9 | const { Engine: CatboxRedis } = require('..');
10 |
11 | const Code = require('@hapi/code');
12 | const Lab = require('@hapi/lab');
13 |
14 | const internals = {};
15 |
16 | const { it, describe } = exports.lab = Lab.script();
17 | const expect = Code.expect;
18 |
19 | describe('Connection', { retry: true }, () => {
20 |
21 | it('creates a new connection', async () => {
22 |
23 | const client = new Catbox.Client(CatboxRedis);
24 | await client.start();
25 | expect(client.isReady()).to.equal(true);
26 | });
27 |
28 | it('closes the connection', async () => {
29 |
30 | const client = new Catbox.Client(CatboxRedis);
31 | await client.start();
32 | expect(client.isReady()).to.equal(true);
33 | await client.stop();
34 | expect(client.isReady()).to.equal(false);
35 | });
36 |
37 | it('allow passing client in option', () => {
38 |
39 | return new Promise((resolve, reject) => {
40 |
41 | const redisClient = IoRedis.createClient();
42 |
43 | let getCalled = false;
44 | const _get = redisClient.get;
45 | redisClient.get = function (...args) {
46 |
47 | getCalled = true;
48 | return _get.apply(redisClient, args);
49 | };
50 |
51 | redisClient.on('error', reject);
52 | redisClient.once('ready', async () => {
53 |
54 | const client = new Catbox.Client(CatboxRedis, {
55 | client: redisClient
56 | });
57 | await client.start();
58 | expect(client.isReady()).to.equal(true);
59 | const key = { id: 'x', segment: 'test' };
60 | await client.get(key);
61 | expect(getCalled).to.equal(true);
62 |
63 | resolve();
64 | });
65 | });
66 | });
67 |
68 | it('does not stop provided client in options', async () => {
69 |
70 | const redisClient = IoRedis.createClient();
71 |
72 | await new Promise((resolve, reject) => {
73 |
74 | redisClient.once('error', reject);
75 | redisClient.once('ready', resolve);
76 | });
77 |
78 | const client = new Catbox.Client(CatboxRedis, { client: redisClient });
79 | await client.start();
80 | expect(client.isReady()).to.equal(true);
81 | await client.stop();
82 | expect(client.isReady()).to.equal(false);
83 | expect(redisClient.status).to.equal('ready');
84 | await redisClient.quit();
85 | });
86 |
87 | it('gets an item after setting it', async () => {
88 |
89 | const client = new Catbox.Client(CatboxRedis);
90 | await client.start();
91 |
92 | const key = { id: 'x', segment: 'test' };
93 | await client.set(key, '123', 500);
94 |
95 | const result = await client.get(key);
96 | expect(result.item).to.equal('123');
97 | });
98 |
99 | it('fails setting an item circular references', async () => {
100 |
101 | const client = new Catbox.Client(CatboxRedis);
102 | await client.start();
103 | const key = { id: 'x', segment: 'test' };
104 | const value = { a: 1 };
105 | value.b = value;
106 |
107 | await expect(client.set(key, value, 10)).to.reject(/Converting circular structure to JSON/);
108 | });
109 |
110 | it('ignored starting a connection twice on same event', () => {
111 |
112 | return new Promise((resolve, reject) => {
113 |
114 | const client = new Catbox.Client(CatboxRedis);
115 | let x = 2;
116 | const start = async () => {
117 |
118 | await client.start();
119 | expect(client.isReady()).to.equal(true);
120 | --x;
121 | if (!x) {
122 | resolve();
123 | }
124 | };
125 |
126 | start();
127 | start();
128 | });
129 | });
130 |
131 | it('ignored starting a connection twice chained', async () => {
132 |
133 | const client = new Catbox.Client(CatboxRedis);
134 |
135 | await client.start();
136 | expect(client.isReady()).to.equal(true);
137 |
138 | await client.start();
139 | expect(client.isReady()).to.equal(true);
140 | });
141 |
142 | it('returns not found on get when using null key', async () => {
143 |
144 | const client = new Catbox.Client(CatboxRedis);
145 | await client.start();
146 |
147 | const result = await client.get(null);
148 |
149 | expect(result).to.equal(null);
150 | });
151 |
152 | it('returns not found on get when item expired', async () => {
153 |
154 | const client = new Catbox.Client(CatboxRedis);
155 | await client.start();
156 |
157 | const key = { id: 'x', segment: 'test' };
158 | await client.set(key, 'x', 1);
159 |
160 | await Hoek.wait(2);
161 | const result = await client.get(key);
162 | expect(result).to.equal(null);
163 | });
164 |
165 | it('errors on set when using null key', async () => {
166 |
167 | const client = new Catbox.Client(CatboxRedis);
168 | await client.start();
169 |
170 | await expect(client.set(null, {}, 1000)).to.reject();
171 | });
172 |
173 | it('errors on get when using invalid key', async () => {
174 |
175 | const client = new Catbox.Client(CatboxRedis);
176 | await client.start();
177 |
178 | await expect(client.get({})).to.reject();
179 | });
180 |
181 | it('errors on drop when using invalid key', async () => {
182 |
183 | const client = new Catbox.Client(CatboxRedis);
184 | await client.start();
185 |
186 | await expect(client.drop({})).to.reject();
187 | });
188 |
189 | it('errors on set when using invalid key', async () => {
190 |
191 | const client = new Catbox.Client(CatboxRedis);
192 | await client.start();
193 |
194 | await expect(client.set({}, {}, 1000)).to.reject();
195 | });
196 |
197 | it('ignores set when using non-positive ttl value', async () => {
198 |
199 | const client = new Catbox.Client(CatboxRedis);
200 | await client.start();
201 | const key = { id: 'x', segment: 'test' };
202 | await client.set(key, 'y', 0);
203 | });
204 |
205 | it('errors on drop when using null key', async () => {
206 |
207 | const client = new Catbox.Client(CatboxRedis);
208 | await client.start();
209 |
210 | await expect(client.drop(null)).to.reject();
211 | });
212 |
213 | it('errors on get when stopped', async () => {
214 |
215 | const client = new Catbox.Client(CatboxRedis);
216 | await client.stop();
217 |
218 | const key = { id: 'x', segment: 'test' };
219 | await expect(client.connection.get(key)).to.reject('Connection not started');
220 | });
221 |
222 | it('errors on set when stopped', async () => {
223 |
224 | const client = new Catbox.Client(CatboxRedis);
225 | await client.stop();
226 |
227 | const key = { id: 'x', segment: 'test' };
228 | expect(() => client.connection.set(key, 'y', 1)).to.throw('Connection not started');
229 | });
230 |
231 | it('errors on drop when stopped', async () => {
232 |
233 | const client = new Catbox.Client(CatboxRedis);
234 | await client.stop();
235 |
236 | const key = { id: 'x', segment: 'test' };
237 |
238 | try {
239 | await client.connection.drop(key);
240 | }
241 | catch (err) {
242 | expect(err.message).to.equal('Connection not started');
243 | }
244 | });
245 |
246 | it('errors on missing segment name', () => {
247 |
248 | const config = {
249 | expiresIn: 50000
250 | };
251 | const fn = () => {
252 |
253 | const client = new Catbox.Client(CatboxRedis);
254 | new Catbox.Policy(config, client, '');
255 | };
256 |
257 | expect(fn).to.throw(Error);
258 | });
259 |
260 | it('errors on bad segment name', () => {
261 |
262 | const config = {
263 | expiresIn: 50000
264 | };
265 | const fn = () => {
266 |
267 | const client = new Catbox.Client(CatboxRedis);
268 | new Catbox.Policy(config, client, 'a\0b');
269 | };
270 |
271 | expect(fn).to.throw(Error);
272 | });
273 |
274 | it('errors when cache item dropped while stopped', async () => {
275 |
276 | const client = new Catbox.Client(CatboxRedis);
277 | await client.stop();
278 |
279 | await expect(client.drop('a')).to.reject();
280 | });
281 |
282 | describe('start()', () => {
283 |
284 | it('sets client to when the connection succeeds', async () => {
285 |
286 | const options = {
287 | host: '127.0.0.1',
288 | port: 6379
289 | };
290 |
291 | const redis = new CatboxRedis(options);
292 |
293 | await redis.start();
294 | expect(redis.client).to.exist();
295 | });
296 |
297 | it('reuses the client when a connection is already started', async () => {
298 |
299 | const options = {
300 | host: '127.0.0.1',
301 | port: 6379
302 | };
303 |
304 | const redis = new CatboxRedis(options);
305 |
306 | await redis.start();
307 | const client = redis.client;
308 |
309 | await redis.start();
310 | expect(client).to.equal(redis.client);
311 | });
312 |
313 | it('returns an error when connection fails', async () => {
314 |
315 | const options = {
316 | host: '127.0.0.1',
317 | port: 6380
318 | };
319 |
320 | const redis = new CatboxRedis(options);
321 |
322 | await expect(redis.start()).to.reject();
323 |
324 | expect(redis.client).to.not.exist();
325 | });
326 |
327 | it('sends auth command when password is provided', async () => {
328 |
329 | const options = {
330 | host: '127.0.0.1',
331 | port: 6379,
332 | password: 'wrongpassword'
333 | };
334 |
335 | const redis = new CatboxRedis(options);
336 |
337 | const warn = console.warn;
338 | let consoleMessage = '';
339 | console.warn = function (message) {
340 |
341 | consoleMessage += message;
342 | };
343 |
344 | await redis.start();
345 |
346 | console.warn = warn;
347 | expect(consoleMessage).to.contain('does not require a password, but a password was supplied');
348 | });
349 |
350 | it('fails in error when auth is not correct', async () => {
351 |
352 | const options = {
353 | host: '127.0.0.1',
354 | port: 6378,
355 | password: 'foo'
356 | };
357 |
358 | const redis = new CatboxRedis(options);
359 |
360 | await expect(redis.start()).to.reject();
361 |
362 | expect(redis.client).to.not.exist();
363 | });
364 |
365 | it('success when auth is correct', async () => {
366 |
367 | const options = {
368 | host: '127.0.0.1',
369 | port: 6378,
370 | password: 'secret'
371 | };
372 |
373 | const redis = new CatboxRedis(options);
374 |
375 | await redis.start();
376 | expect(redis.client).to.exist();
377 | });
378 |
379 | it('sends select command when database is provided', async () => {
380 |
381 | const options = {
382 | host: '127.0.0.1',
383 | port: 6379,
384 | database: 1
385 | };
386 |
387 | const redis = new CatboxRedis(options);
388 |
389 | await redis.start();
390 | expect(redis.client).to.exist();
391 | });
392 |
393 | it('connects to a unix domain socket when one is provided', () => {
394 |
395 | const socketPath = '/tmp/catbox-redis.sock';
396 | const promise = new Promise((resolve, reject) => {
397 |
398 | let connected = false;
399 | const server = new Net.createServer((socket) => {
400 |
401 | connected = true;
402 | socket.destroy();
403 | });
404 |
405 | server.once('error', reject);
406 | server.listen(socketPath, async () => {
407 |
408 | const redis = new CatboxRedis({ socket: socketPath });
409 | await expect(redis.start()).to.reject('Connection is closed.');
410 | expect(connected).to.equal(true);
411 | server.close(resolve);
412 | });
413 | });
414 |
415 | return promise;
416 | });
417 |
418 | it('connects via a Redis URL when one is provided', async () => {
419 |
420 | const options = {
421 | url: 'redis://127.0.0.1:6379'
422 | };
423 |
424 | const redis = new CatboxRedis(options);
425 |
426 | await redis.start();
427 | expect(redis.client).to.exist();
428 | });
429 |
430 | describe('', () => {
431 |
432 | it('connects to a sentinel cluster', async () => {
433 |
434 | const sentinel = new Mock(27379, (argv) => {
435 |
436 | if (argv[0] === 'sentinel' && argv[1] === 'get-master-addr-by-name') {
437 | return ['127.0.0.1', '6379'];
438 | }
439 | });
440 |
441 | sentinel.once('connect', () => {
442 |
443 | sentinel.disconnect();
444 | });
445 |
446 | const options = {
447 | sentinels: [
448 | {
449 | host: '127.0.0.1',
450 | port: 27379
451 | },
452 | {
453 | host: '127.0.0.2',
454 | port: 27379
455 | }
456 | ],
457 | sentinelName: 'mymaster'
458 | };
459 |
460 | const redis = new CatboxRedis(options);
461 |
462 | await redis.start();
463 | const client = redis.client;
464 | expect(client).to.exist();
465 | expect(client.connector.options.sentinels).to.equal(options.sentinels);
466 | expect(client.connector.options.name).to.equal(options.sentinelName);
467 | });
468 | });
469 |
470 | it('does not stops the client on error post connection', async () => {
471 |
472 | const options = {
473 | host: '127.0.0.1',
474 | port: 6379
475 | };
476 |
477 | const redis = new CatboxRedis(options);
478 |
479 | await redis.start();
480 | expect(redis.client).to.exist();
481 |
482 | redis.client.emit('error', new Error('injected'));
483 | expect(redis.client).to.exist();
484 | });
485 | });
486 |
487 | describe('isReady()', () => {
488 |
489 | it('returns true when when connected', async () => {
490 |
491 | const options = {
492 | host: '127.0.0.1',
493 | port: 6379
494 | };
495 |
496 | const redis = new CatboxRedis(options);
497 |
498 | await redis.start();
499 | expect(redis.client).to.exist();
500 | expect(redis.isReady()).to.equal(true);
501 | await redis.stop();
502 | });
503 |
504 | it('returns false when stopped', async () => {
505 |
506 | const options = {
507 | host: '127.0.0.1',
508 | port: 6379
509 | };
510 |
511 | const redis = new CatboxRedis(options);
512 |
513 | await redis.start();
514 | expect(redis.client).to.exist();
515 | expect(redis.isReady()).to.equal(true);
516 | await redis.stop();
517 | expect(redis.isReady()).to.equal(false);
518 | });
519 | });
520 |
521 | describe('validateSegmentName()', () => {
522 |
523 | it('returns an error when the name is empty', () => {
524 |
525 | const options = {
526 | host: '127.0.0.1',
527 | port: 6379
528 | };
529 |
530 | const redis = new CatboxRedis(options);
531 |
532 | const result = redis.validateSegmentName('');
533 |
534 | expect(result).to.be.instanceOf(Error);
535 | expect(result.message).to.equal('Empty string');
536 | });
537 |
538 | it('returns an error when the name has a null character', () => {
539 |
540 | const options = {
541 | host: '127.0.0.1',
542 | port: 6379
543 | };
544 |
545 | const redis = new CatboxRedis(options);
546 |
547 | const result = redis.validateSegmentName('\0test');
548 |
549 | expect(result).to.be.instanceOf(Error);
550 | });
551 |
552 | it('returns null when there aren\'t any errors', () => {
553 |
554 | const options = {
555 | host: '127.0.0.1',
556 | port: 6379
557 | };
558 |
559 | const redis = new CatboxRedis(options);
560 |
561 | const result = redis.validateSegmentName('valid');
562 |
563 | expect(result).to.not.be.instanceOf(Error);
564 | expect(result).to.equal(null);
565 | });
566 | });
567 |
568 | describe('get()', () => {
569 |
570 | it('returns a promise that rejects when the connection is closed', async () => {
571 |
572 | const options = {
573 | host: '127.0.0.1',
574 | port: 6379
575 | };
576 |
577 | const redis = new CatboxRedis(options);
578 |
579 | try {
580 | await redis.get('test');
581 | }
582 | catch (err) {
583 | expect(err.message).to.equal('Connection not started');
584 | }
585 | });
586 |
587 | it('returns a promise that rejects when there is an error returned from getting an item', async () => {
588 |
589 | const options = {
590 | host: '127.0.0.1',
591 | port: 6379
592 | };
593 |
594 | const redis = new CatboxRedis(options);
595 | redis.client = {
596 | get: function (item) {
597 |
598 | return Promise.reject(Error());
599 | }
600 | };
601 |
602 | await expect(redis.get('test')).to.reject();
603 | });
604 |
605 | it('returns a promise that rejects when there is an error parsing the result', async () => {
606 |
607 | const options = {
608 | host: '127.0.0.1',
609 | port: 6379
610 | };
611 |
612 | const redis = new CatboxRedis(options);
613 | redis.client = {
614 |
615 | get: function (item) {
616 |
617 | return Promise.resolve('test');
618 | }
619 | };
620 |
621 | await expect(redis.get('test')).to.reject('Bad envelope content');
622 | });
623 |
624 | it('returns a promise that rejects when there is an error with the envelope structure (stored)', async () => {
625 |
626 | const options = {
627 | host: '127.0.0.1',
628 | port: 6379
629 | };
630 |
631 | const redis = new CatboxRedis(options);
632 | redis.client = {
633 | get: function (item) {
634 |
635 | return Promise.resolve('{ "item": "false" }');
636 | }
637 | };
638 |
639 | await expect(redis.get('test')).to.reject('Incorrect envelope structure');
640 | });
641 |
642 | it('returns a promise that rejects when there is an error with the envelope structure (item)', async () => {
643 |
644 | const options = {
645 | host: '127.0.0.1',
646 | port: 6379
647 | };
648 |
649 | const redis = new CatboxRedis(options);
650 | redis.client = {
651 | get: function (item) {
652 |
653 | return Promise.resolve('{ "stored": "123" }');
654 | }
655 | };
656 |
657 | await expect(redis.get('test')).to.reject('Incorrect envelope structure');
658 | });
659 |
660 | it('is able to retrieve an object thats stored when connection is started', async () => {
661 |
662 | const options = {
663 | host: '127.0.0.1',
664 | port: 6379,
665 | partition: 'wwwtest'
666 | };
667 | const key = {
668 | id: 'test',
669 | segment: 'test'
670 | };
671 |
672 | const redis = new CatboxRedis(options);
673 | await redis.start();
674 | await redis.set(key, 'myvalue', 200);
675 | const result = await redis.get(key);
676 | expect(result.item).to.equal('myvalue');
677 | });
678 |
679 | it('returns null when unable to find the item', async () => {
680 |
681 | const options = {
682 | host: '127.0.0.1',
683 | port: 6379,
684 | partition: 'wwwtest'
685 | };
686 | const key = {
687 | id: 'notfound',
688 | segment: 'notfound'
689 | };
690 |
691 | const redis = new CatboxRedis(options);
692 | await redis.start();
693 | const result = await redis.get(key);
694 | expect(result).to.not.exist();
695 | });
696 |
697 | it('can store and retrieve falsy values such as int 0', async () => {
698 |
699 | const options = {
700 | host: '127.0.0.1',
701 | port: 6379,
702 | partition: 'wwwtest'
703 | };
704 | const key = {
705 | id: 'test',
706 | segment: 'test'
707 | };
708 |
709 | const redis = new CatboxRedis(options);
710 | await redis.start();
711 | await redis.set(key, 0, 200);
712 | const result = await redis.get(key);
713 | expect(result.item).to.equal(0);
714 | });
715 |
716 | it('can store and retrieve falsy values such as boolean false', async () => {
717 |
718 | const options = {
719 | host: '127.0.0.1',
720 | port: 6379,
721 | partition: 'wwwtest'
722 | };
723 | const key = {
724 | id: 'test',
725 | segment: 'test'
726 | };
727 |
728 | const redis = new CatboxRedis(options);
729 | await redis.start();
730 | await redis.set(key, false, 200);
731 | const result = await redis.get(key);
732 | expect(result.item).to.equal(false);
733 | });
734 | });
735 |
736 | describe('set()', () => {
737 |
738 | it('returns a promise that rejects when the connection is closed', async () => {
739 |
740 | const options = {
741 | host: '127.0.0.1',
742 | port: 6379
743 | };
744 |
745 | const redis = new CatboxRedis(options);
746 |
747 | try {
748 | await redis.set('test1', 'test1', 3600);
749 | }
750 | catch (err) {
751 | expect(err.message).to.equal('Connection not started');
752 | }
753 | });
754 |
755 | it('returns a promise that rejects when there is an error returned from setting an item', async () => {
756 |
757 | const options = {
758 | host: '127.0.0.1',
759 | port: 6379
760 | };
761 |
762 | const redis = new CatboxRedis(options);
763 | redis.client = {
764 | psetex: function (key, ttls, value) {
765 |
766 | return Promise.reject(Error());
767 | }
768 | };
769 |
770 | await expect(redis.set('test', 'test', 3600)).to.reject();
771 | });
772 | });
773 |
774 | describe('drop()', () => {
775 |
776 | it('returns a promise that rejects when the connection is closed', async () => {
777 |
778 | const options = {
779 | host: '127.0.0.1',
780 | port: 6379
781 | };
782 |
783 | const redis = new CatboxRedis(options);
784 |
785 | try {
786 | await redis.drop('test2');
787 | }
788 | catch (err) {
789 | expect(err.message).to.equal('Connection not started');
790 | }
791 | });
792 |
793 | it('deletes the item from redis', async () => {
794 |
795 | const options = {
796 | host: '127.0.0.1',
797 | port: 6379
798 | };
799 |
800 | const redis = new CatboxRedis(options);
801 | redis.client = {
802 | del: function (key) {
803 |
804 | return Promise.resolve(null);
805 | }
806 | };
807 |
808 | await redis.drop('test');
809 | });
810 | });
811 |
812 | describe('generateKey()', () => {
813 |
814 | it('generates the storage key from a given catbox key', () => {
815 |
816 | const options = {
817 | partition: 'foo'
818 | };
819 |
820 | const redis = new CatboxRedis(options);
821 |
822 | const key = {
823 | id: 'bar',
824 | segment: 'baz'
825 | };
826 |
827 | expect(redis.generateKey(key)).to.equal('foo:baz:bar');
828 | });
829 |
830 | it('generates the storage key from a given catbox key without partition', () => {
831 |
832 | const options = {};
833 |
834 | const redis = new CatboxRedis(options);
835 |
836 | const key = {
837 | id: 'bar',
838 | segment: 'baz'
839 | };
840 |
841 | expect(redis.generateKey(key)).to.equal('baz:bar');
842 | });
843 | });
844 |
845 | describe('stop()', () => {
846 |
847 | it('sets the client to null', async () => {
848 |
849 | const options = {
850 | host: '127.0.0.1',
851 | port: 6379
852 | };
853 |
854 | const redis = new CatboxRedis(options);
855 |
856 | await redis.start();
857 | expect(redis.client).to.exist();
858 | await redis.stop();
859 | expect(redis.client).to.not.exist();
860 | });
861 | });
862 | });
863 |
--------------------------------------------------------------------------------
/test/mock.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Adapted from https://github.com/luin/ioredis
4 | // Copyright (c) 2015-2019 Zihua Li - MIT Licensed
5 |
6 | const EventEmitter = require('events').EventEmitter;
7 | const Net = require('net');
8 |
9 | const Parser = require('redis-parser');
10 |
11 |
12 | const internals = {};
13 |
14 |
15 | module.exports = internals.MockServer = class extends EventEmitter {
16 |
17 | constructor(port, handler) {
18 |
19 | super();
20 |
21 | this.REDIS_OK = '+OK';
22 |
23 | this.port = port;
24 | this.handler = handler;
25 |
26 | this.connect();
27 | }
28 |
29 | connect() {
30 |
31 | this.socket = Net.createServer();
32 |
33 | this.socket.on('connection', (socket) => {
34 |
35 | process.nextTick(() => this.emit('connect', socket));
36 |
37 | const parser = new Parser({
38 | returnBuffers: true,
39 | returnReply: (reply) => {
40 |
41 | reply = this.convertBufferToString(reply);
42 | this.write(socket, this.handler?.(reply));
43 | },
44 | returnError: function () {}
45 | });
46 |
47 | socket.on('end', function () {
48 |
49 | this.emit('disconnect', socket);
50 | });
51 |
52 | socket.on('data', (data) => {
53 |
54 | parser.execute(data);
55 | });
56 | });
57 |
58 | this.socket.listen(this.port);
59 | }
60 |
61 | write(c, input) {
62 |
63 | const convert = function (str, data) {
64 |
65 | let result;
66 |
67 | if (typeof data === 'undefined') {
68 | data = internals.MockServer.REDIS_OK;
69 | }
70 |
71 | if (data === internals.MockServer.REDIS_OK) {
72 | result = '+OK\r\n';
73 | }
74 | else if (data instanceof Error) {
75 | result = '-' + data.message + '\r\n';
76 | }
77 | else if (Array.isArray(data)) {
78 | result = '*' + data.length + '\r\n';
79 | data.forEach((item) => {
80 |
81 | result += convert(str, item);
82 | });
83 | }
84 | else if (typeof data === 'number') {
85 | result = ':' + data + '\r\n';
86 | }
87 | else if (data === null) {
88 | result = '$-1\r\n';
89 | }
90 | else {
91 | data = data.toString();
92 | result = '$' + data.length + '\r\n';
93 | result += data + '\r\n';
94 | }
95 |
96 | return str + result;
97 | };
98 |
99 | if (c.writable) {
100 | c.write(convert('', input));
101 | }
102 | }
103 |
104 | convertBufferToString(value, encoding) {
105 |
106 | if (value instanceof Buffer) {
107 | return value.toString(encoding);
108 | }
109 |
110 | if (Array.isArray(value)) {
111 | const length = value.length;
112 | const res = Array(length);
113 |
114 | for (let i = 0; i < length; ++i) {
115 | res[i] = value[i] instanceof Buffer && encoding === 'utf8'
116 | ? value[i].toString()
117 | : this.convertBufferToString(value[i], encoding);
118 | }
119 |
120 | return res;
121 | }
122 |
123 | return value;
124 | }
125 |
126 | disconnect() {
127 |
128 | this.socket.close();
129 | }
130 | };
131 |
--------------------------------------------------------------------------------
/test/types.ts:
--------------------------------------------------------------------------------
1 | import { Engine } from '..';
2 | import Redis, { Cluster } from 'ioredis';
3 |
4 | async function test() {
5 | const cache = new Engine({
6 | client: new Redis(),
7 | host: 'localhost',
8 | partition: 'test',
9 | port: 2018,
10 | });
11 |
12 | await cache.start()
13 |
14 | cache.get({
15 | segment: 'test',
16 | id: 'test',
17 | });
18 |
19 | cache.set({
20 | segment: 'test',
21 | id: 'test',
22 | }, 'test', 123);
23 |
24 |
25 | new Engine({
26 | client: new Cluster([
27 | {
28 | host: '127.0.0.1',
29 | port: 27379
30 | },
31 | {
32 | host: '127.0.0.2',
33 | port: 27379
34 | }
35 | ])
36 | });
37 | }
38 |
39 | test();
40 |
--------------------------------------------------------------------------------