hsync 0.30.1 → 0.31.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.
@@ -0,0 +1,315 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { initRelays, setNet } from '../../lib/socket-relays.js';
3
+ import { sockets } from '../../lib/socket-map.js';
4
+
5
+ describe('socket-relays', () => {
6
+ let mockNet;
7
+ let mockSocket;
8
+ let mockHsyncClient;
9
+ let mockPeer;
10
+
11
+ beforeEach(() => {
12
+ mockSocket = {
13
+ socketId: null,
14
+ write: vi.fn(),
15
+ end: vi.fn(),
16
+ on: vi.fn(),
17
+ connect: vi.fn((port, host, cb) => cb && cb()),
18
+ };
19
+
20
+ // Create mock net module with class-based constructor
21
+ mockNet = {
22
+ Socket: class MockSocket {
23
+ constructor() {
24
+ Object.assign(this, mockSocket);
25
+ }
26
+ },
27
+ };
28
+
29
+ mockPeer = {
30
+ hostName: 'remote.example.com',
31
+ notifications: {
32
+ oncloseRelaySocket: vi.fn(),
33
+ },
34
+ notifiers: {
35
+ closeListenerSocket: vi.fn(),
36
+ },
37
+ packAndSend: vi.fn(),
38
+ };
39
+
40
+ mockHsyncClient = {
41
+ myHostName: 'local.example.com',
42
+ };
43
+
44
+ setNet(mockNet);
45
+ });
46
+
47
+ describe('setNet', () => {
48
+ it('should set the net implementation', () => {
49
+ const customNet = { Socket: vi.fn() };
50
+ setNet(customNet);
51
+ // No error means success
52
+ });
53
+ });
54
+
55
+ describe('initRelays', () => {
56
+ it('should return object with required methods', () => {
57
+ const relays = initRelays(mockHsyncClient);
58
+
59
+ expect(relays.addSocketRelay).toBeTypeOf('function');
60
+ expect(relays.getSocketRelays).toBeTypeOf('function');
61
+ expect(relays.connectSocket).toBeTypeOf('function');
62
+ });
63
+
64
+ it('should attach methods to hsyncClient', () => {
65
+ initRelays(mockHsyncClient);
66
+
67
+ expect(mockHsyncClient.cachedRelays).toBeTypeOf('object');
68
+ expect(mockHsyncClient.addSocketRelay).toBeTypeOf('function');
69
+ expect(mockHsyncClient.getSocketRelays).toBeTypeOf('function');
70
+ expect(mockHsyncClient.connectSocket).toBeTypeOf('function');
71
+ });
72
+ });
73
+
74
+ describe('addSocketRelay', () => {
75
+ let relays;
76
+
77
+ beforeEach(() => {
78
+ relays = initRelays(mockHsyncClient);
79
+ });
80
+
81
+ it('should add relay with provided options', () => {
82
+ const relay = relays.addSocketRelay({
83
+ port: 3000,
84
+ targetPort: 4000,
85
+ targetHost: 'myserver.local',
86
+ whitelist: 'allowed.com',
87
+ blacklist: 'blocked.com',
88
+ });
89
+
90
+ expect(relay.port).toBe(3000);
91
+ expect(relay.targetPort).toBe(4000);
92
+ expect(relay.targetHost).toBe('myserver.local');
93
+ expect(relay.whitelist).toBe('allowed.com');
94
+ expect(relay.blacklist).toBe('blocked.com');
95
+ });
96
+
97
+ it('should use port as targetPort if not specified', () => {
98
+ const relay = relays.addSocketRelay({
99
+ port: 3000,
100
+ });
101
+
102
+ expect(relay.targetPort).toBe(3000);
103
+ });
104
+
105
+ it('should use localhost as targetHost if not specified', () => {
106
+ const relay = relays.addSocketRelay({
107
+ port: 3000,
108
+ });
109
+
110
+ expect(relay.targetHost).toBe('localhost');
111
+ });
112
+
113
+ it('should store relay by port key', () => {
114
+ relays.addSocketRelay({
115
+ port: 3000,
116
+ });
117
+
118
+ expect(mockHsyncClient.cachedRelays['p3000']).toBeDefined();
119
+ });
120
+
121
+ it('should set hostName same as targetHost', () => {
122
+ const relay = relays.addSocketRelay({
123
+ port: 3000,
124
+ targetHost: 'myserver.local',
125
+ });
126
+
127
+ expect(relay.hostName).toBe('myserver.local');
128
+ });
129
+ });
130
+
131
+ describe('getSocketRelays', () => {
132
+ let relays;
133
+
134
+ beforeEach(() => {
135
+ relays = initRelays(mockHsyncClient);
136
+ });
137
+
138
+ it('should return empty array when no relays', () => {
139
+ const result = relays.getSocketRelays();
140
+
141
+ expect(result).toEqual([]);
142
+ });
143
+
144
+ it('should return relay info', () => {
145
+ relays.addSocketRelay({
146
+ port: 3000,
147
+ targetPort: 4000,
148
+ targetHost: 'myserver.local',
149
+ whitelist: 'allowed.com',
150
+ blacklist: 'blocked.com',
151
+ });
152
+
153
+ const result = relays.getSocketRelays();
154
+
155
+ expect(result).toHaveLength(1);
156
+ expect(result[0]).toEqual({
157
+ port: 3000,
158
+ targetHost: 'myserver.local',
159
+ targetPort: 4000,
160
+ whitelist: 'allowed.com',
161
+ blacklist: 'blocked.com',
162
+ hostName: 'myserver.local',
163
+ });
164
+ });
165
+
166
+ it('should return multiple relays', () => {
167
+ relays.addSocketRelay({ port: 3000 });
168
+ relays.addSocketRelay({ port: 4000 });
169
+
170
+ const result = relays.getSocketRelays();
171
+
172
+ expect(result).toHaveLength(2);
173
+ });
174
+
175
+ it('should return empty strings for undefined whitelist/blacklist', () => {
176
+ relays.addSocketRelay({
177
+ port: 3000,
178
+ });
179
+
180
+ const result = relays.getSocketRelays();
181
+
182
+ expect(result[0].whitelist).toBe('');
183
+ expect(result[0].blacklist).toBe('');
184
+ });
185
+ });
186
+
187
+ describe('connectSocket', () => {
188
+ let relays;
189
+
190
+ beforeEach(() => {
191
+ relays = initRelays(mockHsyncClient);
192
+ });
193
+
194
+ it('should throw if no relay found for port', () => {
195
+ expect(() =>
196
+ relays.connectSocket(mockPeer, {
197
+ port: 9999,
198
+ socketId: 'test-socket',
199
+ hostName: 'remote.example.com',
200
+ })
201
+ ).toThrow('no relay found for port: 9999');
202
+ });
203
+
204
+ it('should create socket and connect to relay target', async () => {
205
+ relays.addSocketRelay({
206
+ port: 3000,
207
+ targetPort: 4000,
208
+ targetHost: 'myserver.local',
209
+ });
210
+
211
+ const result = await relays.connectSocket(mockPeer, {
212
+ port: 3000,
213
+ socketId: 'test-socket',
214
+ hostName: 'remote.example.com',
215
+ });
216
+
217
+ // Verify socket was connected to correct target
218
+ expect(mockSocket.connect).toHaveBeenCalledWith(4000, 'myserver.local', expect.any(Function));
219
+ expect(result.socketId).toBe('test-socket');
220
+ expect(result.targetHost).toBe('myserver.local');
221
+ expect(result.targetPort).toBe(4000);
222
+ });
223
+
224
+ it('should assign socketId to created socket', async () => {
225
+ relays.addSocketRelay({
226
+ port: 3000,
227
+ });
228
+
229
+ await relays.connectSocket(mockPeer, {
230
+ port: 3000,
231
+ socketId: 'my-socket-id',
232
+ hostName: 'remote.example.com',
233
+ });
234
+
235
+ // Socket should be stored in sockets map with the socketId
236
+ expect(sockets['my-socket-id']).toBeDefined();
237
+ expect(sockets['my-socket-id'].socketId).toBe('my-socket-id');
238
+ });
239
+
240
+ it('should register oncloseRelaySocket notification', async () => {
241
+ relays.addSocketRelay({
242
+ port: 3000,
243
+ });
244
+
245
+ await relays.connectSocket(mockPeer, {
246
+ port: 3000,
247
+ socketId: 'test-socket',
248
+ hostName: 'remote.example.com',
249
+ });
250
+
251
+ expect(mockPeer.notifications.oncloseRelaySocket).toHaveBeenCalled();
252
+ });
253
+
254
+ it('should register socket event handlers', async () => {
255
+ relays.addSocketRelay({
256
+ port: 3000,
257
+ });
258
+
259
+ await relays.connectSocket(mockPeer, {
260
+ port: 3000,
261
+ socketId: 'test-socket',
262
+ hostName: 'remote.example.com',
263
+ });
264
+
265
+ expect(mockSocket.on).toHaveBeenCalledWith('data', expect.any(Function));
266
+ expect(mockSocket.on).toHaveBeenCalledWith('close', expect.any(Function));
267
+ expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function));
268
+ });
269
+
270
+ it('should send data via RTC when packAndSend available', async () => {
271
+ relays.addSocketRelay({
272
+ port: 3000,
273
+ });
274
+
275
+ await relays.connectSocket(mockPeer, {
276
+ port: 3000,
277
+ socketId: 'test-socket',
278
+ hostName: 'remote.example.com',
279
+ });
280
+
281
+ // Get the data handler
282
+ const dataHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'data')[1];
283
+ const testData = Buffer.from('test data');
284
+ dataHandler(testData);
285
+
286
+ expect(mockPeer.packAndSend).toHaveBeenCalledWith(
287
+ 'socketData/test-socket',
288
+ expect.any(Buffer)
289
+ );
290
+ });
291
+
292
+ it('should reject on socket error', async () => {
293
+ relays.addSocketRelay({
294
+ port: 3000,
295
+ });
296
+
297
+ // Make connect call the error handler instead
298
+ mockSocket.connect = vi.fn((_port, _host, _cb) => {
299
+ // Don't call success callback
300
+ });
301
+
302
+ const connectPromise = relays.connectSocket(mockPeer, {
303
+ port: 3000,
304
+ socketId: 'test-socket',
305
+ hostName: 'remote.example.com',
306
+ });
307
+
308
+ // Get error handler and call it
309
+ const errorHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'error')[1];
310
+ errorHandler(new Error('Connection failed'));
311
+
312
+ await expect(connectPromise).rejects.toThrow('Connection failed');
313
+ });
314
+ });
315
+ });
@@ -0,0 +1,223 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createWebHandler, setNet } from '../../lib/web-handler.js';
3
+
4
+ describe('web-handler', () => {
5
+ let mockNet;
6
+ let mockSocket;
7
+ let mockMqConn;
8
+
9
+ beforeEach(() => {
10
+ // Create mock socket
11
+ mockSocket = {
12
+ socketId: null,
13
+ write: vi.fn(),
14
+ end: vi.fn(),
15
+ on: vi.fn(),
16
+ connect: vi.fn((port, host, cb) => cb()),
17
+ };
18
+
19
+ // Create mock net module with class-based constructor
20
+ mockNet = {
21
+ Socket: class MockSocket {
22
+ constructor() {
23
+ Object.assign(this, mockSocket);
24
+ }
25
+ },
26
+ };
27
+
28
+ // Create mock MQTT connection
29
+ mockMqConn = {
30
+ publish: vi.fn(),
31
+ };
32
+
33
+ setNet(mockNet);
34
+ });
35
+
36
+ describe('setNet', () => {
37
+ it('should set the net implementation', () => {
38
+ const customNet = { Socket: vi.fn() };
39
+ setNet(customNet);
40
+ // No error means success - net is used internally
41
+ });
42
+ });
43
+
44
+ describe('createWebHandler', () => {
45
+ it('should return handler with required methods', () => {
46
+ const handler = createWebHandler({
47
+ myHostName: 'test.example.com',
48
+ localHost: 'localhost',
49
+ port: 3000,
50
+ mqConn: mockMqConn,
51
+ });
52
+
53
+ expect(handler.handleWebRequest).toBeTypeOf('function');
54
+ expect(handler.sockets).toBeTypeOf('object');
55
+ expect(handler.end).toBeTypeOf('function');
56
+ });
57
+
58
+ it('should start with empty sockets', () => {
59
+ const handler = createWebHandler({
60
+ myHostName: 'test.example.com',
61
+ localHost: 'localhost',
62
+ port: 3000,
63
+ mqConn: mockMqConn,
64
+ });
65
+
66
+ expect(Object.keys(handler.sockets)).toHaveLength(0);
67
+ });
68
+ });
69
+
70
+ describe('handleWebRequest', () => {
71
+ let handler;
72
+
73
+ beforeEach(() => {
74
+ handler = createWebHandler({
75
+ myHostName: 'test.example.com',
76
+ localHost: 'localhost',
77
+ port: 3000,
78
+ mqConn: mockMqConn,
79
+ });
80
+ });
81
+
82
+ it('should ignore requests for different hostnames', () => {
83
+ handler.handleWebRequest('other.example.com', 'socket-123', null, Buffer.from('test'));
84
+
85
+ // Socket should not be created for different hostname
86
+ expect(handler.sockets['socket-123']).toBeUndefined();
87
+ });
88
+
89
+ it('should create new socket for new socketId', () => {
90
+ const message = Buffer.from('GET / HTTP/1.1\r\n\r\n');
91
+
92
+ handler.handleWebRequest('test.example.com', 'socket-123', null, message);
93
+
94
+ // Verify socket was created and connected
95
+ expect(handler.sockets['socket-123']).toBeDefined();
96
+ expect(mockSocket.connect).toHaveBeenCalledWith(3000, 'localhost', expect.any(Function));
97
+ });
98
+
99
+ it('should store socket in sockets map', () => {
100
+ const message = Buffer.from('GET / HTTP/1.1\r\n\r\n');
101
+
102
+ handler.handleWebRequest('test.example.com', 'socket-123', null, message);
103
+
104
+ expect(handler.sockets['socket-123']).toBeDefined();
105
+ });
106
+
107
+ it('should write message to socket after connect', () => {
108
+ const message = Buffer.from('GET / HTTP/1.1\r\n\r\n');
109
+
110
+ handler.handleWebRequest('test.example.com', 'socket-123', null, message);
111
+
112
+ expect(mockSocket.write).toHaveBeenCalledWith(message);
113
+ });
114
+
115
+ it('should handle close action', () => {
116
+ const message = Buffer.from('GET / HTTP/1.1\r\n\r\n');
117
+
118
+ // First create a socket
119
+ handler.handleWebRequest('test.example.com', 'socket-123', null, message);
120
+ expect(handler.sockets['socket-123']).toBeDefined();
121
+
122
+ // Now close it
123
+ handler.handleWebRequest('test.example.com', 'socket-123', 'close', Buffer.from(''));
124
+
125
+ expect(mockSocket.end).toHaveBeenCalled();
126
+ });
127
+
128
+ it('should ignore close for non-existent socket', () => {
129
+ handler.handleWebRequest('test.example.com', 'nonexistent', 'close', Buffer.from(''));
130
+
131
+ // Should not throw
132
+ expect(mockSocket.end).not.toHaveBeenCalled();
133
+ });
134
+
135
+ it('should reuse existing socket for same socketId', () => {
136
+ const message1 = Buffer.from('first message');
137
+ const message2 = Buffer.from('second message');
138
+
139
+ handler.handleWebRequest('test.example.com', 'socket-123', null, message1);
140
+ handler.handleWebRequest('test.example.com', 'socket-123', null, message2);
141
+
142
+ // Socket should only be connected once (reused for second message)
143
+ expect(mockSocket.connect).toHaveBeenCalledTimes(1);
144
+ // But write should be called twice (once on connect, once for second message)
145
+ expect(mockSocket.write).toHaveBeenCalledTimes(2);
146
+ });
147
+
148
+ it('should register socket event handlers', () => {
149
+ const message = Buffer.from('test');
150
+
151
+ handler.handleWebRequest('test.example.com', 'socket-123', null, message);
152
+
153
+ expect(mockSocket.on).toHaveBeenCalledWith('data', expect.any(Function));
154
+ expect(mockSocket.on).toHaveBeenCalledWith('close', expect.any(Function));
155
+ expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function));
156
+ });
157
+
158
+ it('should publish reply on socket data', () => {
159
+ const message = Buffer.from('request');
160
+ const responseData = Buffer.from('HTTP/1.1 200 OK\r\n\r\n');
161
+
162
+ handler.handleWebRequest('test.example.com', 'socket-123', null, message);
163
+
164
+ // Get the data handler and call it
165
+ const dataHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'data')[1];
166
+ dataHandler(responseData);
167
+
168
+ expect(mockMqConn.publish).toHaveBeenCalledWith(
169
+ 'reply/test.example.com/socket-123',
170
+ responseData
171
+ );
172
+ });
173
+
174
+ it('should publish close on socket close', () => {
175
+ const message = Buffer.from('request');
176
+
177
+ handler.handleWebRequest('test.example.com', 'socket-123', null, message);
178
+
179
+ // Get the close handler and call it
180
+ const closeHandler = mockSocket.on.mock.calls.find((call) => call[0] === 'close')[1];
181
+ closeHandler();
182
+
183
+ expect(mockMqConn.publish).toHaveBeenCalledWith('close/test.example.com/socket-123', '');
184
+ });
185
+ });
186
+
187
+ describe('end', () => {
188
+ it('should close all sockets', () => {
189
+ const handler = createWebHandler({
190
+ myHostName: 'test.example.com',
191
+ localHost: 'localhost',
192
+ port: 3000,
193
+ mqConn: mockMqConn,
194
+ });
195
+
196
+ // Create some sockets
197
+ handler.handleWebRequest('test.example.com', 'socket-1', null, Buffer.from('test1'));
198
+ handler.handleWebRequest('test.example.com', 'socket-2', null, Buffer.from('test2'));
199
+
200
+ handler.end();
201
+
202
+ expect(mockSocket.end).toHaveBeenCalled();
203
+ });
204
+
205
+ it('should handle errors when closing sockets', () => {
206
+ const handler = createWebHandler({
207
+ myHostName: 'test.example.com',
208
+ localHost: 'localhost',
209
+ port: 3000,
210
+ mqConn: mockMqConn,
211
+ });
212
+
213
+ mockSocket.end = vi.fn(() => {
214
+ throw new Error('socket error');
215
+ });
216
+
217
+ handler.handleWebRequest('test.example.com', 'socket-1', null, Buffer.from('test'));
218
+
219
+ // Should not throw
220
+ expect(() => handler.end()).not.toThrow();
221
+ });
222
+ });
223
+ });