Skip to content

Commit a6aa6bd

Browse files
committed
add ProxyController for easier proxy comms
1 parent 2f39426 commit a6aa6bd

File tree

3 files changed

+114
-4
lines changed

3 files changed

+114
-4
lines changed

packages/test-utils/lib/dockers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const RUNNING_SERVERS = new Map<Array<string>, ReturnType<typeof spawnRedisServe
119119

120120
export interface ProxiedRedisServerDocker {
121121
ports: number[],
122+
apiPort: number,
122123
dockerId: string
123124
}
124125

@@ -150,12 +151,14 @@ export async function spawnProxiedRedisServerDocker(
150151
for (let i = 0; i < config.nOfProxies; i++) {
151152
ports.push((await portIterator.next()).value);
152153
}
154+
const apiPort = (await portIterator.next()).value;
153155

154156
const dockerArgs =[
155157
"run",
156158
"-d",
157159
"--network", "host",
158160
"-e", `LISTEN_PORT=${ports.join(',')}`,
161+
"-e", `API_PORT=${apiPort}`,
159162
"-e", "TIEOUT=0",
160163
"-e", `DEFAULT_INTERCEPTORS=${config.defaultInterceptors.join(',')}`,
161164
"-e", "ENABLE_LOGGING=true",
@@ -176,6 +179,7 @@ export async function spawnProxiedRedisServerDocker(
176179

177180
return {
178181
ports,
182+
apiPort,
179183
dockerId: stdout.trim(),
180184
};
181185
}

packages/test-utils/lib/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import * as fs from 'node:fs';
2727
import * as os from 'node:os';
2828
import * as path from 'node:path';
2929
import { RedisProxy, getFreePortNumber } from './proxy/redis-proxy';
30+
import ProxyController from './proxy/proxy-controller';
3031

3132

3233
interface TestUtilsConfig {
@@ -308,7 +309,7 @@ export default class TestUtils {
308309
TYPE_MAPPING extends TypeMapping = {}
309310
>(
310311
title: string,
311-
fn: (proxiedClusterClient: RedisClusterType<M, F, S, RESP, TYPE_MAPPING>, proxyUrl: string) => unknown,
312+
fn: (proxiedClusterClient: RedisClusterType<M, F, S, RESP, TYPE_MAPPING>, proxyController: ProxyController) => unknown,
312313
options: Omit<ClusterTestOptions<M, F, S, RESP, TYPE_MAPPING>, 'numberOfReplicas' | 'minimumDockerVersion' | 'serverArguments'>
313314
) {
314315
let spawnPromise: ReturnType<typeof spawnProxiedRedisServer>;
@@ -319,7 +320,7 @@ export default class TestUtils {
319320

320321
it(title, async function () {
321322
if (!spawnPromise) return this.skip();
322-
const { ports } = await spawnPromise;
323+
const { ports, apiPort } = await spawnPromise;
323324

324325
const cluster = createCluster({
325326
rootNodes: ports.map(port => ({
@@ -331,15 +332,17 @@ export default class TestUtils {
331332
...options.clusterConfiguration
332333
});
333334

335+
const proxyController = new ProxyController(`http://localhost:${apiPort}`)
336+
334337
if(options.disableClusterSetup) {
335-
return fn(cluster, 'hh');
338+
return fn(cluster, proxyController);
336339
}
337340

338341
await cluster.connect();
339342

340343
try {
341344
await TestUtils.#clusterFlushAll(cluster);
342-
await fn(cluster, 'hi');
345+
await fn(cluster, proxyController);
343346
} finally {
344347
await TestUtils.#clusterFlushAll(cluster);
345348
cluster.destroy();
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
export type ProxyStats = {
2+
totalConnections: number;
3+
activeConnections: number;
4+
totalBytesReceived: number;
5+
totalBytesSent: number;
6+
totalCommandsReceived: number;
7+
totalCommandsSent: number;
8+
};
9+
10+
export type SendResult = {
11+
success: boolean;
12+
connectionId: string;
13+
error?: string;
14+
};
15+
16+
export type ProxyConfig = {
17+
listenPort: number;
18+
listenHost?: string;
19+
targetHost: string;
20+
targetPort: number;
21+
timeout?: number;
22+
enableLogging?: boolean;
23+
};
24+
25+
export default class ProxyController {
26+
constructor(private url: string) { }
27+
28+
private fetchJson(path: string, options?: RequestInit): Promise<unknown> {
29+
return fetch(`${this.url}${path}`, options).then(res => res.json());
30+
}
31+
32+
getStats(): Promise<Record<string, ProxyStats>> {
33+
return this.fetchJson('/stats') as Promise<Record<string, ProxyStats>>;
34+
}
35+
36+
getConnections(): Promise<Record<string, readonly string[]>> {
37+
return this.fetchJson('/connections') as Promise<Record<string, readonly string[]>>;
38+
}
39+
40+
getNodes(): Promise<{ ids: string[] }> {
41+
return this.fetchJson('/nodes') as Promise<{ ids: string[] }>;
42+
}
43+
44+
createNode(config: Partial<ProxyConfig>): Promise<{ success: boolean; cfg: ProxyConfig }> {
45+
return this.fetchJson('/nodes', {
46+
method: 'POST',
47+
headers: { 'Content-Type': 'application/json' },
48+
body: JSON.stringify(config)
49+
}) as Promise<{ success: boolean; cfg: ProxyConfig }>;
50+
}
51+
52+
deleteNode(nodeId: string): Promise<{ success: boolean }> {
53+
return this.fetchJson(`/nodes/${nodeId}`, {
54+
method: 'DELETE'
55+
}) as Promise<{ success: boolean }>;
56+
}
57+
58+
sendToClient(connectionId: string, data: string, encoding: 'base64' | 'raw' = 'base64'): Promise<SendResult> {
59+
return this.fetchJson(`/send-to-client/${connectionId}?encoding=${encoding}`, {
60+
method: 'POST',
61+
headers: { 'Content-Type': 'text/plain' },
62+
body: data
63+
}) as Promise<SendResult>;
64+
}
65+
66+
sendToClients(connectionIds: string[], data: string, encoding: 'base64' | 'raw' = 'base64'): Promise<{ results: SendResult[] }> {
67+
return this.fetchJson(`/send-to-clients?connectionIds=${connectionIds.join(',')}&encoding=${encoding}`, {
68+
method: 'POST',
69+
headers: { 'Content-Type': 'text/plain' },
70+
body: data
71+
}) as Promise<{ results: SendResult[] }>;
72+
}
73+
74+
sendToAllClients(data: string, encoding: 'base64' | 'raw' = 'base64'): Promise<{ results: SendResult[] }> {
75+
return this.fetchJson(`/send-to-all-clients?encoding=${encoding}`, {
76+
method: 'POST',
77+
headers: { 'Content-Type': 'text/plain' },
78+
body: data
79+
}) as Promise<{ results: SendResult[] }>;
80+
}
81+
82+
closeConnection(connectionId: string): Promise<{ success: boolean; connectionId: string }> {
83+
return this.fetchJson(`/connections/${connectionId}`, {
84+
method: 'DELETE'
85+
}) as Promise<{ success: boolean; connectionId: string }>;
86+
}
87+
88+
createScenario(responses: string[], encoding: 'base64' | 'raw' = 'base64'): Promise<{ success: boolean; totalResponses: number }> {
89+
return this.fetchJson('/scenarios', {
90+
method: 'POST',
91+
headers: { 'Content-Type': 'application/json' },
92+
body: JSON.stringify({ responses, encoding })
93+
}) as Promise<{ success: boolean; totalResponses: number }>;
94+
}
95+
96+
addInterceptor(name: string, match: string, response: string, encoding: 'base64' | 'raw' = 'base64'): Promise<{ success: boolean; name: string }> {
97+
return this.fetchJson('/interceptors', {
98+
method: 'POST',
99+
headers: { 'Content-Type': 'application/json' },
100+
body: JSON.stringify({ name, match, response, encoding })
101+
}) as Promise<{ success: boolean; name: string }>;
102+
}
103+
}

0 commit comments

Comments
 (0)