Skip to content

Commit 4ae14bb

Browse files
authored
Add Redis transparent proxy test utilities (#3019)
1 parent 0541b32 commit 4ae14bb

File tree

5 files changed

+509
-0
lines changed

5 files changed

+509
-0
lines changed

packages/client/lib/test-utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ export const GLOBAL = {
9393
password: 'password'
9494
}
9595
},
96+
OPEN_RESP_3: {
97+
serverArguments: [...DEBUG_MODE_ARGS],
98+
clientOptions: {
99+
RESP: 3,
100+
}
101+
},
96102
ASYNC_BASIC_AUTH: {
97103
serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS],
98104
clientOptions: {

packages/test-utils/lib/index.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { hideBin } from 'yargs/helpers';
2626
import * as fs from 'node:fs';
2727
import * as os from 'node:os';
2828
import * as path from 'node:path';
29+
import { RedisProxy, getFreePortNumber } from './redis-proxy';
2930

3031
interface TestUtilsConfig {
3132
/**
@@ -296,7 +297,44 @@ export default class TestUtils {
296297
}
297298
});
298299
}
300+
testWithProxiedClient(
301+
title: string,
302+
fn: (proxiedClient: RedisClientType<any, any, any, any, any>, proxy: RedisProxy) => unknown,
303+
options: ClientTestOptions<any, any, any, any, any>
304+
) {
305+
306+
this.testWithClient(title, async (client) => {
307+
const freePort = await getFreePortNumber()
308+
const socketOptions = client?.options?.socket;
309+
const proxy = new RedisProxy({
310+
listenHost: '127.0.0.1',
311+
listenPort: freePort,
312+
//@ts-ignore
313+
targetPort: socketOptions.port,
314+
//@ts-ignore
315+
targetHost: socketOptions.host,
316+
enableLogging: true
317+
});
318+
319+
320+
await proxy.start();
321+
const proxyClient = client.duplicate({
322+
socket: {
323+
port: proxy.config.listenPort,
324+
host: proxy.config.listenHost
325+
},
326+
});
299327

328+
await proxyClient.connect();
329+
330+
try {
331+
await fn(proxyClient, proxy);
332+
} finally {
333+
await proxyClient.destroy();
334+
await proxy.stop()
335+
}
336+
}, options);
337+
}
300338
testWithClientSentinel<
301339
M extends RedisModules = {},
302340
F extends RedisFunctions = {},
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { strict as assert } from 'node:assert';
2+
import { Buffer } from 'node:buffer';
3+
import { testUtils, GLOBAL } from './test-utils';
4+
import { RedisProxy } from './redis-proxy';
5+
import type { RedisClientType } from '@redis/client/lib/client/index.js';
6+
7+
describe('RedisSocketProxy', function () {
8+
testUtils.testWithClient('basic proxy functionality', async (client: RedisClientType<any, any, any, any, any>) => {
9+
const socketOptions = client?.options?.socket;
10+
//@ts-ignore
11+
assert(socketOptions?.port, 'Test requires a TCP connection to Redis');
12+
13+
const proxyPort = 50000 + Math.floor(Math.random() * 10000);
14+
const proxy = new RedisProxy({
15+
listenHost: '127.0.0.1',
16+
listenPort: proxyPort,
17+
//@ts-ignore
18+
targetPort: socketOptions.port,
19+
//@ts-ignore
20+
targetHost: socketOptions.host || '127.0.0.1',
21+
enableLogging: true
22+
});
23+
24+
const proxyEvents = {
25+
connections: [] as any[],
26+
dataTransfers: [] as any[]
27+
};
28+
29+
proxy.on('connection', (connectionInfo) => {
30+
proxyEvents.connections.push(connectionInfo);
31+
});
32+
33+
proxy.on('data', (connectionId, direction, data) => {
34+
proxyEvents.dataTransfers.push({ connectionId, direction, dataLength: data.length });
35+
});
36+
37+
try {
38+
await proxy.start();
39+
40+
const proxyClient = client.duplicate({
41+
socket: {
42+
port: proxyPort,
43+
host: '127.0.0.1'
44+
},
45+
});
46+
47+
await proxyClient.connect();
48+
49+
const stats = proxy.getStats();
50+
assert.equal(stats.activeConnections, 1, 'Should have one active connection');
51+
assert.equal(proxyEvents.connections.length, 1, 'Should have recorded one connection event');
52+
53+
const pingResult = await proxyClient.ping();
54+
assert.equal(pingResult, 'PONG', 'Client should be able to communicate with Redis through the proxy');
55+
56+
const clientToServerTransfers = proxyEvents.dataTransfers.filter(t => t.direction === 'client->server');
57+
const serverToClientTransfers = proxyEvents.dataTransfers.filter(t => t.direction === 'server->client');
58+
59+
assert(clientToServerTransfers.length > 0, 'Should have client->server data transfers');
60+
assert(serverToClientTransfers.length > 0, 'Should have server->client data transfers');
61+
62+
const testKey = `test:proxy:${Date.now()}`;
63+
const testValue = 'proxy-test-value';
64+
65+
await proxyClient.set(testKey, testValue);
66+
const retrievedValue = await proxyClient.get(testKey);
67+
assert.equal(retrievedValue, testValue, 'Should be able to set and get values through proxy');
68+
69+
proxyClient.destroy();
70+
71+
72+
} finally {
73+
await proxy.stop();
74+
}
75+
}, GLOBAL.SERVERS.OPEN_RESP_3);
76+
77+
testUtils.testWithProxiedClient('custom message injection via proxy client',
78+
async (proxiedClient: RedisClientType<any, any, any, any, any>, proxy: RedisProxy) => {
79+
const customMessageTransfers: any[] = [];
80+
81+
proxy.on('data', (connectionId, direction, data) => {
82+
if (direction === 'server->client') {
83+
customMessageTransfers.push({ connectionId, dataLength: data.length, data });
84+
}
85+
});
86+
87+
88+
const stats = proxy.getStats();
89+
assert.equal(stats.activeConnections, 1, 'Should have one active connection');
90+
91+
// Send a resp3 push
92+
const customMessage = Buffer.from('>4\r\n$6\r\nMOVING\r\n:1\r\n:2\r\n$6\r\nhost:3\r\n');
93+
94+
const sendResults = proxy.sendToAllClients(customMessage);
95+
assert.equal(sendResults.length, 1, 'Should send to one client');
96+
assert.equal(sendResults[0].success, true, 'Custom message send should succeed');
97+
98+
99+
const customMessageFound = customMessageTransfers.find(transfer =>
100+
transfer.dataLength === customMessage.length
101+
);
102+
assert(customMessageFound, 'Should have recorded the custom message transfer');
103+
104+
assert.equal(customMessageFound.dataLength, customMessage.length,
105+
'Custom message length should match');
106+
107+
const pingResult = await proxiedClient.ping();
108+
assert.equal(pingResult, 'PONG', 'Client should be able to communicate with Redis through the proxy');
109+
110+
}, GLOBAL.SERVERS.OPEN_RESP_3)
111+
});

0 commit comments

Comments
 (0)