laravel-echo-api-gateway 0.5.8 → 0.5.9
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/.eslintrc.js +1 -0
- package/.github/workflows/ci.yml +49 -45
- package/README.md +93 -17
- package/dist/Channel.d.ts +89 -0
- package/dist/Connector.d.ts +92 -0
- package/dist/EventFormatter.d.ts +10 -0
- package/dist/Websocket.d.ts +78 -0
- package/dist/laravel-echo-api-gateway.common.js +403 -762
- package/dist/laravel-echo-api-gateway.iife.js +487 -846
- package/dist/laravel-echo-api-gateway.js +401 -760
- package/jest.config.js +3 -7
- package/js-src/Channel.ts +74 -62
- package/js-src/Connector.ts +100 -40
- package/js-src/EventFormatter.ts +24 -0
- package/js-src/Websocket.ts +217 -168
- package/js-tests/Connector.test.ts +295 -60
- package/js-tests/EchoCompatibility.test.ts +96 -0
- package/js-tests/helpers.ts +37 -0
- package/package.json +23 -30
- package/phpstan.neon.dist +3 -12
- package/phpunit.xml +2 -2
- package/rollup.config.mjs +14 -0
- package/tsconfig.json +6 -6
- package/dist/js-src/Channel.d.ts +0 -68
- package/dist/js-src/Connector.d.ts +0 -49
- package/dist/js-src/Websocket.d.ts +0 -38
- package/rollup.config.js +0 -31
|
@@ -1,109 +1,344 @@
|
|
|
1
1
|
import WS from "jest-websocket-mock";
|
|
2
2
|
import { Connector } from "../js-src/Connector";
|
|
3
3
|
import { Channel } from "../js-src/Channel";
|
|
4
|
-
|
|
5
|
-
const mockedHost = 'ws://localhost:1234';
|
|
4
|
+
import { flushPromises, identify, mockedHost, mockFetchResponse, restartServer } from "./helpers";
|
|
6
5
|
|
|
7
6
|
describe('Connector', () => {
|
|
8
7
|
let server: WS;
|
|
8
|
+
let connector: Connector;
|
|
9
9
|
|
|
10
10
|
beforeEach(() => {
|
|
11
|
-
|
|
12
|
-
server = new WS(mockedHost);
|
|
11
|
+
server = new WS(mockedHost, { jsonProtocol: true });
|
|
13
12
|
});
|
|
14
13
|
|
|
15
14
|
afterEach(() => {
|
|
16
|
-
|
|
15
|
+
connector?.disconnect();
|
|
16
|
+
connector = undefined;
|
|
17
|
+
WS.clean();
|
|
18
|
+
jest.restoreAllMocks();
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
test('socket id is correctly set', async () => {
|
|
20
|
-
|
|
21
|
-
host: mockedHost,
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
await server.connected;
|
|
22
|
+
connector = new Connector({ host: mockedHost });
|
|
25
23
|
|
|
26
|
-
await
|
|
27
|
-
server.send('{"event":"whoami","data":{"socket_id":"test-socket-id"}}')
|
|
24
|
+
await identify(server, 'test-socket-id');
|
|
28
25
|
|
|
29
|
-
expect(connector.socketId()).toBe('test-socket-id')
|
|
30
|
-
})
|
|
26
|
+
expect(connector.socketId()).toBe('test-socket-id');
|
|
27
|
+
});
|
|
31
28
|
|
|
32
29
|
test('we reconnect to the server on error', async () => {
|
|
33
|
-
|
|
34
|
-
host: mockedHost,
|
|
35
|
-
})
|
|
30
|
+
connector = new Connector({ host: mockedHost, reconnectDelay: 10 });
|
|
36
31
|
|
|
37
|
-
await server
|
|
38
|
-
await expect(server).toReceiveMessage('{"event":"whoami"}');
|
|
39
|
-
server.send('{"event":"whoami","data":{"socket_id":"test-socket-id"}}')
|
|
32
|
+
await identify(server, 'test-socket-id');
|
|
40
33
|
|
|
41
|
-
server
|
|
42
|
-
await server
|
|
43
|
-
server.server.stop(() => (server = new WS(mockedHost)));
|
|
34
|
+
server = await restartServer(server);
|
|
35
|
+
await identify(server, 'test-socket-id2');
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
server.send('{"event":"whoami","data":{"socket_id":"test-socket-id2"}}')
|
|
37
|
+
expect(connector.socketId()).toBe('test-socket-id2');
|
|
38
|
+
});
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
test('we only open one new connection per reconnect', async () => {
|
|
41
|
+
connector = new Connector({ host: mockedHost, reconnectDelay: 10 });
|
|
51
42
|
|
|
52
|
-
|
|
53
|
-
const connector = new Connector({
|
|
54
|
-
host: mockedHost,
|
|
55
|
-
})
|
|
43
|
+
await identify(server, 'test-socket-id');
|
|
56
44
|
|
|
57
|
-
await server
|
|
45
|
+
server = await restartServer(server);
|
|
46
|
+
await identify(server, 'test-socket-id2');
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
expect(server.server.clients()).toHaveLength(1);
|
|
50
|
+
});
|
|
61
51
|
|
|
62
|
-
|
|
52
|
+
test('we keep retrying until the server is available, with a single connection', async () => {
|
|
53
|
+
WS.clean();
|
|
63
54
|
|
|
64
|
-
|
|
55
|
+
connector = new Connector({ host: mockedHost, reconnectDelay: 5, maxReconnectDelay: 20 });
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
65
57
|
|
|
66
|
-
server
|
|
58
|
+
server = new WS(mockedHost, { jsonProtocol: true });
|
|
59
|
+
await identify(server, 'test-socket-id');
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
67
61
|
|
|
68
|
-
expect(
|
|
62
|
+
expect(connector.socketId()).toBe('test-socket-id');
|
|
63
|
+
expect(server.server.clients()).toHaveLength(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('we can subscribe to a channel and listen to events', async () => {
|
|
67
|
+
connector = new Connector({ host: mockedHost });
|
|
68
|
+
|
|
69
|
+
await identify(server, 'test-socket-id');
|
|
70
|
+
|
|
71
|
+
const channel = connector.channel('my-test-channel');
|
|
72
|
+
|
|
73
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'my-test-channel' } });
|
|
74
|
+
|
|
75
|
+
expect(channel).toBeInstanceOf(Channel);
|
|
69
76
|
|
|
70
77
|
const handler1 = jest.fn();
|
|
71
78
|
const handler2 = jest.fn();
|
|
72
79
|
|
|
73
|
-
channel.
|
|
80
|
+
channel.listen('.my-test-event', handler1);
|
|
74
81
|
|
|
75
|
-
server.send(
|
|
82
|
+
server.send({ event: 'my-test-event', channel: 'my-test-channel', data: { id: 1 } });
|
|
76
83
|
|
|
77
|
-
expect(handler1).
|
|
78
|
-
expect(handler2).not.
|
|
79
|
-
})
|
|
84
|
+
expect(handler1).toHaveBeenCalledWith({ id: 1 });
|
|
85
|
+
expect(handler2).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
80
87
|
|
|
81
|
-
test('
|
|
82
|
-
|
|
83
|
-
host: mockedHost,
|
|
84
|
-
})
|
|
88
|
+
test('channels subscribed before the connection is ready are subscribed once identified', async () => {
|
|
89
|
+
connector = new Connector({ host: mockedHost });
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
connector.channel('early-channel');
|
|
87
92
|
|
|
88
|
-
await
|
|
89
|
-
server.send('{"event":"whoami","data":{"socket_id":"test-socket-id"}}')
|
|
93
|
+
await identify(server, 'test-socket-id');
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'early-channel' } });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('every listener of the same event is called', async () => {
|
|
99
|
+
connector = new Connector({ host: mockedHost });
|
|
100
|
+
await identify(server, 'test-socket-id');
|
|
101
|
+
|
|
102
|
+
const handler1 = jest.fn();
|
|
103
|
+
const handler2 = jest.fn();
|
|
104
|
+
|
|
105
|
+
connector.channel('my-test-channel').listen('.my-test-event', handler1);
|
|
106
|
+
connector.channel('my-test-channel').listen('.my-test-event', handler2);
|
|
107
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'my-test-channel' } });
|
|
92
108
|
|
|
93
|
-
|
|
109
|
+
server.send({ event: 'my-test-event', channel: 'my-test-channel', data: {} });
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
expect(handler1).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(handler2).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
96
114
|
|
|
97
|
-
|
|
115
|
+
test('stopListening with a callback only removes that callback', async () => {
|
|
116
|
+
connector = new Connector({ host: mockedHost });
|
|
117
|
+
await identify(server, 'test-socket-id');
|
|
98
118
|
|
|
99
119
|
const handler1 = jest.fn();
|
|
100
120
|
const handler2 = jest.fn();
|
|
101
121
|
|
|
102
|
-
channel.
|
|
122
|
+
const channel = connector.channel('my-test-channel')
|
|
123
|
+
.listen('.my-test-event', handler1)
|
|
124
|
+
.listen('.my-test-event', handler2);
|
|
125
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'my-test-channel' } });
|
|
126
|
+
|
|
127
|
+
channel.stopListening('.my-test-event', handler1);
|
|
128
|
+
server.send({ event: 'my-test-event', channel: 'my-test-channel', data: {} });
|
|
129
|
+
|
|
130
|
+
expect(handler1).not.toHaveBeenCalled();
|
|
131
|
+
expect(handler2).toHaveBeenCalledTimes(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('stopListening without a callback removes every listener of the event', async () => {
|
|
135
|
+
connector = new Connector({ host: mockedHost });
|
|
136
|
+
await identify(server, 'test-socket-id');
|
|
137
|
+
|
|
138
|
+
const handler = jest.fn();
|
|
103
139
|
|
|
104
|
-
|
|
140
|
+
const channel = connector.channel('my-test-channel').listen('.my-test-event', handler);
|
|
141
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'my-test-channel' } });
|
|
105
142
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
channel.stopListening('.my-test-event');
|
|
144
|
+
server.send({ event: 'my-test-event', channel: 'my-test-channel', data: {} });
|
|
145
|
+
|
|
146
|
+
expect(handler).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('channels joined after connecting are resubscribed after a reconnect', async () => {
|
|
150
|
+
connector = new Connector({ host: mockedHost, reconnectDelay: 10 });
|
|
151
|
+
await identify(server, 'test-socket-id');
|
|
152
|
+
|
|
153
|
+
const handler = jest.fn();
|
|
154
|
+
connector.channel('late-channel').listen('.my-test-event', handler);
|
|
155
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'late-channel' } });
|
|
156
|
+
|
|
157
|
+
server = await restartServer(server);
|
|
158
|
+
await identify(server, 'test-socket-id2');
|
|
159
|
+
|
|
160
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'late-channel' } });
|
|
161
|
+
|
|
162
|
+
server.send({ event: 'my-test-event', channel: 'late-channel', data: {} });
|
|
163
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('channels that were left are not resubscribed after a reconnect', async () => {
|
|
167
|
+
connector = new Connector({ host: mockedHost, reconnectDelay: 10 });
|
|
168
|
+
connector.channel('left-channel');
|
|
169
|
+
connector.channel('kept-channel');
|
|
170
|
+
|
|
171
|
+
await identify(server, 'test-socket-id');
|
|
172
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'left-channel' } });
|
|
173
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'kept-channel' } });
|
|
174
|
+
|
|
175
|
+
connector.leave('left-channel');
|
|
176
|
+
await expect(server).toReceiveMessage({ event: 'unsubscribe', data: { channel: 'left-channel' } });
|
|
177
|
+
|
|
178
|
+
server = await restartServer(server);
|
|
179
|
+
await identify(server, 'test-socket-id2');
|
|
180
|
+
|
|
181
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'kept-channel' } });
|
|
182
|
+
await flushPromises();
|
|
183
|
+
expect(server.messages).not.toContainEqual({ event: 'subscribe', data: { channel: 'left-channel' } });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('pings are not duplicated after reconnecting', async () => {
|
|
187
|
+
connector = new Connector({ host: mockedHost, reconnectDelay: 10, pingInterval: 40 });
|
|
188
|
+
await identify(server, 'test-socket-id');
|
|
189
|
+
|
|
190
|
+
server = await restartServer(server);
|
|
191
|
+
await identify(server, 'test-socket-id2');
|
|
192
|
+
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, 210));
|
|
194
|
+
|
|
195
|
+
const pings = server.messages.filter((message: any) => message.event === 'ping');
|
|
196
|
+
expect(pings.length).toBeGreaterThanOrEqual(3);
|
|
197
|
+
expect(pings.length).toBeLessThanOrEqual(6);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('subscribed callbacks run when the server confirms the subscription', async () => {
|
|
201
|
+
connector = new Connector({ host: mockedHost });
|
|
202
|
+
await identify(server, 'test-socket-id');
|
|
203
|
+
|
|
204
|
+
const subscribed = jest.fn();
|
|
205
|
+
connector.channel('my-test-channel').subscribed(subscribed);
|
|
206
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'my-test-channel' } });
|
|
207
|
+
|
|
208
|
+
server.send({ event: 'subscription_succeeded', channel: 'my-test-channel', data: [] });
|
|
209
|
+
|
|
210
|
+
expect(subscribed).toHaveBeenCalledTimes(1);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('we can send a whisper event and listen for whispers', async () => {
|
|
214
|
+
connector = new Connector({ host: mockedHost });
|
|
215
|
+
mockFetchResponse({ auth: 'signature' });
|
|
216
|
+
await identify(server, 'test-socket-id');
|
|
217
|
+
|
|
218
|
+
const channel = connector.privateChannel('my-test-channel');
|
|
219
|
+
await expect(server).toReceiveMessage({
|
|
220
|
+
event: 'subscribe',
|
|
221
|
+
data: { channel: 'private-my-test-channel', auth: 'signature' },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const handler = jest.fn();
|
|
225
|
+
channel.listenForWhisper('typing', handler);
|
|
226
|
+
channel.whisper('typing', { name: 'Ana' });
|
|
227
|
+
|
|
228
|
+
await expect(server).toReceiveMessage({
|
|
229
|
+
event: 'client-typing',
|
|
230
|
+
channel: 'private-my-test-channel',
|
|
231
|
+
data: { name: 'Ana' },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
server.send({ event: 'client-typing', channel: 'private-my-test-channel', data: { name: 'Luis' } });
|
|
235
|
+
expect(handler).toHaveBeenCalledWith({ name: 'Luis' });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('private channel authorization', () => {
|
|
239
|
+
test('posts to the auth endpoint with the configured headers', async () => {
|
|
240
|
+
const fetchMock = mockFetchResponse({ auth: 'signature' });
|
|
241
|
+
|
|
242
|
+
connector = new Connector({
|
|
243
|
+
host: mockedHost,
|
|
244
|
+
authEndpoint: 'https://app.test/broadcasting/auth',
|
|
245
|
+
bearerToken: 'secret-token',
|
|
246
|
+
auth: { headers: { 'X-Tenant': 'tenant-1' } },
|
|
247
|
+
});
|
|
248
|
+
connector.privateChannel('orders');
|
|
249
|
+
|
|
250
|
+
await identify(server, 'test-socket-id');
|
|
251
|
+
|
|
252
|
+
await expect(server).toReceiveMessage({
|
|
253
|
+
event: 'subscribe',
|
|
254
|
+
data: { channel: 'private-orders', auth: 'signature' },
|
|
255
|
+
});
|
|
256
|
+
expect(fetchMock).toHaveBeenCalledWith('https://app.test/broadcasting/auth', {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: {
|
|
259
|
+
'Content-Type': 'application/json',
|
|
260
|
+
'Accept': 'application/json',
|
|
261
|
+
'Authorization': 'Bearer secret-token',
|
|
262
|
+
'X-Tenant': 'tenant-1',
|
|
263
|
+
},
|
|
264
|
+
body: JSON.stringify({ socket_id: 'test-socket-id', channel_name: 'private-orders' }),
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('uses channelAuthorization endpoint and headers when given', async () => {
|
|
269
|
+
const fetchMock = mockFetchResponse({ auth: 'signature' });
|
|
270
|
+
|
|
271
|
+
connector = new Connector({
|
|
272
|
+
host: mockedHost,
|
|
273
|
+
channelAuthorization: {
|
|
274
|
+
endpoint: 'https://api.test/broadcasting/auth',
|
|
275
|
+
headers: { 'X-Api-Key': 'key' },
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
connector.privateChannel('orders');
|
|
279
|
+
|
|
280
|
+
await identify(server, 'test-socket-id');
|
|
281
|
+
await expect(server).toReceiveMessage({
|
|
282
|
+
event: 'subscribe',
|
|
283
|
+
data: { channel: 'private-orders', auth: 'signature' },
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(fetchMock).toHaveBeenCalledWith('https://api.test/broadcasting/auth', expect.objectContaining({
|
|
287
|
+
headers: expect.objectContaining({ 'X-Api-Key': 'key' }),
|
|
288
|
+
}));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('uses channelAuthorization.customHandler when given', async () => {
|
|
292
|
+
const fetchMock = mockFetchResponse({ auth: 'from-fetch' });
|
|
293
|
+
const customHandler = jest.fn((params, callback) => callback(null, { auth: 'from-handler' }));
|
|
294
|
+
|
|
295
|
+
connector = new Connector({ host: mockedHost, channelAuthorization: { customHandler } });
|
|
296
|
+
connector.privateChannel('orders');
|
|
297
|
+
|
|
298
|
+
await identify(server, 'test-socket-id');
|
|
299
|
+
await expect(server).toReceiveMessage({
|
|
300
|
+
event: 'subscribe',
|
|
301
|
+
data: { channel: 'private-orders', auth: 'from-handler' },
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(customHandler).toHaveBeenCalledWith(
|
|
305
|
+
{ socketId: 'test-socket-id', channelName: 'private-orders' },
|
|
306
|
+
expect.any(Function),
|
|
307
|
+
);
|
|
308
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('reports a failed authorization to the channel error callbacks', async () => {
|
|
312
|
+
mockFetchResponse({ message: 'Forbidden' }, false, 403);
|
|
313
|
+
|
|
314
|
+
connector = new Connector({ host: mockedHost });
|
|
315
|
+
const onError = jest.fn();
|
|
316
|
+
connector.privateChannel('orders').error(onError);
|
|
317
|
+
|
|
318
|
+
await identify(server, 'test-socket-id');
|
|
319
|
+
await flushPromises();
|
|
320
|
+
await flushPromises();
|
|
321
|
+
|
|
322
|
+
expect(onError).toHaveBeenCalledWith(expect.objectContaining({ type: 'AuthError', status: 403 }));
|
|
323
|
+
expect(server.messages).not.toContainEqual(expect.objectContaining({ event: 'subscribe' }));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('reports a rejected subscription to the channel error callbacks', async () => {
|
|
327
|
+
mockFetchResponse({ auth: 'bad-signature' });
|
|
328
|
+
|
|
329
|
+
connector = new Connector({ host: mockedHost });
|
|
330
|
+
const onError = jest.fn();
|
|
331
|
+
connector.privateChannel('orders').error(onError);
|
|
332
|
+
|
|
333
|
+
await identify(server, 'test-socket-id');
|
|
334
|
+
await expect(server).toReceiveMessage({
|
|
335
|
+
event: 'subscribe',
|
|
336
|
+
data: { channel: 'private-orders', auth: 'bad-signature' },
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
server.send({ event: 'error', channel: 'private-orders', data: { message: 'Invalid auth signature' } });
|
|
340
|
+
|
|
341
|
+
expect(onError).toHaveBeenCalledWith({ message: 'Invalid auth signature' });
|
|
342
|
+
});
|
|
343
|
+
});
|
|
109
344
|
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import WS from "jest-websocket-mock";
|
|
2
|
+
import EchoV2 from "laravel-echo";
|
|
3
|
+
import { Connector, broadcaster } from "../js-src/Connector";
|
|
4
|
+
import { identify, mockedHost, mockFetchResponse } from "./helpers";
|
|
5
|
+
|
|
6
|
+
// laravel-echo v1, installed under an alias. Its CommonJS build exports the class itself.
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
8
|
+
const echoV1Module = require("laravel-echo-v1");
|
|
9
|
+
const EchoV1 = echoV1Module.default ?? echoV1Module;
|
|
10
|
+
|
|
11
|
+
const echoVersions: Array<[string, any]> = [
|
|
12
|
+
['laravel-echo v1', EchoV1],
|
|
13
|
+
['laravel-echo v2', EchoV2],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe.each(echoVersions)('%s', (_version, Echo) => {
|
|
17
|
+
let server: WS;
|
|
18
|
+
let echo: any;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
server = new WS(mockedHost, { jsonProtocol: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
echo?.disconnect();
|
|
26
|
+
echo = undefined;
|
|
27
|
+
WS.clean();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test.each([
|
|
31
|
+
['Connector class', Connector],
|
|
32
|
+
['broadcaster export', broadcaster],
|
|
33
|
+
])('accepts the %s as broadcaster', async (_label, broadcasterOption) => {
|
|
34
|
+
echo = new Echo({ broadcaster: broadcasterOption, host: mockedHost });
|
|
35
|
+
|
|
36
|
+
await identify(server, 'socket-1');
|
|
37
|
+
|
|
38
|
+
expect(echo.connector).toBeInstanceOf(Connector);
|
|
39
|
+
expect(echo.socketId()).toBe('socket-1');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('authorizes a private channel and delivers namespaced events', async () => {
|
|
43
|
+
const fetchMock = mockFetchResponse({ auth: 'signature' });
|
|
44
|
+
|
|
45
|
+
echo = new Echo({
|
|
46
|
+
broadcaster: Connector,
|
|
47
|
+
host: mockedHost,
|
|
48
|
+
authEndpoint: 'https://app.test/broadcasting/auth',
|
|
49
|
+
bearerToken: 'secret-token',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const handler = jest.fn();
|
|
53
|
+
echo.private('orders').listen('OrderCreated', handler);
|
|
54
|
+
|
|
55
|
+
await identify(server, 'socket-1');
|
|
56
|
+
|
|
57
|
+
await expect(server).toReceiveMessage({
|
|
58
|
+
event: 'subscribe',
|
|
59
|
+
data: { channel: 'private-orders', auth: 'signature' },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(fetchMock).toHaveBeenCalledWith('https://app.test/broadcasting/auth', expect.objectContaining({
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: expect.objectContaining({ Authorization: 'Bearer secret-token' }),
|
|
65
|
+
body: JSON.stringify({ socket_id: 'socket-1', channel_name: 'private-orders' }),
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
server.send({ event: 'App\\Events\\OrderCreated', channel: 'private-orders', data: { id: 1 } });
|
|
69
|
+
|
|
70
|
+
expect(handler).toHaveBeenCalledWith({ id: 1 });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('leaving a channel unsubscribes from it', async () => {
|
|
74
|
+
echo = new Echo({ broadcaster: Connector, host: mockedHost });
|
|
75
|
+
|
|
76
|
+
echo.channel('news');
|
|
77
|
+
await identify(server, 'socket-1');
|
|
78
|
+
await expect(server).toReceiveMessage({ event: 'subscribe', data: { channel: 'news' } });
|
|
79
|
+
|
|
80
|
+
echo.leave('news');
|
|
81
|
+
|
|
82
|
+
await expect(server).toReceiveMessage({ event: 'unsubscribe', data: { channel: 'news' } });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Connector', () => {
|
|
87
|
+
afterEach(() => WS.clean());
|
|
88
|
+
|
|
89
|
+
test('does not open a connection when constructed without options', () => {
|
|
90
|
+
// laravel-echo v2 probes custom broadcasters by calling `new broadcaster()` without arguments.
|
|
91
|
+
const server = new WS(mockedHost, { jsonProtocol: true });
|
|
92
|
+
|
|
93
|
+
expect(() => new Connector()).not.toThrow();
|
|
94
|
+
expect(server.server.clients()).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import WS from "jest-websocket-mock";
|
|
2
|
+
|
|
3
|
+
export const mockedHost = 'ws://localhost:1234';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wait for the client to connect and ask for its socket id, then answer with the given id.
|
|
7
|
+
*/
|
|
8
|
+
export async function identify(server: WS, socketId: string): Promise<void> {
|
|
9
|
+
await server.connected;
|
|
10
|
+
await expect(server).toReceiveMessage({ event: 'whoami' });
|
|
11
|
+
server.send({ event: 'whoami', data: { socket_id: socketId } });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Close the current mock server and start a fresh one on the same host, simulating a dropped connection.
|
|
16
|
+
*/
|
|
17
|
+
export async function restartServer(server: WS): Promise<WS> {
|
|
18
|
+
server.close();
|
|
19
|
+
await server.closed;
|
|
20
|
+
WS.clean();
|
|
21
|
+
|
|
22
|
+
return new WS(mockedHost, { jsonProtocol: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function mockFetchResponse(body: object, ok = true, status = 200): jest.Mock {
|
|
26
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
27
|
+
ok,
|
|
28
|
+
status,
|
|
29
|
+
json: () => Promise.resolve(body),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
(globalThis as any).fetch = fetchMock;
|
|
33
|
+
|
|
34
|
+
return fetchMock;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const flushPromises = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "laravel-echo-api-gateway",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "Use Laravel Echo with API Gateway Websockets",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"laravel",
|
|
@@ -21,46 +21,39 @@
|
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"main": "dist/laravel-echo-api-gateway.common.js",
|
|
23
23
|
"module": "dist/laravel-echo-api-gateway.js",
|
|
24
|
-
"types": "dist/
|
|
24
|
+
"types": "dist/Connector.d.ts",
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "npm run compile && npm run declarations",
|
|
27
27
|
"compile": "./node_modules/.bin/rollup -c",
|
|
28
28
|
"declarations": "./node_modules/.bin/tsc --emitDeclarationOnly",
|
|
29
29
|
"lint": "eslint --ext .js,.ts ./js-src ./js-tests",
|
|
30
|
-
"prepublish": "npm run build",
|
|
31
30
|
"release": "npm run test && standard-version && git push --follow-tags && npm publish",
|
|
32
|
-
"test": "jest"
|
|
31
|
+
"test": "jest",
|
|
32
|
+
"prepare": "npm run build"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@
|
|
36
|
-
"@
|
|
37
|
-
"@
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
40
|
-
"@babel/plugin-transform-object-assign": "^7.8.3",
|
|
41
|
-
"@babel/preset-env": "^7.9.6",
|
|
42
|
-
"@rollup/plugin-babel": "^5.0.0",
|
|
43
|
-
"@types/babel__traverse": "^7.20.6",
|
|
44
|
-
"@types/jest": "^24.0.18",
|
|
45
|
-
"@types/node": "^12.7.5",
|
|
46
|
-
"@typescript-eslint/eslint-plugin": "^3.7.0",
|
|
47
|
-
"@typescript-eslint/parser": "^3.7.0",
|
|
35
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
36
|
+
"@types/jest": "^29.5.14",
|
|
37
|
+
"@types/node": "^20.19.43",
|
|
38
|
+
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
|
39
|
+
"@typescript-eslint/parser": "^5.62.0",
|
|
48
40
|
"eslint": "^7.5.0",
|
|
49
|
-
"jest": "^
|
|
50
|
-
"jest-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"rollup
|
|
56
|
-
"rollup-plugin-node-resolve": "^5.2.0",
|
|
57
|
-
"rollup-plugin-typescript2": "^0.27.1",
|
|
41
|
+
"jest": "^29.7.0",
|
|
42
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
43
|
+
"jest-websocket-mock": "^2.5.0",
|
|
44
|
+
"laravel-echo": "^2.5.0",
|
|
45
|
+
"laravel-echo-v1": "npm:laravel-echo@^1.10.0",
|
|
46
|
+
"mock-socket": "^9.3.1",
|
|
47
|
+
"rollup": "^4.64.0",
|
|
58
48
|
"standard-version": "^8.0.1",
|
|
59
|
-
"ts-jest": "^
|
|
60
|
-
"tslib": "^
|
|
61
|
-
"typescript": "^5.
|
|
49
|
+
"ts-jest": "^29.4.14",
|
|
50
|
+
"tslib": "^2.8.1",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
62
52
|
},
|
|
63
53
|
"engines": {
|
|
64
|
-
"node": ">=
|
|
54
|
+
"node": ">=18"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"laravel-echo": "^1.10.0 || ^2.0.0"
|
|
65
58
|
}
|
|
66
59
|
}
|
package/phpstan.neon.dist
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
includes:
|
|
2
|
-
- ./vendor/
|
|
2
|
+
- ./vendor/larastan/larastan/extension.neon
|
|
3
3
|
|
|
4
4
|
parameters:
|
|
5
5
|
|
|
6
|
-
phpVersion: 80399
|
|
7
|
-
|
|
8
6
|
paths:
|
|
9
7
|
- src
|
|
10
|
-
- tests
|
|
11
8
|
|
|
12
|
-
level:
|
|
9
|
+
level: 6
|
|
13
10
|
|
|
14
11
|
ignoreErrors:
|
|
15
|
-
-
|
|
16
|
-
- '#^Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage::withArgs\(\).$#'
|
|
17
|
-
#
|
|
18
|
-
# excludePaths:
|
|
19
|
-
# - ./*/*/FileToBeExcluded.php
|
|
20
|
-
|
|
21
|
-
checkMissingIterableValueType: false
|
|
12
|
+
- identifier: missingType.iterableValue
|
package/phpunit.xml
CHANGED