1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-07 13:22:56 +03:00
Files
node-redis/lib/client.spec.ts
Leibale Eidelman 4e6d018d77 V4 (#1624)
* init v4

* add .gitignore to benchmark

* spawn redis-servers for tests,
add some tests,
fix client auth on connect

* add tests coverage report

* add tests workflow, replace nyc text reporter with text-summary

* run tests with node 16.x & redis 6.x only (for now)

* add socket events on client,
stop reconnectiong when manually calling disconnect,
remove abort signal listener when a command is written on the socket

* add isOpen boolean getter on client, add maxLength option to command queue, add test for client.multi

* move to use CommonJS

* add MULTI and EXEC commands to when executing multi command, make client.multi return type innerit the module commands, clean some tests, exclute spec files from coverage report

* missing file from commit 61edd4f1b5

* exclude spec files from coverage report

* add support for options in a command function (.get, .set, ...), add support for the SELECT command, implement a couple of commands, fix client socket reconnection strategy, add support for using replicas (RO) in cluster, and more..

* fix client.blPop test

* use which to find redis-server path

* change command options to work with Symbol rather then WeakSet

* implement more commands

* Add support for lua scripts in client & muilti, fix client socket initiator, implement simple cluster nodes discovery strategy

* replace `callbackify` with `legacyMode`

* add the SCAN command and client.scanIterator

* rename scanIterator

* init benchmark workflow

* fix benchmark workflow

* fix benchmark workflow

* fix benchmark workflow

* push coverage report to Coveralls

* fix Coveralls

* generator lcov (for Coveralls)

* fix .nycrc.json

* PubSub

* add support for all set commands (including sScanIterator)

* support pipeline

* fix KEEPTTL in SET

* remove console.log

* add HyperLogLog commands

* update README.md (thanks to @guyroyse)

* add support for most of the "keys commands"

* fix EXPIREAT.spec.ts

* add support for date in both EXPIREAT & EXPIRE

* add tests

* better cluster nodes discorvery strategy after MOVED error, add PubSub test

* fix PubSub UNSUBSCRIBE/PUNSUBSCRIBE without channel and/or listener

* fix PubSub

* add release-it to dev dependencies

* Release 4.0.0-next.0

* fix .npmignore

* Release 4.0.0-next.1

* fix links in README.md

* fix .npmignore

* Release 4.0.0-next.2

* add support for all sorted set commands

* add support for most stream commands

* add missing file from commit 53de279afe

* lots of todo commends

* make PubSub test more stable

* clean ZPOPMAX

* add support for lua scripts and modules in cluster, spawn cluster for tests, add some cluster tests, fix pubsub listener arguments

* GET.spec.ts

* add support for List commands, fix some Sorted Set commands, add some cluster commands, spawn cluster for testing, add support for command options in cluster, and more

* add missing file from commit faab94fab2

* clean ZRANK and ZREVRANK

* add XREAD and XREADGROUP commands

* remove unused files

* implement a couple of more commands, make cluster random iterator be per node (instead of per slot)

* Release 4.0.0-next.3

* app spec files to npmignore

* fix some code analyzers (LGTM, deepsource, codeclimate) issues

* fix CLUSTER_NODES, add some tests

* add HSCAN, clean some commands, add tests for generic transformers

* add missing files from 0feb35a1fb

* update README.md (thanks to @guyroyse)

* handle ASK errors, add some commands and tests

* Release 4.0.0-next.4

* replace "modern" with "v4"

* remove unused imports

* add all ACL subcommands, all MODULE subcommands, and some other commands

* remove 2 unused imports

* fix BITFIELD command

* fix XTRIM spec file

* clean code

* fix package.json types field

* better modules support, fix some bugs in legacy mode, add some tests

* remove unused function

* add test for hScanIterator

* change node mimimum version to 12 (latest LTS)

* update tsconfig.json to support node 12, run tests on Redis 5 & 6 and on all node live versions

* remove future node releases :P

* remove "lib" from ts compiler options

* Update tsconfig.json

* fix build

* run some tests only on supported redis versions, use coveralls parallel mode

* fix tests

* Do not use "timers/promises", fix "isRedisVersionGreaterThan"

* skip AbortController tests when not available

* use 'fs'.promises instead of 'fs/promises'

* add some missing commands

* run GETDEL tests only if the redis version is greater than 6.2

* implement some GEO commands, improve scan generic transformer, expose RPUSHX

* fix GEOSEARCH & GEOSEARCHSTORE

* use socket.setNoDelay and queueMicrotask to improve latency

* commands-queue.ts: String length / byte length counting issue (#1630)

* Update commands-queue.ts

Hopefully fixing #1628

* Reverted 2fa5ea6, and implemented test for byte length check

* Changed back to Buffer.byteLength, due to issue author input. Updated test to look for 4 bytes.

* Fixed. There were two places that length was calculated.

* Removed redundant string assignment

* add 2 bytes test as well

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* fix scripts in multi

* do not hide bugs in redis

* fix for e7bf09644b

* remove unused import

* implement WATCH command, fix ZRANGESTORE & GEOSEARCHSTORE tests

* update README.md

Co-authored-by: @GuyRoyse

* use typedoc to auto generate documentation

* run "npm install" before "npm run documentation"

* clean documentation workflow

* fix WATCH spec file

* increase "CLUSTER_NODE_TIMEOUT" to 5000ms to avoid "CLUSTERDOWN" errors in tests

* pull cluster state every 100 ms

* await meetPromises before pulling the cluster state

* enhance the way commanders (client/multi/cluster) get extended with modules and scripts

* add test for socket retry strategy

* implement more commands

* set GETEX minimum version to 6.2

* remove unused imports

* add support for multi in cluster

* upgrade dependencies

* Release 4.0.0-next.5

* remove unused imports

* improve benchmarking

* use the same Multi with duplicated clients

* exclude some files from the documentation, add some exports, clean code

* fix #1636 - handle null in multi.exec

* remove unused import

* add supoprt for tuples in HSET

* add FIRST_KEY_INDEX to HSET

* add a bunch of missing commands, fix MSET and HELLO, add some tests

* add FIRST_KEY_INDEX to MSET and MSETNX

* upgrade actions

* fix coverallsapp/github-action version

* Update documentation.yml

* Update documentation.yml

* clean code

* remove unused imports

* use "npm ci" instead of "npm install"

* fix `self` binding on client modules, use connection pool for `duplicateConnection`

* add client.executeIsolated, rename "duplicateConnection" to "isolated", update README.md (thanks to @GuyRoyse and @SimonPrickett)

* update README (thanks to @GuyRoyse), add some tests

* try to fix "cluster is down" errors in tests

* try to fix "cluster is down" errors in tests

* upgrade dependencies

* update package-lock

* Release 4.0.0-next.6

* fix #1636 - fix WatchError

* fix for f1bf0beebf - remove .only from multi tests

* Release 4.0.0-next.7

* update README and other markdown files

Co-authored-by: @GuyRoyse & @SimonPrickett

* Doc updates. (#1640)

* update docs, upgrade dependencies

* fix README

* Release 4.0.0-rc.0

* Update README.md

* update docs, add `connectTimeout` options, fix tls

Co-authored-by: Guy Royse <guy@guyroyse.com>

* npm update, "fix" some tests, clean code

* fix AssertionError import

* fix #1642 - fix XREAD, XREADGROUP and XTRIM

* fix #1644 - add the QUIT command

* add socket.noDelay and socket.keepAlive configurations

* Update README.md (#1645)

* Update README.md

Fixed issue with how connection string was specified.
Now you can have user@host without having to specify a password, which just makes more sense

* Update client-configuration.md as well

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* update socket.reconnectStrategy description

* fix borken link in v3-to-v4.md

* increase test coverage, fix bug in cluster redirection strategy, implement CLIENT_ID, remove unused EXEC command

Co-authored-by: Nova <novaw@warrenservices.co.uk>
Co-authored-by: Simon Prickett <simon@crudworks.org>
Co-authored-by: Guy Royse <guy@guyroyse.com>
2021-09-02 10:04:48 -04:00

563 lines
17 KiB
TypeScript

import { strict as assert, AssertionError } from 'assert';
import { once } from 'events';
import { itWithClient, TEST_REDIS_SERVERS, TestRedisServers, waitTillBeenCalled, isRedisVersionGreaterThan } from './test-utils';
import RedisClient from './client';
import { AbortError, ClientClosedError, ConnectionTimeoutError, WatchError } from './errors';
import { defineScript } from './lua-script';
import { spy } from 'sinon';
export const SQUARE_SCRIPT = defineScript({
NUMBER_OF_KEYS: 0,
SCRIPT: 'return ARGV[1] * ARGV[1];',
transformArguments(number: number): Array<string> {
return [number.toString()];
},
transformReply(reply: number): number {
return reply;
}
});
describe('Client', () => {
describe('authentication', () => {
itWithClient(TestRedisServers.PASSWORD, 'Client should be authenticated', async client => {
assert.equal(
await client.ping(),
'PONG'
);
});
it('should not retry connecting if failed due to wrong auth', async () => {
const client = RedisClient.create({
socket: {
...TEST_REDIS_SERVERS[TestRedisServers.PASSWORD],
password: 'wrongpassword'
}
});
await assert.rejects(
client.connect(),
{
message: isRedisVersionGreaterThan([6]) ?
'WRONGPASS invalid username-password pair or user is disabled.' :
'ERR invalid password'
}
);
assert.equal(client.isOpen, false);
});
});
describe('legacyMode', () => {
const client = RedisClient.create({
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN],
scripts: {
square: SQUARE_SCRIPT
},
legacyMode: true
});
before(() => client.connect());
afterEach(() => client.v4.flushAll());
after(() => client.disconnect());
it('client.sendCommand should call the callback', done => {
(client as any).sendCommand('PING', (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.equal(reply, 'PONG');
done();
} catch (err) {
done(err);
}
});
});
it('client.sendCommand should work without callback', async () => {
(client as any).sendCommand('PING');
await client.v4.ping(); // make sure the first command was replied
});
it('client.v4.sendCommand should return a promise', async () => {
assert.equal(
await client.v4.sendCommand(['PING']),
'PONG'
);
});
it('client.{command} should accept vardict arguments', done => {
(client as any).set('a', 'b', (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.equal(reply, 'OK');
done();
} catch (err) {
done(err);
}
});
});
it('client.{command} should accept arguments array', done => {
(client as any).set(['a', 'b'], (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.equal(reply, 'OK');
done();
} catch (err) {
done(err);
}
});
});
it('client.{command} should accept mix of strings and array of strings', done => {
(client as any).set(['a'], 'b', ['XX'], (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.equal(reply, null);
done();
} catch (err) {
done(err);
}
});
});
it('client.multi.ping.exec should call the callback', done => {
(client as any).multi()
.ping()
.exec((err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.deepEqual(reply, ['PONG']);
done();
} catch (err) {
done(err);
}
});
});
it('client.multi.ping.exec should work without callback', async () => {
(client as any).multi()
.ping()
.exec();
await client.v4.ping(); // make sure the first command was replied
});
it('client.multi.ping.v4.ping.v4.exec should return a promise', async () => {
assert.deepEqual(
await ((client as any).multi()
.ping()
.v4.ping()
.v4.exec()),
['PONG', 'PONG']
);
});
it('client.{script} should return a promise', async () => {
assert.equal(await client.square(2), 4);
});
});
describe('events', () => {
it('connect, ready, end', async () => {
const client = RedisClient.create({
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
});
await Promise.all([
client.connect(),
once(client, 'connect'),
once(client, 'ready')
]);
await Promise.all([
client.disconnect(),
once(client, 'end')
]);
});
});
describe('sendCommand', () => {
itWithClient(TestRedisServers.OPEN, 'PING', async client => {
assert.equal(await client.sendCommand(['PING']), 'PONG');
});
describe('AbortController', () => {
before(function () {
if (!global.AbortController) {
this.skip();
}
});
itWithClient(TestRedisServers.OPEN, 'success', async client => {
await client.sendCommand(['PING'], {
signal: new AbortController().signal
});
});
itWithClient(TestRedisServers.OPEN, 'AbortError', client => {
const controller = new AbortController();
controller.abort();
return assert.rejects(
client.sendCommand(['PING'], {
signal: controller.signal
}),
AbortError
);
});
});
});
describe('multi', () => {
itWithClient(TestRedisServers.OPEN, 'simple', async client => {
assert.deepEqual(
await client.multi()
.ping()
.set('key', 'value')
.get('key')
.exec(),
['PONG', 'OK', 'value']
);
});
itWithClient(TestRedisServers.OPEN, 'should reject the whole chain on error', client => {
client.on('error', () => {
// ignore errors
});
return assert.rejects(
client.multi()
.ping()
.addCommand(['DEBUG', 'RESTART'])
.ping()
.exec()
);
});
it('with script', async () => {
const client = RedisClient.create({
scripts: {
square: SQUARE_SCRIPT
}
});
await client.connect();
try {
assert.deepEqual(
await client.multi()
.square(2)
.exec(),
[4]
);
} finally {
await client.disconnect();
}
});
itWithClient(TestRedisServers.OPEN, 'WatchError', async client => {
await client.watch('key');
await client.set(
RedisClient.commandOptions({
isolated: true
}),
'key',
'1'
);
await assert.rejects(
client.multi()
.decr('key')
.exec(),
WatchError
);
});
});
it('scripts', async () => {
const client = RedisClient.create({
scripts: {
square: SQUARE_SCRIPT
}
});
await client.connect();
try {
assert.equal(
await client.square(2),
4
);
} finally {
await client.disconnect();
}
});
it('modules', async () => {
const client = RedisClient.create({
modules: {
module: {
echo: {
transformArguments(message: string): Array<string> {
return ['ECHO', message];
},
transformReply(reply: string): string {
return reply;
}
}
}
}
});
await client.connect();
try {
assert.equal(
await client.module.echo('message'),
'message'
);
} finally {
await client.disconnect();
}
});
itWithClient(TestRedisServers.OPEN, 'executeIsolated', async client => {
await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
assert.equal(
await client.executeIsolated(isolatedClient =>
isolatedClient.sendCommand(['CLIENT', 'GETNAME'])
),
null
);
});
itWithClient(TestRedisServers.OPEN, 'should reconnect after DEBUG RESTART', async client => {
client.on('error', () => {
// ignore errors
});
await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
await assert.rejects(client.sendCommand(['DEBUG', 'RESTART']));
assert.ok(await client.sendCommand(['CLIENT', 'GETNAME']) === null);
});
itWithClient(TestRedisServers.OPEN, 'should SELECT db after reconnection', async client => {
client.on('error', () => {
// ignore errors
});
await client.select(1);
await assert.rejects(client.sendCommand(['DEBUG', 'RESTART']));
assert.equal(
(await client.clientInfo()).db,
1
);
}, {
// because of CLIENT INFO
minimumRedisVersion: [6, 2]
});
itWithClient(TestRedisServers.OPEN, 'scanIterator', async client => {
const promises = [],
keys = new Set();
for (let i = 0; i < 100; i++) {
const key = i.toString();
keys.add(key);
promises.push(client.set(key, ''));
}
await Promise.all(promises);
const results = new Set();
for await (const key of client.scanIterator()) {
results.add(key);
}
assert.deepEqual(keys, results);
});
itWithClient(TestRedisServers.OPEN, 'hScanIterator', async client => {
const hash: Record<string, string> = {};
for (let i = 0; i < 100; i++) {
hash[i.toString()] = i.toString();
}
await client.hSet('key', hash);
const results: Record<string, string> = {};
for await (const { field, value } of client.hScanIterator('key')) {
results[field] = value;
}
assert.deepEqual(hash, results);
});
itWithClient(TestRedisServers.OPEN, 'sScanIterator', async client => {
const members = new Set<string>();
for (let i = 0; i < 100; i++) {
members.add(i.toString());
}
await client.sAdd('key', Array.from(members));
const results = new Set<string>();
for await (const key of client.sScanIterator('key')) {
results.add(key);
}
assert.deepEqual(members, results);
});
itWithClient(TestRedisServers.OPEN, 'zScanIterator', async client => {
const members = [];
for (let i = 0; i < 100; i++) {
members.push({
score: 1,
value: i.toString()
});
}
await client.zAdd('key', members);
const map = new Map();
for await (const member of client.zScanIterator('key')) {
map.set(member.value, member.score);
}
type MemberTuple = [string, number];
function sort(a: MemberTuple, b: MemberTuple) {
return Number(b[0]) - Number(a[0]);
}
assert.deepEqual(
[...map.entries()].sort(sort),
members.map<MemberTuple>(member => [member.value, member.score]).sort(sort)
);
});
itWithClient(TestRedisServers.OPEN, 'PubSub', async publisher => {
const subscriber = publisher.duplicate();
await subscriber.connect();
try {
const channelListener1 = spy(),
channelListener2 = spy(),
patternListener = spy();
await Promise.all([
subscriber.subscribe('channel', channelListener1),
subscriber.subscribe('channel', channelListener2),
subscriber.pSubscribe('channel*', patternListener)
]);
await Promise.all([
waitTillBeenCalled(channelListener1),
waitTillBeenCalled(channelListener2),
waitTillBeenCalled(patternListener),
publisher.publish('channel', 'message')
]);
assert.ok(channelListener1.calledOnceWithExactly('message', 'channel'));
assert.ok(channelListener2.calledOnceWithExactly('message', 'channel'));
assert.ok(patternListener.calledOnceWithExactly('message', 'channel'));
await subscriber.unsubscribe('channel', channelListener1);
await Promise.all([
waitTillBeenCalled(channelListener2),
waitTillBeenCalled(patternListener),
publisher.publish('channel', 'message')
]);
assert.ok(channelListener1.calledOnce);
assert.ok(channelListener2.calledTwice);
assert.ok(channelListener2.secondCall.calledWithExactly('message', 'channel'));
assert.ok(patternListener.calledTwice);
assert.ok(patternListener.secondCall.calledWithExactly('message', 'channel'));
await subscriber.unsubscribe('channel');
await Promise.all([
waitTillBeenCalled(patternListener),
publisher.publish('channel', 'message')
]);
assert.ok(channelListener1.calledOnce);
assert.ok(channelListener2.calledTwice);
assert.ok(patternListener.calledThrice);
assert.ok(patternListener.thirdCall.calledWithExactly('message', 'channel'));
await subscriber.pUnsubscribe();
await publisher.publish('channel', 'message');
assert.ok(channelListener1.calledOnce);
assert.ok(channelListener2.calledTwice);
assert.ok(patternListener.calledThrice);
} finally {
await subscriber.disconnect();
}
});
it('ConnectionTimeoutError', async () => {
const client = RedisClient.create({
socket: {
...TEST_REDIS_SERVERS[TestRedisServers.OPEN],
connectTimeout: 1
}
});
try {
const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
start = process.hrtime.bigint();
// block the event loop for 1ms, to make sure the connection will timeout
while (process.hrtime.bigint() - start < 1_000_000) {}
await promise;
} catch (err) {
if (err instanceof AssertionError) {
await client.disconnect();
}
throw err;
}
});
it('client.quit', async () => {
const client = RedisClient.create({
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
});
await client.connect();
try {
const quitPromise = client.quit();
assert.equal(client.isOpen, false);
await Promise.all([
quitPromise,
assert.rejects(client.ping(), ClientClosedError)
]);
} finally {
if (client.isOpen) {
await client.disconnect();
}
}
});
});