mockrtc 0.1.0 → 0.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/README.md +2 -2
- package/dist/client/mockrtc-admin-request-builder.d.ts +21 -0
- package/dist/client/mockrtc-admin-request-builder.js +171 -0
- package/dist/client/mockrtc-admin-request-builder.js.map +1 -0
- package/dist/client/mockrtc-client.d.ts +5 -1
- package/dist/client/mockrtc-client.js +20 -16
- package/dist/client/mockrtc-client.js.map +1 -1
- package/dist/client/mockrtc-remote-peer.d.ts +3 -2
- package/dist/client/mockrtc-remote-peer.js.map +1 -1
- package/dist/handling/handler-step-definitions.d.ts +3 -2
- package/dist/handling/handler-step-definitions.js.map +1 -1
- package/dist/handling/handler-steps.js +1 -1
- package/dist/handling/handler-steps.js.map +1 -1
- package/dist/main-browser.d.ts +2 -0
- package/dist/main-browser.js +5 -1
- package/dist/main-browser.js.map +1 -1
- package/dist/main.d.ts +5 -4
- package/dist/main.js +5 -1
- package/dist/main.js.map +1 -1
- package/dist/mockrtc-peer.d.ts +37 -6
- package/dist/mockrtc.d.ts +138 -1
- package/dist/mockrtc.js +1 -0
- package/dist/mockrtc.js.map +1 -1
- package/dist/server/mockrtc-admin-plugin.d.ts +2 -2
- package/dist/server/mockrtc-admin-plugin.js +147 -2
- package/dist/server/mockrtc-admin-plugin.js.map +1 -1
- package/dist/server/mockrtc-server-peer.d.ts +8 -2
- package/dist/server/mockrtc-server-peer.js +106 -5
- package/dist/server/mockrtc-server-peer.js.map +1 -1
- package/dist/server/mockrtc-server.d.ts +11 -3
- package/dist/server/mockrtc-server.js +44 -6
- package/dist/server/mockrtc-server.js.map +1 -1
- package/dist/webrtc/datachannel-stream.d.ts +2 -0
- package/dist/webrtc/datachannel-stream.js +12 -0
- package/dist/webrtc/datachannel-stream.js.map +1 -1
- package/dist/webrtc/mediatrack-stream.d.ts +4 -0
- package/dist/webrtc/mediatrack-stream.js +13 -1
- package/dist/webrtc/mediatrack-stream.js.map +1 -1
- package/dist/webrtc/mockrtc-connection.d.ts +1 -1
- package/dist/webrtc/mockrtc-connection.js +77 -60
- package/dist/webrtc/mockrtc-connection.js.map +1 -1
- package/dist/webrtc/rtc-connection.d.ts +23 -4
- package/dist/webrtc/rtc-connection.js +45 -6
- package/dist/webrtc/rtc-connection.js.map +1 -1
- package/dist/webrtc-hooks.js +4 -2
- package/dist/webrtc-hooks.js.map +1 -1
- package/package.json +12 -6
- package/src/client/mockrtc-admin-request-builder.ts +184 -0
- package/src/client/mockrtc-client.ts +26 -22
- package/src/client/mockrtc-remote-peer.ts +9 -8
- package/src/handling/handler-step-definitions.ts +6 -4
- package/src/handling/handler-steps.ts +6 -5
- package/src/main-browser.ts +4 -0
- package/src/main.ts +18 -4
- package/src/mockrtc-peer.ts +41 -6
- package/src/mockrtc.ts +163 -1
- package/src/server/mockrtc-admin-plugin.ts +159 -6
- package/src/server/mockrtc-server-peer.ts +183 -6
- package/src/server/mockrtc-server.ts +75 -14
- package/src/webrtc/datachannel-stream.ts +16 -0
- package/src/webrtc/mediatrack-stream.ts +16 -3
- package/src/webrtc/mockrtc-connection.ts +21 -6
- package/src/webrtc/rtc-connection.ts +79 -14
- package/src/webrtc-hooks.ts +7 -4
- package/test/integration/events.spec.ts +536 -0
- package/test/integration/matching.spec.ts +68 -0
- package/test/integration/proxy.spec.ts +66 -1
- package/test/test-setup.ts +19 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MockRTCEventData } from '../../src/mockrtc';
|
|
7
|
+
import {
|
|
8
|
+
MockRTC,
|
|
9
|
+
expect,
|
|
10
|
+
getDeferred,
|
|
11
|
+
waitForState,
|
|
12
|
+
delay
|
|
13
|
+
} from '../test-setup';
|
|
14
|
+
|
|
15
|
+
describe("MockRTC event subscriptions", function () {
|
|
16
|
+
|
|
17
|
+
const mockRTC = MockRTC.getRemote();
|
|
18
|
+
|
|
19
|
+
beforeEach(() => mockRTC.start());
|
|
20
|
+
afterEach(() => mockRTC.stop());
|
|
21
|
+
|
|
22
|
+
describe("for connection events", function () {
|
|
23
|
+
|
|
24
|
+
it("should fire an event when a mock peer connects", async () => {
|
|
25
|
+
const eventPromise = getDeferred<MockRTCEventData['peer-connected']>();
|
|
26
|
+
|
|
27
|
+
mockRTC.on('peer-connected', (peer) => eventPromise.resolve(peer));
|
|
28
|
+
|
|
29
|
+
const mockPeer = await mockRTC.buildPeer().waitForNextMessage().thenSend('Goodbye');
|
|
30
|
+
|
|
31
|
+
const localConnection = new RTCPeerConnection();
|
|
32
|
+
|
|
33
|
+
const { offer, setAnswer } = await mockPeer.createOffer({
|
|
34
|
+
connectionMetadata: {
|
|
35
|
+
userAgent: navigator.userAgent
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
await localConnection.setRemoteDescription(offer);
|
|
39
|
+
|
|
40
|
+
const localAnswer = await localConnection.createAnswer();
|
|
41
|
+
await localConnection.setLocalDescription(localAnswer);
|
|
42
|
+
await setAnswer(localAnswer);
|
|
43
|
+
|
|
44
|
+
// Wait until the connection opens successfully:
|
|
45
|
+
await waitForState(localConnection, 'connected');
|
|
46
|
+
|
|
47
|
+
const connectionEvent = await eventPromise;
|
|
48
|
+
expect(connectionEvent.peerId).to.equal(mockPeer.peerId);
|
|
49
|
+
expect(connectionEvent.sessionId).not.to.equal(undefined);
|
|
50
|
+
expect(connectionEvent.localSessionDescription.type).to.equal('offer');
|
|
51
|
+
expect(connectionEvent.localSessionDescription.sdp!.length).to.be.greaterThan(10);
|
|
52
|
+
expect(connectionEvent.remoteSessionDescription.type).to.equal('answer');
|
|
53
|
+
expect(connectionEvent.remoteSessionDescription.sdp!.length).to.be.greaterThan(10);
|
|
54
|
+
|
|
55
|
+
expect(connectionEvent.timingEvents.startTime).to.be.lessThanOrEqual(Date.now());
|
|
56
|
+
expect(connectionEvent.timingEvents.connectTimestamp).to.be.greaterThan(0);
|
|
57
|
+
expect(connectionEvent.timingEvents.externalAttachTimestamp).to.equal(undefined);
|
|
58
|
+
expect(connectionEvent.timingEvents.disconnectTimestamp).to.equal(undefined);
|
|
59
|
+
|
|
60
|
+
expect(connectionEvent.metadata.userAgent).to.equal(navigator.userAgent);
|
|
61
|
+
|
|
62
|
+
const { selectedLocalCandidate, selectedRemoteCandidate } = connectionEvent;
|
|
63
|
+
[selectedLocalCandidate, selectedRemoteCandidate].forEach((candidate) => {
|
|
64
|
+
expect(candidate.address).to.match(/[:\w\.]+/); // IPv4 or 6
|
|
65
|
+
expect(candidate.port).to.be.greaterThan(0);
|
|
66
|
+
expect(candidate.protocol).to.equal('udp');
|
|
67
|
+
expect(candidate.type).not.to.equal(undefined);
|
|
68
|
+
});
|
|
69
|
+
expect(selectedLocalCandidate.port).to.not.equal(selectedRemoteCandidate.port);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should not fire an event when an external peer connects", async () => {
|
|
73
|
+
const eventPromise = getDeferred<MockRTCEventData['peer-connected']>();
|
|
74
|
+
|
|
75
|
+
mockRTC.on('peer-connected', (peer) => eventPromise.resolve(peer));
|
|
76
|
+
|
|
77
|
+
const mockPeer = await mockRTC.buildPeer().waitForNextMessage().thenSend('Goodbye');
|
|
78
|
+
|
|
79
|
+
const localConnection = new RTCPeerConnection();
|
|
80
|
+
|
|
81
|
+
const { offer, setAnswer } = await mockPeer.createExternalOffer();
|
|
82
|
+
await localConnection.setRemoteDescription(offer);
|
|
83
|
+
|
|
84
|
+
const localAnswer = await localConnection.createAnswer();
|
|
85
|
+
await localConnection.setLocalDescription(localAnswer);
|
|
86
|
+
await setAnswer(localAnswer);
|
|
87
|
+
|
|
88
|
+
// Wait until the connection opens successfully:
|
|
89
|
+
await waitForState(localConnection, 'connected');
|
|
90
|
+
|
|
91
|
+
const result = await Promise.race([
|
|
92
|
+
delay(500).then(() => 'timeout'),
|
|
93
|
+
eventPromise
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
// No event fires within 500ms
|
|
97
|
+
expect(result).to.equal('timeout');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should fire an event when an external peer is attached", async () => {
|
|
101
|
+
const eventPromise = getDeferred<MockRTCEventData['external-peer-attached']>();
|
|
102
|
+
|
|
103
|
+
mockRTC.on('external-peer-attached', (peer) => eventPromise.resolve(peer));
|
|
104
|
+
|
|
105
|
+
const mockPeer = await mockRTC.buildPeer().waitForNextMessage().thenSend('Goodbye');
|
|
106
|
+
|
|
107
|
+
// Hook the local connection (so traffic is redirected via an external peer)
|
|
108
|
+
const localConnection = new RTCPeerConnection();
|
|
109
|
+
MockRTC.hookWebRTCConnection(localConnection, mockPeer);
|
|
110
|
+
|
|
111
|
+
// Create and connect an unhooked remote connection:
|
|
112
|
+
const remoteConn = new RTCPeerConnection();
|
|
113
|
+
remoteConn.createDataChannel("test-channel");
|
|
114
|
+
const remoteOffer = await remoteConn.createOffer();
|
|
115
|
+
remoteConn.setLocalDescription(remoteOffer);
|
|
116
|
+
await localConnection.setRemoteDescription(remoteOffer);
|
|
117
|
+
const localAnswer = await localConnection.createAnswer();
|
|
118
|
+
localConnection.setLocalDescription(localAnswer);
|
|
119
|
+
remoteConn.setRemoteDescription(localAnswer);
|
|
120
|
+
|
|
121
|
+
// Wait until the connection opens successfully:
|
|
122
|
+
await waitForState(localConnection, 'connected');
|
|
123
|
+
|
|
124
|
+
const attachEvent = await eventPromise;
|
|
125
|
+
expect(attachEvent.peerId).to.equal(mockPeer.peerId);
|
|
126
|
+
expect(attachEvent.sessionId).not.to.equal(undefined);
|
|
127
|
+
|
|
128
|
+
const { externalConnection } = attachEvent;
|
|
129
|
+
expect(externalConnection.sessionId).not.to.equal(attachEvent.sessionId);
|
|
130
|
+
expect(externalConnection.localSessionDescription.type).to.equal('answer');
|
|
131
|
+
expect(externalConnection.localSessionDescription.sdp!.length).to.be.greaterThan(10);
|
|
132
|
+
expect(externalConnection.remoteSessionDescription.type).to.equal('offer');
|
|
133
|
+
expect(externalConnection.remoteSessionDescription.sdp!.length).to.be.greaterThan(10);
|
|
134
|
+
|
|
135
|
+
expect(attachEvent.timingEvents.startTime).to.be.lessThanOrEqual(Date.now());
|
|
136
|
+
expect(attachEvent.timingEvents.connectTimestamp).to.be.greaterThan(0);
|
|
137
|
+
expect(attachEvent.timingEvents.externalAttachTimestamp).to.be.greaterThan(0);
|
|
138
|
+
expect(attachEvent.timingEvents.disconnectTimestamp).to.equal(undefined);
|
|
139
|
+
|
|
140
|
+
const { selectedLocalCandidate, selectedRemoteCandidate } = externalConnection;
|
|
141
|
+
[selectedLocalCandidate, selectedRemoteCandidate].forEach((candidate) => {
|
|
142
|
+
expect(candidate.address).to.match(/[:\w\.]+/); // IPv4 or 6
|
|
143
|
+
expect(candidate.port).to.be.greaterThan(0);
|
|
144
|
+
expect(candidate.protocol).to.equal('udp');
|
|
145
|
+
expect(candidate.type).not.to.equal(undefined);
|
|
146
|
+
});
|
|
147
|
+
expect(selectedLocalCandidate.port).to.not.equal(selectedRemoteCandidate.port);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should fire an event when a mock peer is disconnected by MockRTC", async () => {
|
|
151
|
+
const eventPromise = getDeferred<MockRTCEventData['peer-disconnected']>();
|
|
152
|
+
|
|
153
|
+
mockRTC.on('peer-disconnected', (peer) => eventPromise.resolve(peer));
|
|
154
|
+
|
|
155
|
+
const mockPeer = await mockRTC.buildPeer().thenClose(); // MockRTC closes immediately
|
|
156
|
+
|
|
157
|
+
const localConnection = new RTCPeerConnection();
|
|
158
|
+
|
|
159
|
+
const { offer, setAnswer } = await mockPeer.createOffer();
|
|
160
|
+
await localConnection.setRemoteDescription(offer);
|
|
161
|
+
|
|
162
|
+
const localAnswer = await localConnection.createAnswer();
|
|
163
|
+
await localConnection.setLocalDescription(localAnswer);
|
|
164
|
+
await setAnswer(localAnswer);
|
|
165
|
+
|
|
166
|
+
// Wait until the connection opens successfully:
|
|
167
|
+
await waitForState(localConnection, 'connected');
|
|
168
|
+
|
|
169
|
+
const connectionEvent = await eventPromise;
|
|
170
|
+
expect(connectionEvent.peerId).to.equal(mockPeer.peerId);
|
|
171
|
+
expect(connectionEvent.sessionId).not.to.equal(undefined);
|
|
172
|
+
|
|
173
|
+
expect(connectionEvent.timingEvents.startTime).to.be.lessThanOrEqual(Date.now());
|
|
174
|
+
expect(connectionEvent.timingEvents.connectTimestamp).to.be.greaterThan(0);
|
|
175
|
+
expect(connectionEvent.timingEvents.externalAttachTimestamp).to.equal(undefined);
|
|
176
|
+
expect(connectionEvent.timingEvents.disconnectTimestamp).to.be.greaterThan(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should fire an event when a mock peer is disconnected by the peer", async () => {
|
|
180
|
+
const connectEventPromise = getDeferred<MockRTCEventData['peer-connected']>();
|
|
181
|
+
const disconnectEventPromise = getDeferred<MockRTCEventData['peer-disconnected']>();
|
|
182
|
+
|
|
183
|
+
mockRTC.on('peer-connected', (peer) => connectEventPromise.resolve(peer));
|
|
184
|
+
mockRTC.on('peer-disconnected', (peer) => disconnectEventPromise.resolve(peer));
|
|
185
|
+
|
|
186
|
+
const mockPeer = await mockRTC.buildPeer().thenEcho(); // Stay open indefinitely
|
|
187
|
+
|
|
188
|
+
const localConnection = new RTCPeerConnection();
|
|
189
|
+
|
|
190
|
+
const { offer, setAnswer } = await mockPeer.createOffer();
|
|
191
|
+
await localConnection.setRemoteDescription(offer);
|
|
192
|
+
|
|
193
|
+
const localAnswer = await localConnection.createAnswer();
|
|
194
|
+
await localConnection.setLocalDescription(localAnswer);
|
|
195
|
+
await setAnswer(localAnswer);
|
|
196
|
+
|
|
197
|
+
// Wait until the connection opens successfully. We need to wait until MockRTC is fully
|
|
198
|
+
// aware - if we disconnect before full connection, there are no events at all.
|
|
199
|
+
await connectEventPromise;
|
|
200
|
+
|
|
201
|
+
localConnection.close(); // Explicitly close the local connection
|
|
202
|
+
|
|
203
|
+
const connectionEvent = await disconnectEventPromise;
|
|
204
|
+
expect(connectionEvent.peerId).to.equal(mockPeer.peerId);
|
|
205
|
+
expect(connectionEvent.sessionId).not.to.equal(undefined);
|
|
206
|
+
|
|
207
|
+
expect(connectionEvent.timingEvents.startTime).to.be.lessThanOrEqual(Date.now());
|
|
208
|
+
expect(connectionEvent.timingEvents.connectTimestamp).to.be.greaterThan(0);
|
|
209
|
+
expect(connectionEvent.timingEvents.externalAttachTimestamp).to.equal(undefined);
|
|
210
|
+
expect(connectionEvent.timingEvents.disconnectTimestamp).to.be.greaterThan(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should not fire an event when an external peer disconnects", async () => {
|
|
214
|
+
const eventPromise = getDeferred<MockRTCEventData['peer-disconnected']>();
|
|
215
|
+
|
|
216
|
+
mockRTC.on('peer-disconnected', (peer) => eventPromise.resolve(peer));
|
|
217
|
+
|
|
218
|
+
const mockPeer = await mockRTC.buildPeer().thenClose();
|
|
219
|
+
|
|
220
|
+
const localConnection = new RTCPeerConnection();
|
|
221
|
+
|
|
222
|
+
const { offer, setAnswer } = await mockPeer.createExternalOffer();
|
|
223
|
+
await localConnection.setRemoteDescription(offer);
|
|
224
|
+
|
|
225
|
+
const localAnswer = await localConnection.createAnswer();
|
|
226
|
+
await localConnection.setLocalDescription(localAnswer);
|
|
227
|
+
await setAnswer(localAnswer);
|
|
228
|
+
|
|
229
|
+
// Wait until the connection opens successfully:
|
|
230
|
+
await waitForState(localConnection, 'connected');
|
|
231
|
+
|
|
232
|
+
const result = await Promise.race([
|
|
233
|
+
delay(500).then(() => 'timeout'),
|
|
234
|
+
eventPromise
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
// No event fires within 500ms
|
|
238
|
+
expect(result).to.equal('timeout');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("for data channels", function () {
|
|
244
|
+
|
|
245
|
+
it("fires an event when a data channel is created", async () => {
|
|
246
|
+
const eventPromise = getDeferred<MockRTCEventData['data-channel-opened']>();
|
|
247
|
+
|
|
248
|
+
mockRTC.on('data-channel-opened', (channel) => eventPromise.resolve(channel));
|
|
249
|
+
|
|
250
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
251
|
+
.waitForChannel()
|
|
252
|
+
.thenSend('Test message');
|
|
253
|
+
|
|
254
|
+
const localConnection = new RTCPeerConnection();
|
|
255
|
+
localConnection.createDataChannel("test-channel", {
|
|
256
|
+
protocol: "mockrtc-protocol"
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const localOffer = await localConnection.createOffer();
|
|
260
|
+
await localConnection.setLocalDescription(localOffer);
|
|
261
|
+
const { answer } = await mockPeer.answerOffer(localOffer);
|
|
262
|
+
await localConnection.setRemoteDescription(answer);
|
|
263
|
+
|
|
264
|
+
const channelEvent = await eventPromise;
|
|
265
|
+
expect(channelEvent.peerId).to.equal(mockPeer.peerId);
|
|
266
|
+
expect(channelEvent.sessionId).not.to.equal(undefined);
|
|
267
|
+
expect(channelEvent.channelId).to.equal(1);
|
|
268
|
+
expect(channelEvent.channelLabel).to.equal('test-channel');
|
|
269
|
+
expect(channelEvent.channelProtocol).to.equal("mockrtc-protocol");
|
|
270
|
+
|
|
271
|
+
expect(channelEvent.timingEvents.startTime).to.be.lessThanOrEqual(Date.now());
|
|
272
|
+
expect(channelEvent.timingEvents.connectTimestamp).to.be.greaterThan(0);
|
|
273
|
+
expect(channelEvent.eventTimestamp)
|
|
274
|
+
.to.be.greaterThan(channelEvent.timingEvents.connectTimestamp);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("fires an event when a data channel message is sent", async () => {
|
|
278
|
+
const eventPromise = getDeferred<MockRTCEventData['data-channel-message-sent']>();
|
|
279
|
+
|
|
280
|
+
mockRTC.on('data-channel-message-sent', (message) => eventPromise.resolve(message));
|
|
281
|
+
|
|
282
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
283
|
+
.waitForChannel()
|
|
284
|
+
.thenSend('Test message');
|
|
285
|
+
|
|
286
|
+
const localConnection = new RTCPeerConnection();
|
|
287
|
+
localConnection.createDataChannel("test-channel");
|
|
288
|
+
|
|
289
|
+
const localOffer = await localConnection.createOffer();
|
|
290
|
+
await localConnection.setLocalDescription(localOffer);
|
|
291
|
+
const { answer } = await mockPeer.answerOffer(localOffer);
|
|
292
|
+
await localConnection.setRemoteDescription(answer);
|
|
293
|
+
|
|
294
|
+
const messageEvent = await eventPromise;
|
|
295
|
+
expect(messageEvent.peerId).to.equal(mockPeer.peerId);
|
|
296
|
+
expect(messageEvent.sessionId).not.to.equal(undefined);
|
|
297
|
+
expect(messageEvent.channelId).to.equal(1);
|
|
298
|
+
|
|
299
|
+
expect(messageEvent.direction).to.equal('sent');
|
|
300
|
+
expect(messageEvent.isBinary).to.equal(false);
|
|
301
|
+
expect(messageEvent.content.toString()).to.equal('Test message');
|
|
302
|
+
|
|
303
|
+
expect(messageEvent.timingEvents.startTime).to.be.lessThanOrEqual(Date.now());
|
|
304
|
+
expect(messageEvent.timingEvents.connectTimestamp).to.be.greaterThan(0);
|
|
305
|
+
expect(messageEvent.eventTimestamp)
|
|
306
|
+
.to.be.greaterThan(messageEvent.timingEvents.connectTimestamp);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("fires an event when a data channel message is received", async () => {
|
|
310
|
+
const eventPromise = getDeferred<MockRTCEventData['data-channel-message-received']>();
|
|
311
|
+
|
|
312
|
+
mockRTC.on('data-channel-message-received', (message) => eventPromise.resolve(message));
|
|
313
|
+
|
|
314
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
315
|
+
.waitForChannel()
|
|
316
|
+
.send('Outgoing message')
|
|
317
|
+
.waitForNextMessage()
|
|
318
|
+
.thenClose();
|
|
319
|
+
|
|
320
|
+
const localConnection = new RTCPeerConnection();
|
|
321
|
+
const dataChannel = localConnection.createDataChannel("test-channel");
|
|
322
|
+
|
|
323
|
+
// Send a message to MockRTC once the connection opens:
|
|
324
|
+
dataChannel.addEventListener('open', () => {
|
|
325
|
+
dataChannel.send(
|
|
326
|
+
Buffer.from('Technically binary message from client')
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const localOffer = await localConnection.createOffer();
|
|
331
|
+
await localConnection.setLocalDescription(localOffer);
|
|
332
|
+
const { answer } = await mockPeer.answerOffer(localOffer);
|
|
333
|
+
await localConnection.setRemoteDescription(answer);
|
|
334
|
+
|
|
335
|
+
const messageEvent = await eventPromise;
|
|
336
|
+
expect(messageEvent.peerId).to.equal(mockPeer.peerId);
|
|
337
|
+
expect(messageEvent.sessionId).not.to.equal(undefined);
|
|
338
|
+
expect(messageEvent.channelId).to.equal(1);
|
|
339
|
+
|
|
340
|
+
expect(messageEvent.direction).to.equal('received');
|
|
341
|
+
expect(messageEvent.isBinary).to.equal(true);
|
|
342
|
+
expect(messageEvent.content.toString()).to.equal('Technically binary message from client');
|
|
343
|
+
|
|
344
|
+
expect(messageEvent.timingEvents.startTime).to.be.lessThanOrEqual(Date.now());
|
|
345
|
+
expect(messageEvent.timingEvents.connectTimestamp).to.be.greaterThan(0);
|
|
346
|
+
expect(messageEvent.eventTimestamp)
|
|
347
|
+
.to.be.greaterThan(messageEvent.timingEvents.connectTimestamp);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("fires an event when a data channel is closed", async () => {
|
|
351
|
+
const eventPromise = getDeferred<MockRTCEventData['data-channel-closed']>();
|
|
352
|
+
|
|
353
|
+
mockRTC.on('data-channel-closed', (channel) => eventPromise.resolve(channel));
|
|
354
|
+
|
|
355
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
356
|
+
.waitForChannel()
|
|
357
|
+
.thenClose();
|
|
358
|
+
|
|
359
|
+
const localConnection = new RTCPeerConnection();
|
|
360
|
+
const dataChannel = localConnection.createDataChannel("test-channel");
|
|
361
|
+
|
|
362
|
+
const localOffer = await localConnection.createOffer();
|
|
363
|
+
await localConnection.setLocalDescription(localOffer);
|
|
364
|
+
const { answer } = await mockPeer.answerOffer(localOffer);
|
|
365
|
+
await localConnection.setRemoteDescription(answer);
|
|
366
|
+
|
|
367
|
+
dataChannel.addEventListener('open', () => dataChannel.close());
|
|
368
|
+
|
|
369
|
+
const channelEvent = await eventPromise;
|
|
370
|
+
expect(channelEvent.peerId).to.equal(mockPeer.peerId);
|
|
371
|
+
expect(channelEvent.sessionId).not.to.equal(undefined);
|
|
372
|
+
expect(channelEvent.channelId).to.equal(1);
|
|
373
|
+
|
|
374
|
+
expect(channelEvent.timingEvents.startTime).to.be.lessThanOrEqual(Date.now());
|
|
375
|
+
expect(channelEvent.timingEvents.connectTimestamp).to.be.greaterThan(0);
|
|
376
|
+
expect(channelEvent.eventTimestamp)
|
|
377
|
+
.to.be.greaterThan(channelEvent.timingEvents.connectTimestamp);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("does not fire any events for external connection data channels", async () => {
|
|
381
|
+
const eventPromises = ([
|
|
382
|
+
'data-channel-opened',
|
|
383
|
+
'data-channel-message-sent',
|
|
384
|
+
'data-channel-message-received',
|
|
385
|
+
'data-channel-closed'
|
|
386
|
+
] as const).map((eventName) => {
|
|
387
|
+
const eventPromise = getDeferred<MockRTCEventData[typeof eventName]>();
|
|
388
|
+
mockRTC.on(eventName, (message) => eventPromise.resolve(message));
|
|
389
|
+
return eventPromise;
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
393
|
+
.waitForChannel()
|
|
394
|
+
.send('Outgoing message')
|
|
395
|
+
.waitForNextMessage()
|
|
396
|
+
.thenClose();
|
|
397
|
+
|
|
398
|
+
const localConnection = new RTCPeerConnection();
|
|
399
|
+
const dataChannel = localConnection.createDataChannel("test-channel");
|
|
400
|
+
|
|
401
|
+
// Send a message to MockRTC once the connection opens:
|
|
402
|
+
dataChannel.addEventListener('open', () => {
|
|
403
|
+
dataChannel.send(
|
|
404
|
+
Buffer.from('Technically binary message from client')
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const localOffer = await localConnection.createOffer();
|
|
409
|
+
await localConnection.setLocalDescription(localOffer);
|
|
410
|
+
const { answer } = await mockPeer.answerExternalOffer(localOffer); // <-- External
|
|
411
|
+
await localConnection.setRemoteDescription(answer);
|
|
412
|
+
|
|
413
|
+
const result = await Promise.race([
|
|
414
|
+
delay(500).then(() => 'timeout'),
|
|
415
|
+
...eventPromises
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
// No event fires within 500ms
|
|
419
|
+
expect(result).to.equal('timeout');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe("for media tracks", function () {
|
|
425
|
+
|
|
426
|
+
it("fires an event when a media track is created", async () => {
|
|
427
|
+
const eventPromise = getDeferred<MockRTCEventData['media-track-opened']>();
|
|
428
|
+
|
|
429
|
+
mockRTC.on('media-track-opened', (track) => eventPromise.resolve(track));
|
|
430
|
+
|
|
431
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
432
|
+
.waitForNextMedia()
|
|
433
|
+
.thenClose();
|
|
434
|
+
|
|
435
|
+
const localConnection = new RTCPeerConnection();
|
|
436
|
+
|
|
437
|
+
const localOffer = await localConnection.createOffer({
|
|
438
|
+
offerToReceiveAudio: true
|
|
439
|
+
});
|
|
440
|
+
await localConnection.setLocalDescription(localOffer);
|
|
441
|
+
const { answer } = await mockPeer.answerOffer(localOffer);
|
|
442
|
+
await localConnection.setRemoteDescription(answer);
|
|
443
|
+
|
|
444
|
+
const trackEvent = await eventPromise;
|
|
445
|
+
expect(trackEvent.peerId).to.equal(mockPeer.peerId);
|
|
446
|
+
expect(trackEvent.sessionId).not.to.equal(undefined);
|
|
447
|
+
expect(trackEvent.trackMid).to.equal("0");
|
|
448
|
+
expect(trackEvent.trackDirection).to.equal("SendOnly");
|
|
449
|
+
expect(trackEvent.trackType).to.equal("audio");
|
|
450
|
+
|
|
451
|
+
expect(trackEvent.timingEvents.startTime).to.be.lessThanOrEqual(Date.now());
|
|
452
|
+
expect(trackEvent.timingEvents.connectTimestamp).to.be.greaterThan(0);
|
|
453
|
+
expect(trackEvent.eventTimestamp)
|
|
454
|
+
.to.be.greaterThan(trackEvent.timingEvents.connectTimestamp);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("fires an event when a media track is closed", async () => {
|
|
458
|
+
const eventPromise = getDeferred<MockRTCEventData['media-track-closed']>();
|
|
459
|
+
|
|
460
|
+
mockRTC.on('media-track-closed', (track) => eventPromise.resolve(track));
|
|
461
|
+
|
|
462
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
463
|
+
.thenEcho();
|
|
464
|
+
|
|
465
|
+
const localConnection = new RTCPeerConnection();
|
|
466
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
467
|
+
stream.getTracks().forEach((track) => localConnection.addTrack(track, stream));
|
|
468
|
+
|
|
469
|
+
const localOffer = await localConnection.createOffer();
|
|
470
|
+
await localConnection.setLocalDescription(localOffer);
|
|
471
|
+
const { answer } = await mockPeer.answerOffer(localOffer);
|
|
472
|
+
await localConnection.setRemoteDescription(answer);
|
|
473
|
+
|
|
474
|
+
await waitForState(localConnection, 'connected');
|
|
475
|
+
|
|
476
|
+
// Close the connection entirely, implicitly closing the media tracks:
|
|
477
|
+
localConnection.close();
|
|
478
|
+
// (renegotiating doesn't work - browsers keep the stream but updates direction to 'inactive')
|
|
479
|
+
|
|
480
|
+
const trackEvent = await eventPromise;
|
|
481
|
+
expect(trackEvent.peerId).to.equal(mockPeer.peerId);
|
|
482
|
+
expect(trackEvent.sessionId).not.to.equal(undefined);
|
|
483
|
+
expect(trackEvent.trackMid).to.equal("0");
|
|
484
|
+
|
|
485
|
+
expect(trackEvent.timingEvents.startTime).to.be.lessThanOrEqual(Date.now());
|
|
486
|
+
expect(trackEvent.timingEvents.connectTimestamp).to.be.greaterThan(0);
|
|
487
|
+
expect(trackEvent.eventTimestamp)
|
|
488
|
+
.to.be.greaterThan(trackEvent.timingEvents.connectTimestamp);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("should fire media stats events whilst the connection is open", async function () {
|
|
492
|
+
this.timeout(5000);
|
|
493
|
+
|
|
494
|
+
const receivedStats: Array<MockRTCEventData['media-track-stats']> = [];
|
|
495
|
+
mockRTC.on('media-track-stats', (stats) => receivedStats.push(stats));
|
|
496
|
+
|
|
497
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
498
|
+
.sleep(10000)
|
|
499
|
+
.thenClose();
|
|
500
|
+
|
|
501
|
+
const localConnection = new RTCPeerConnection();
|
|
502
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
503
|
+
stream.getTracks().forEach((track) => localConnection.addTrack(track, stream));
|
|
504
|
+
|
|
505
|
+
const localOffer = await localConnection.createOffer();
|
|
506
|
+
await localConnection.setLocalDescription(localOffer);
|
|
507
|
+
const { answer, session } = await mockPeer.answerOffer(await localOffer);
|
|
508
|
+
await localConnection.setRemoteDescription(answer);
|
|
509
|
+
|
|
510
|
+
await waitForState(localConnection, 'connected');
|
|
511
|
+
await delay(2500); // Stats fires every 1s
|
|
512
|
+
|
|
513
|
+
expect(receivedStats.length).to.be.greaterThanOrEqual(2);
|
|
514
|
+
receivedStats.forEach((stats) => {
|
|
515
|
+
expect(stats.peerId).to.equal(mockPeer.peerId);
|
|
516
|
+
expect(stats.sessionId).to.equal(session.sessionId);
|
|
517
|
+
expect(stats.trackMid).to.equal("0");
|
|
518
|
+
|
|
519
|
+
expect(stats.timingEvents.startTime).to.be.lessThanOrEqual(Date.now());
|
|
520
|
+
expect(stats.timingEvents.connectTimestamp).to.be.greaterThan(0);
|
|
521
|
+
expect(stats.eventTimestamp)
|
|
522
|
+
.to.be.greaterThan(stats.timingEvents.connectTimestamp);
|
|
523
|
+
});
|
|
524
|
+
const [firstStats, secondStats] = receivedStats;
|
|
525
|
+
|
|
526
|
+
expect(firstStats.totalBytesReceived).to.be.greaterThan(1);
|
|
527
|
+
expect(firstStats.totalBytesSent).to.equal(0);
|
|
528
|
+
|
|
529
|
+
expect(secondStats.totalBytesReceived).to.be.greaterThan(firstStats.totalBytesReceived);
|
|
530
|
+
expect(secondStats.totalBytesSent).to.equal(0);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MockRTC,
|
|
3
|
+
expect,
|
|
4
|
+
delay
|
|
5
|
+
} from '../test-setup';
|
|
6
|
+
|
|
7
|
+
describe("Connection rule matching", () => {
|
|
8
|
+
|
|
9
|
+
const mockRTC = MockRTC.getRemote({ recordMessages: true });
|
|
10
|
+
|
|
11
|
+
beforeEach(() => mockRTC.start());
|
|
12
|
+
afterEach(() => mockRTC.stop());
|
|
13
|
+
|
|
14
|
+
it("by default, matches and proxies all connections", async () => {
|
|
15
|
+
const remoteConn = new RTCPeerConnection();
|
|
16
|
+
const remotelyReceivedMessages: Array<string | Buffer> = [];
|
|
17
|
+
|
|
18
|
+
remoteConn.addEventListener('datachannel', ({ channel }) => {
|
|
19
|
+
channel.addEventListener('message', ({ data }) =>
|
|
20
|
+
remotelyReceivedMessages.push(data)
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const matchingPeer = await mockRTC.getMatchingPeer();
|
|
25
|
+
// No rules defined!
|
|
26
|
+
|
|
27
|
+
// Create a local data connection:
|
|
28
|
+
const localConn = new RTCPeerConnection();
|
|
29
|
+
MockRTC.hookWebRTCConnection(localConn, matchingPeer); // Automatically redirect traffic via matchingPeer
|
|
30
|
+
|
|
31
|
+
const dataChannel = localConn.createDataChannel("dataChannel");
|
|
32
|
+
|
|
33
|
+
// Create a local offer (which will be hooked automatically):
|
|
34
|
+
const localOffer = await localConn.createOffer();
|
|
35
|
+
localConn.setLocalDescription(localOffer);
|
|
36
|
+
|
|
37
|
+
// v-- Normally happens remotely, via signalling ---
|
|
38
|
+
remoteConn.setRemoteDescription(localOffer);
|
|
39
|
+
const remoteAnswer = await remoteConn.createAnswer();
|
|
40
|
+
remoteConn.setLocalDescription(remoteAnswer);
|
|
41
|
+
// ^-- Normally happens remotely, via signalling ---
|
|
42
|
+
|
|
43
|
+
// Accept the real remote answer, and start communicating:
|
|
44
|
+
localConn.setRemoteDescription(remoteAnswer);
|
|
45
|
+
|
|
46
|
+
await new Promise<void>((resolve) => dataChannel.onopen = () => resolve());
|
|
47
|
+
|
|
48
|
+
dataChannel.send('local message 1');
|
|
49
|
+
dataChannel.send('local message 2');
|
|
50
|
+
dataChannel.send('local message 3');
|
|
51
|
+
|
|
52
|
+
await delay(100);
|
|
53
|
+
|
|
54
|
+
// Traffic is passed through untouched, as expected:
|
|
55
|
+
expect(remotelyReceivedMessages).to.deep.equal([
|
|
56
|
+
'local message 1',
|
|
57
|
+
'local message 2',
|
|
58
|
+
'local message 3'
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
// But does go through the proxy:
|
|
62
|
+
expect(await matchingPeer.getAllMessages()).to.deep.equal([
|
|
63
|
+
'local message 1',
|
|
64
|
+
'local message 2',
|
|
65
|
+
'local message 3',
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { MockRTCEventData } from '../../src/mockrtc';
|
|
6
7
|
import {
|
|
7
8
|
MockRTC,
|
|
8
9
|
expect,
|
|
9
10
|
waitForChannelOpen,
|
|
10
11
|
waitForChannelClose,
|
|
11
12
|
waitForState,
|
|
12
|
-
setupPerfectNegotiation
|
|
13
|
+
setupPerfectNegotiation,
|
|
14
|
+
getDeferred
|
|
13
15
|
} from '../test-setup';
|
|
14
16
|
|
|
15
17
|
describe("When proxying WebRTC traffic", () => {
|
|
@@ -523,4 +525,67 @@ describe("When proxying WebRTC traffic", () => {
|
|
|
523
525
|
expect(remoteFrame!.displayWidth).to.be.greaterThanOrEqual(320);
|
|
524
526
|
});
|
|
525
527
|
|
|
528
|
+
it("should include user-agent metadata when creating a hooked offer", async () => {
|
|
529
|
+
const eventPromise = getDeferred<MockRTCEventData['peer-connected']>();
|
|
530
|
+
mockRTC.on('peer-connected', (peer) => eventPromise.resolve(peer));
|
|
531
|
+
|
|
532
|
+
const remoteConn = new RTCPeerConnection();
|
|
533
|
+
|
|
534
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
535
|
+
.thenClose();
|
|
536
|
+
|
|
537
|
+
// Create a local data connection:
|
|
538
|
+
const localConn = new RTCPeerConnection();
|
|
539
|
+
MockRTC.hookWebRTCConnection(localConn, mockPeer); // Automatically redirect traffic via mockPeer
|
|
540
|
+
localConn.createDataChannel("dataChannel");
|
|
541
|
+
|
|
542
|
+
// Create a local offer (which will be hooked automatically):
|
|
543
|
+
const localOffer = await localConn.createOffer();
|
|
544
|
+
localConn.setLocalDescription(localOffer);
|
|
545
|
+
|
|
546
|
+
// v-- Normally happens remotely, via signalling ---
|
|
547
|
+
remoteConn.setRemoteDescription(localOffer);
|
|
548
|
+
const remoteAnswer = await remoteConn.createAnswer();
|
|
549
|
+
remoteConn.setLocalDescription(remoteAnswer);
|
|
550
|
+
// ^-- Normally happens remotely, via signalling ---
|
|
551
|
+
|
|
552
|
+
// Accept the real remote answer, and start communicating:
|
|
553
|
+
localConn.setRemoteDescription(remoteAnswer);
|
|
554
|
+
|
|
555
|
+
// Check that the connection event automatically includes the user agent:
|
|
556
|
+
const connectEvent = await eventPromise;
|
|
557
|
+
expect(connectEvent.metadata.userAgent).to.equal(navigator.userAgent);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("should include user-agent metadata when creating a hooked answer", async () => {
|
|
561
|
+
const eventPromise = getDeferred<MockRTCEventData['peer-connected']>();
|
|
562
|
+
mockRTC.on('peer-connected', (peer) => eventPromise.resolve(peer));
|
|
563
|
+
|
|
564
|
+
const remoteConn = new RTCPeerConnection();
|
|
565
|
+
remoteConn.createDataChannel("empty-channel"); // Need at least one channel/track to get an offer
|
|
566
|
+
|
|
567
|
+
const mockPeer = await mockRTC.buildPeer()
|
|
568
|
+
.thenClose();
|
|
569
|
+
|
|
570
|
+
// Remote connection starts first, sending us a real offer:
|
|
571
|
+
const remoteOffer = await remoteConn.createOffer();
|
|
572
|
+
remoteConn.setLocalDescription(remoteOffer);
|
|
573
|
+
|
|
574
|
+
// Create a local data connection:
|
|
575
|
+
const localConn = new RTCPeerConnection();
|
|
576
|
+
MockRTC.hookWebRTCConnection(localConn, mockPeer); // Automatically redirect traffic via mockPeer
|
|
577
|
+
|
|
578
|
+
// Receive the remote offer, use that locally to create an answer (this is all hooked):
|
|
579
|
+
await localConn.setRemoteDescription(remoteOffer);
|
|
580
|
+
const localAnswer = await localConn.createAnswer();
|
|
581
|
+
localConn.setLocalDescription(localAnswer);
|
|
582
|
+
|
|
583
|
+
// Signal the answer back to the real unhooked remote connection:
|
|
584
|
+
await remoteConn.setRemoteDescription(localAnswer);
|
|
585
|
+
|
|
586
|
+
// Check that the connection event automatically includes the user agent:
|
|
587
|
+
const connectEvent = await eventPromise;
|
|
588
|
+
expect(connectEvent.metadata.userAgent).to.equal(navigator.userAgent);
|
|
589
|
+
});
|
|
590
|
+
|
|
526
591
|
});
|