@yousolution/node-red-contrib-you-tunnel-websocket 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +45 -0
- package/CHANGELOG.md +109 -0
- package/README.md +83 -4
- package/STABILITY_CONTRACT.md +299 -0
- package/nodes/wsTunnel.html +126 -4
- package/nodes/wsTunnel.js +143 -68
- package/package.json +4 -12
- package/test/ki-001-host-path-passthrough.test.js +187 -0
- package/test/ki-002-tunnel-id-header-default.test.js +60 -0
- package/test/ki-003-debug-log-level.test.js +180 -0
- package/test/ki-004-console-error-usage.test.js +144 -0
- package/test/m-3-concurrent-deployment.test.js +227 -0
- package/test/output-events.test.js +241 -0
- package/test/rwt-001-port-exclusivity.test.js +171 -0
- package/test/rwt-001-port-release.test.js +58 -0
- package/test/rwt-002-shutdown-completion.test.js +226 -0
- package/test/rwt-003-runtime-editor-compatibility.test.js +113 -0
- package/test/rwt-004-persisted-config-compatibility.test.js +140 -0
- package/test/start-failure-path.test.js +212 -0
- package/test/wstunnel-command.test.js +203 -0
- package/test/wstunnel-metrics-input.test.js +234 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wstunnel server — Metrics Input (Regression Test)
|
|
3
|
+
*
|
|
4
|
+
* Tests that the wstunnel server node emits a metrics snapshot on its output
|
|
5
|
+
* when any message is received on its input.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const WS_TUNNEL_PATH = path.join(__dirname, '..', 'nodes', 'wsTunnel.js');
|
|
11
|
+
|
|
12
|
+
const PORT = 1880;
|
|
13
|
+
|
|
14
|
+
const mockSnapshot = {
|
|
15
|
+
label: 'tunnel',
|
|
16
|
+
ts: new Date().toISOString(),
|
|
17
|
+
active_tunnels: 2,
|
|
18
|
+
active_tunnel_ids: ['tunnel-001', 'tunnel-002'],
|
|
19
|
+
active_streams: 5,
|
|
20
|
+
bytes_in_total: 1024000,
|
|
21
|
+
bytes_out_total: 512000,
|
|
22
|
+
backpressure_events_total: 3,
|
|
23
|
+
buffered_bytes_total: 4096,
|
|
24
|
+
buffered_bytes_per_tunnel: { 'tunnel-001': 2048, 'tunnel-002': 2048 },
|
|
25
|
+
frame_too_large_total: 0,
|
|
26
|
+
tunnel_disconnect_total: 1,
|
|
27
|
+
heartbeat_timeout_total: 0,
|
|
28
|
+
stream_stall_cleanup_total: 0,
|
|
29
|
+
event_loop_lag_ms: { p50: 1.2, p99: 8.5 },
|
|
30
|
+
tunnels_detail: {
|
|
31
|
+
'tunnel-001': {
|
|
32
|
+
connectedAt: 1725974400000,
|
|
33
|
+
remoteAddress: '192.168.1.10',
|
|
34
|
+
streamCount: 3,
|
|
35
|
+
peakStreamCount: 5,
|
|
36
|
+
bytesIn: 600000,
|
|
37
|
+
bytesOut: 300000,
|
|
38
|
+
agentVersion: '1.0.0',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const mockStopWebSocketServer = jest.fn(() => Promise.resolve());
|
|
44
|
+
const mockStartWebSocketServer = jest.fn((opts) => {
|
|
45
|
+
return Promise.resolve({
|
|
46
|
+
[opts.port]: {
|
|
47
|
+
webSocketServer: {
|
|
48
|
+
on: jest.fn(),
|
|
49
|
+
clients: new Set(),
|
|
50
|
+
},
|
|
51
|
+
websocketTunnels: {},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const mockGetMetrics = jest.fn(() => ({
|
|
57
|
+
snapshot: jest.fn(() => mockSnapshot),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const mockLogger = {
|
|
61
|
+
info: jest.fn(),
|
|
62
|
+
warn: jest.fn(),
|
|
63
|
+
error: jest.fn(),
|
|
64
|
+
debug: jest.fn(),
|
|
65
|
+
trace: jest.fn(),
|
|
66
|
+
};
|
|
67
|
+
const mockSetLogContext = jest.fn();
|
|
68
|
+
const mockSetLogLevel = jest.fn();
|
|
69
|
+
|
|
70
|
+
function createMockRED() {
|
|
71
|
+
const registeredTypes = {};
|
|
72
|
+
return {
|
|
73
|
+
nodes: {
|
|
74
|
+
createNode: jest.fn((node) => {
|
|
75
|
+
if (!node.id) node.id = 'node-' + Math.random().toString(36).slice(2, 9);
|
|
76
|
+
node.status = jest.fn();
|
|
77
|
+
node.send = jest.fn();
|
|
78
|
+
node.on = jest.fn((event, handler) => {
|
|
79
|
+
node._handlers = node._handlers || {};
|
|
80
|
+
node._handlers[event] = handler;
|
|
81
|
+
});
|
|
82
|
+
}),
|
|
83
|
+
registerType: jest.fn((type, constructor) => {
|
|
84
|
+
registeredTypes[type] = constructor;
|
|
85
|
+
}),
|
|
86
|
+
getNode: jest.fn(() => ({
|
|
87
|
+
options: {
|
|
88
|
+
name: 'test-tunnel',
|
|
89
|
+
host: 'localhost',
|
|
90
|
+
port: PORT,
|
|
91
|
+
path: '',
|
|
92
|
+
tunnelIdHeaderName: 'x-tunnel-id',
|
|
93
|
+
logLevel: 'info',
|
|
94
|
+
},
|
|
95
|
+
})),
|
|
96
|
+
},
|
|
97
|
+
_registeredTypes: registeredTypes,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe('wstunnel server — metrics input', () => {
|
|
102
|
+
beforeAll(() => {
|
|
103
|
+
const serverModule = require('@remotelinker/reverse-ws-tunnel/server');
|
|
104
|
+
serverModule.stopWebSocketServer = mockStopWebSocketServer;
|
|
105
|
+
serverModule.startWebSocketServer = mockStartWebSocketServer;
|
|
106
|
+
|
|
107
|
+
const utilsModule = require('@remotelinker/reverse-ws-tunnel/utils');
|
|
108
|
+
utilsModule.logger = mockLogger;
|
|
109
|
+
utilsModule.setLogContext = mockSetLogContext;
|
|
110
|
+
utilsModule.setLogLevel = mockSetLogLevel;
|
|
111
|
+
utilsModule.getMetrics = mockGetMetrics;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
mockStopWebSocketServer.mockClear();
|
|
116
|
+
mockStartWebSocketServer.mockClear();
|
|
117
|
+
mockGetMetrics.mockClear();
|
|
118
|
+
mockGetMetrics.mockReturnValue({ snapshot: jest.fn(() => mockSnapshot) });
|
|
119
|
+
mockLogger.info.mockClear();
|
|
120
|
+
mockLogger.error.mockClear();
|
|
121
|
+
mockLogger.debug.mockClear();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('emits metrics snapshot when input message is received', async () => {
|
|
125
|
+
const RED = createMockRED();
|
|
126
|
+
const tunnelModule = require(WS_TUNNEL_PATH);
|
|
127
|
+
tunnelModule(RED);
|
|
128
|
+
|
|
129
|
+
const WSTunnelServer = RED._registeredTypes['wstunnel server'];
|
|
130
|
+
const node = { id: 'testNode' };
|
|
131
|
+
WSTunnelServer.call(node, { wstunnel: 'test-config-id', name: 'test-server' });
|
|
132
|
+
|
|
133
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
134
|
+
|
|
135
|
+
const inputHandler = node._handlers['input'];
|
|
136
|
+
expect(inputHandler).toBeDefined();
|
|
137
|
+
|
|
138
|
+
const msg = { payload: {} };
|
|
139
|
+
inputHandler.call(node, msg);
|
|
140
|
+
|
|
141
|
+
expect(mockGetMetrics).toHaveBeenCalled();
|
|
142
|
+
expect(node.send).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(node.send).toHaveBeenCalledWith(msg);
|
|
144
|
+
expect(msg.event).toBe('metrics');
|
|
145
|
+
expect(msg.serverPort).toBe(PORT);
|
|
146
|
+
expect(msg.payload).toEqual(mockSnapshot);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('snapshot contains all expected fields', async () => {
|
|
150
|
+
const RED = createMockRED();
|
|
151
|
+
const tunnelModule = require(WS_TUNNEL_PATH);
|
|
152
|
+
tunnelModule(RED);
|
|
153
|
+
|
|
154
|
+
const WSTunnelServer = RED._registeredTypes['wstunnel server'];
|
|
155
|
+
const node = { id: 'testNode' };
|
|
156
|
+
WSTunnelServer.call(node, { wstunnel: 'test-config-id', name: 'test-server' });
|
|
157
|
+
|
|
158
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
159
|
+
|
|
160
|
+
const inputHandler = node._handlers['input'];
|
|
161
|
+
const msg = {};
|
|
162
|
+
inputHandler.call(node, msg);
|
|
163
|
+
|
|
164
|
+
const snapshot = msg.payload;
|
|
165
|
+
expect(snapshot).toHaveProperty('active_tunnels');
|
|
166
|
+
expect(snapshot).toHaveProperty('active_tunnel_ids');
|
|
167
|
+
expect(snapshot).toHaveProperty('active_streams');
|
|
168
|
+
expect(snapshot).toHaveProperty('bytes_in_total');
|
|
169
|
+
expect(snapshot).toHaveProperty('bytes_out_total');
|
|
170
|
+
expect(snapshot).toHaveProperty('event_loop_lag_ms');
|
|
171
|
+
expect(snapshot).toHaveProperty('tunnels_detail');
|
|
172
|
+
expect(snapshot).toHaveProperty('ts');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('returns zeroed snapshot when no tunnels are active', async () => {
|
|
176
|
+
const zeroSnapshot = {
|
|
177
|
+
label: 'tunnel',
|
|
178
|
+
ts: new Date().toISOString(),
|
|
179
|
+
active_tunnels: 0,
|
|
180
|
+
active_tunnel_ids: [],
|
|
181
|
+
active_streams: 0,
|
|
182
|
+
bytes_in_total: 0,
|
|
183
|
+
bytes_out_total: 0,
|
|
184
|
+
backpressure_events_total: 0,
|
|
185
|
+
buffered_bytes_total: 0,
|
|
186
|
+
buffered_bytes_per_tunnel: {},
|
|
187
|
+
frame_too_large_total: 0,
|
|
188
|
+
tunnel_disconnect_total: 0,
|
|
189
|
+
heartbeat_timeout_total: 0,
|
|
190
|
+
stream_stall_cleanup_total: 0,
|
|
191
|
+
event_loop_lag_ms: { p50: 0, p99: 0 },
|
|
192
|
+
tunnels_detail: {},
|
|
193
|
+
};
|
|
194
|
+
mockGetMetrics.mockReturnValue({ snapshot: jest.fn(() => zeroSnapshot) });
|
|
195
|
+
|
|
196
|
+
const RED = createMockRED();
|
|
197
|
+
const tunnelModule = require(WS_TUNNEL_PATH);
|
|
198
|
+
tunnelModule(RED);
|
|
199
|
+
|
|
200
|
+
const WSTunnelServer = RED._registeredTypes['wstunnel server'];
|
|
201
|
+
const node = { id: 'testNode' };
|
|
202
|
+
WSTunnelServer.call(node, { wstunnel: 'test-config-id', name: 'test-server' });
|
|
203
|
+
|
|
204
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
205
|
+
|
|
206
|
+
const inputHandler = node._handlers['input'];
|
|
207
|
+
const msg = {};
|
|
208
|
+
inputHandler.call(node, msg);
|
|
209
|
+
|
|
210
|
+
expect(msg.payload.active_tunnels).toBe(0);
|
|
211
|
+
expect(msg.payload.active_tunnel_ids).toEqual([]);
|
|
212
|
+
expect(msg.payload.tunnels_detail).toEqual({});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('msg.event is "metrics" to distinguish from connect/disconnect', async () => {
|
|
216
|
+
const RED = createMockRED();
|
|
217
|
+
const tunnelModule = require(WS_TUNNEL_PATH);
|
|
218
|
+
tunnelModule(RED);
|
|
219
|
+
|
|
220
|
+
const WSTunnelServer = RED._registeredTypes['wstunnel server'];
|
|
221
|
+
const node = { id: 'testNode' };
|
|
222
|
+
WSTunnelServer.call(node, { wstunnel: 'test-config-id', name: 'test-server' });
|
|
223
|
+
|
|
224
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
225
|
+
|
|
226
|
+
const inputHandler = node._handlers['input'];
|
|
227
|
+
const msg = {};
|
|
228
|
+
inputHandler.call(node, msg);
|
|
229
|
+
|
|
230
|
+
expect(msg.event).toBe('metrics');
|
|
231
|
+
expect(msg.event).not.toBe('connect');
|
|
232
|
+
expect(msg.event).not.toBe('disconnect');
|
|
233
|
+
});
|
|
234
|
+
});
|