mockrtc 0.1.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/.github/workflows/ci.yml +29 -0
- package/LICENSE +201 -0
- package/README.md +290 -0
- package/dist/admin-bin.d.ts +2 -0
- package/dist/admin-bin.js +67 -0
- package/dist/admin-bin.js.map +1 -0
- package/dist/client/mockrtc-client.d.ts +12 -0
- package/dist/client/mockrtc-client.js +67 -0
- package/dist/client/mockrtc-client.js.map +1 -0
- package/dist/client/mockrtc-remote-peer.d.ts +15 -0
- package/dist/client/mockrtc-remote-peer.js +246 -0
- package/dist/client/mockrtc-remote-peer.js.map +1 -0
- package/dist/control-channel.d.ts +8 -0
- package/dist/control-channel.js +11 -0
- package/dist/control-channel.js.map +1 -0
- package/dist/handling/handler-builder.d.ts +138 -0
- package/dist/handling/handler-builder.js +164 -0
- package/dist/handling/handler-builder.js.map +1 -0
- package/dist/handling/handler-step-definitions.d.ts +63 -0
- package/dist/handling/handler-step-definitions.js +123 -0
- package/dist/handling/handler-step-definitions.js.map +1 -0
- package/dist/handling/handler-steps.d.ts +48 -0
- package/dist/handling/handler-steps.js +218 -0
- package/dist/handling/handler-steps.js.map +1 -0
- package/dist/main-browser.d.ts +9 -0
- package/dist/main-browser.js +26 -0
- package/dist/main-browser.js.map +1 -0
- package/dist/main.d.ts +58 -0
- package/dist/main.js +67 -0
- package/dist/main.js.map +1 -0
- package/dist/mockrtc-admin-plugin.d.ts +56 -0
- package/dist/mockrtc-admin-plugin.js +151 -0
- package/dist/mockrtc-admin-plugin.js.map +1 -0
- package/dist/mockrtc-admin-server.d.ts +7 -0
- package/dist/mockrtc-admin-server.js +18 -0
- package/dist/mockrtc-admin-server.js.map +1 -0
- package/dist/mockrtc-client.d.ts +12 -0
- package/dist/mockrtc-client.js +64 -0
- package/dist/mockrtc-client.js.map +1 -0
- package/dist/mockrtc-handler-builder.d.ts +15 -0
- package/dist/mockrtc-handler-builder.js +24 -0
- package/dist/mockrtc-handler-builder.js.map +1 -0
- package/dist/mockrtc-peer.d.ts +147 -0
- package/dist/mockrtc-peer.js +7 -0
- package/dist/mockrtc-peer.js.map +1 -0
- package/dist/mockrtc-remote-peer.d.ts +15 -0
- package/dist/mockrtc-remote-peer.js +234 -0
- package/dist/mockrtc-remote-peer.js.map +1 -0
- package/dist/mockrtc-server-peer.d.ts +29 -0
- package/dist/mockrtc-server-peer.js +145 -0
- package/dist/mockrtc-server-peer.js.map +1 -0
- package/dist/mockrtc-server.d.ts +14 -0
- package/dist/mockrtc-server.js +53 -0
- package/dist/mockrtc-server.js.map +1 -0
- package/dist/mockrtc.d.ts +25 -0
- package/dist/mockrtc.js +7 -0
- package/dist/mockrtc.js.map +1 -0
- package/dist/package.json +52 -0
- package/dist/server/mockrtc-admin-plugin.d.ts +17 -0
- package/dist/server/mockrtc-admin-plugin.js +163 -0
- package/dist/server/mockrtc-admin-plugin.js.map +1 -0
- package/dist/server/mockrtc-admin-server.d.ts +7 -0
- package/dist/server/mockrtc-admin-server.js +18 -0
- package/dist/server/mockrtc-admin-server.js.map +1 -0
- package/dist/server/mockrtc-server-peer.d.ts +24 -0
- package/dist/server/mockrtc-server-peer.js +141 -0
- package/dist/server/mockrtc-server-peer.js.map +1 -0
- package/dist/server/mockrtc-server.d.ts +14 -0
- package/dist/server/mockrtc-server.js +53 -0
- package/dist/server/mockrtc-server.js.map +1 -0
- package/dist/src/main.d.ts +1 -0
- package/dist/src/main.js +24 -0
- package/dist/src/main.js.map +1 -0
- package/dist/src/mockrtc-peer.d.ts +0 -0
- package/dist/src/mockrtc-peer.js +2 -0
- package/dist/src/mockrtc-peer.js.map +1 -0
- package/dist/src/mockrtc.d.ts +0 -0
- package/dist/src/mockrtc.js +65 -0
- package/dist/src/mockrtc.js.map +1 -0
- package/dist/webrtc/control-channel.d.ts +8 -0
- package/dist/webrtc/control-channel.js +11 -0
- package/dist/webrtc/control-channel.js.map +1 -0
- package/dist/webrtc/datachannel-stream.d.ts +25 -0
- package/dist/webrtc/datachannel-stream.js +86 -0
- package/dist/webrtc/datachannel-stream.js.map +1 -0
- package/dist/webrtc/mediatrack-stream.d.ts +29 -0
- package/dist/webrtc/mediatrack-stream.js +109 -0
- package/dist/webrtc/mediatrack-stream.js.map +1 -0
- package/dist/webrtc/mockrtc-connection.d.ts +14 -0
- package/dist/webrtc/mockrtc-connection.js +147 -0
- package/dist/webrtc/mockrtc-connection.js.map +1 -0
- package/dist/webrtc/peer-connection.d.ts +16 -0
- package/dist/webrtc/peer-connection.js +81 -0
- package/dist/webrtc/peer-connection.js.map +1 -0
- package/dist/webrtc/rtc-connection.d.ts +47 -0
- package/dist/webrtc/rtc-connection.js +370 -0
- package/dist/webrtc/rtc-connection.js.map +1 -0
- package/dist/webrtc-hooks.d.ts +30 -0
- package/dist/webrtc-hooks.js +224 -0
- package/dist/webrtc-hooks.js.map +1 -0
- package/karma.conf.ts +89 -0
- package/ngi-eu-footer.png +0 -0
- package/package.json +86 -0
- package/src/admin-bin.ts +57 -0
- package/src/client/mockrtc-client.ts +79 -0
- package/src/client/mockrtc-remote-peer.ts +286 -0
- package/src/handling/handler-builder.ts +215 -0
- package/src/handling/handler-step-definitions.ts +142 -0
- package/src/handling/handler-steps.ts +254 -0
- package/src/main-browser.ts +44 -0
- package/src/main.ts +109 -0
- package/src/mockrtc-peer.ts +176 -0
- package/src/mockrtc.ts +36 -0
- package/src/server/mockrtc-admin-plugin.ts +196 -0
- package/src/server/mockrtc-admin-server.ts +17 -0
- package/src/server/mockrtc-server-peer.ts +159 -0
- package/src/server/mockrtc-server.ts +53 -0
- package/src/webrtc/control-channel.ts +13 -0
- package/src/webrtc/datachannel-stream.ts +102 -0
- package/src/webrtc/mediatrack-stream.ts +135 -0
- package/src/webrtc/mockrtc-connection.ts +164 -0
- package/src/webrtc/rtc-connection.ts +420 -0
- package/src/webrtc-hooks.ts +245 -0
- package/test/integration/close-steps.spec.ts +39 -0
- package/test/integration/connection-setup.spec.ts +230 -0
- package/test/integration/echo-steps.spec.ts +88 -0
- package/test/integration/proxy.spec.ts +526 -0
- package/test/integration/send-steps.spec.ts +76 -0
- package/test/integration/smoke-test.spec.ts +100 -0
- package/test/integration/wait-steps.spec.ts +225 -0
- package/test/start-test-admin-server.ts +12 -0
- package/test/test-setup.ts +136 -0
- package/test/tsconfig.json +11 -0
- package/tsconfig.json +14 -0
- package/typedoc.json +19 -0
- package/wallaby.js +41 -0
package/src/main.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
MockRTC,
|
|
8
|
+
MockRTCOptions,
|
|
9
|
+
MockRTCPeerBuilder
|
|
10
|
+
} from "./mockrtc";
|
|
11
|
+
|
|
12
|
+
import { MockRTCServer } from "./server/mockrtc-server";
|
|
13
|
+
import { MockRTCAdminServer } from "./server/mockrtc-admin-server";
|
|
14
|
+
export { MockRTCAdminPlugin } from "./server/mockrtc-admin-plugin";
|
|
15
|
+
|
|
16
|
+
import { MockRTCClient, MockRTCClientOptions } from "./client/mockrtc-client";
|
|
17
|
+
|
|
18
|
+
// Re-export lots of types are used in various APIs (mostly to make TypeDoc happy):
|
|
19
|
+
export type { HandlerStepDefinition } from "./handling/handler-step-definitions";
|
|
20
|
+
export type { HandlerStep } from "./handling/handler-steps";
|
|
21
|
+
export type { MockRTCHandlerBuilder } from "./handling/handler-builder";
|
|
22
|
+
|
|
23
|
+
export type { MockRTCServerPeer } from "./server/mockrtc-server-peer";
|
|
24
|
+
export type { SessionData } from "./server/mockrtc-admin-plugin";
|
|
25
|
+
|
|
26
|
+
export type { RTCConnection } from "./webrtc/rtc-connection";
|
|
27
|
+
export type { MockRTCConnection } from "./webrtc/mockrtc-connection";
|
|
28
|
+
export type { DataChannelStream } from "./webrtc/datachannel-stream";
|
|
29
|
+
export type { MediaTrackStream } from "./webrtc/mediatrack-stream";
|
|
30
|
+
|
|
31
|
+
export type { PluggableAdmin } from 'mockttp';
|
|
32
|
+
|
|
33
|
+
export type {
|
|
34
|
+
MockRTC,
|
|
35
|
+
MockRTCOptions,
|
|
36
|
+
MockRTCClientOptions,
|
|
37
|
+
MockRTCPeerBuilder,
|
|
38
|
+
MockRTCAdminServer
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type {
|
|
42
|
+
MockRTCPeer,
|
|
43
|
+
MockRTCPeerOptions,
|
|
44
|
+
MockRTCSession,
|
|
45
|
+
MockRTCOfferParams,
|
|
46
|
+
MockRTCAnswerParams,
|
|
47
|
+
MockRTCExternalOfferParams,
|
|
48
|
+
MockRTCExternalAnswerParams,
|
|
49
|
+
OfferOptions,
|
|
50
|
+
AnswerOptions
|
|
51
|
+
} from './mockrtc-peer';
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
MOCKRTC_CONTROL_CHANNEL,
|
|
55
|
+
type MockRTCControlMessage
|
|
56
|
+
} from './webrtc/control-channel';
|
|
57
|
+
export {
|
|
58
|
+
hookWebRTCConnection,
|
|
59
|
+
hookAllWebRTC
|
|
60
|
+
} from "./webrtc-hooks";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get a MockRTC instance on the local machine.
|
|
64
|
+
*
|
|
65
|
+
* In most simple environments, you can call this method directly and immediately
|
|
66
|
+
* get a MockRTC instance and start mocking peers.
|
|
67
|
+
*
|
|
68
|
+
* In node, the mocked peers will run in process and require no further setup.
|
|
69
|
+
*
|
|
70
|
+
* In browsers this is an alias for {@link getRemote}. You'll need to start a MockRTC
|
|
71
|
+
* admin server outside your tests before calling this, which will create and manage
|
|
72
|
+
* your fake peers outside the browser.
|
|
73
|
+
*
|
|
74
|
+
* @category API
|
|
75
|
+
*/
|
|
76
|
+
export function getLocal(): MockRTC {
|
|
77
|
+
return new MockRTCServer();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get a MockRTC instance, managed by a MockRTC admin server running elsewhere.
|
|
82
|
+
*
|
|
83
|
+
* This connects to a MockRTC server, and uses that to start
|
|
84
|
+
* and stop mock peers.
|
|
85
|
+
*
|
|
86
|
+
* @category API
|
|
87
|
+
*/
|
|
88
|
+
export function getRemote(options: MockRTCClientOptions = {}): MockRTC {
|
|
89
|
+
return new MockRTCClient(options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get a MockRTC admin server, which can be used with a MockRTC remote client to create
|
|
94
|
+
* & manage mock peers either from remote machines or from local environments
|
|
95
|
+
* that lack necessary capabilities, e.g. to use MockRTC from inside a browser.
|
|
96
|
+
*
|
|
97
|
+
* This function exists so you can set up these servers programmatically, but for most
|
|
98
|
+
* usage you can just run your tests via the `mockrtc` binary, which will automatically
|
|
99
|
+
* start and stop an admin server for you:
|
|
100
|
+
*
|
|
101
|
+
* ```
|
|
102
|
+
* mockrtc -c <your test command>
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @category API
|
|
106
|
+
*/
|
|
107
|
+
export function getAdminServer(): MockRTCAdminServer {
|
|
108
|
+
return new MockRTCAdminServer();
|
|
109
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface MockRTCPeerOptions {
|
|
7
|
+
recordMessages?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A MockRTC peer represents a target you can connect to, and exposes an API to create
|
|
12
|
+
* offers or answers to create new connections.
|
|
13
|
+
*
|
|
14
|
+
* Peers have defined behaviour, and each connection will be handled accordingly and
|
|
15
|
+
* independently.
|
|
16
|
+
*
|
|
17
|
+
* Peers can also optionally track all the messages and metadata across all their
|
|
18
|
+
* connections.
|
|
19
|
+
*/
|
|
20
|
+
export interface MockRTCPeer {
|
|
21
|
+
readonly peerId: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates an offer for a new connection to this mock peer.
|
|
25
|
+
*
|
|
26
|
+
* Returns a set of offer parameters: the offer itself, a session to renegotiate
|
|
27
|
+
* the connection in future, and a setAnswer callback to call with an answer
|
|
28
|
+
* once you have one.
|
|
29
|
+
*/
|
|
30
|
+
createOffer(options?: OfferOptions): Promise<MockRTCOfferParams>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Takes an offer for a WebRTC connection elsewhere, and creates an answer to
|
|
34
|
+
* connect that to this peer.
|
|
35
|
+
*
|
|
36
|
+
* Returns a set of answer parameters: the answer itself, and a session to renegotiate
|
|
37
|
+
* the connection in future.
|
|
38
|
+
*/
|
|
39
|
+
answerOffer(
|
|
40
|
+
offer: RTCSessionDescriptionInit,
|
|
41
|
+
options?: AnswerOptions
|
|
42
|
+
): Promise<MockRTCAnswerParams>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates an offer for a new external connection to this mock peer.
|
|
46
|
+
*
|
|
47
|
+
* External connections are used for proxying traffic. They do not do anything
|
|
48
|
+
* by default (so they ignore this peer's configured steps) but a mock connection
|
|
49
|
+
* can be connected to an external connection using methods like
|
|
50
|
+
* {@link MockRTCHandlerBuilder.thenPassThrough thenPassThrough}.
|
|
51
|
+
*
|
|
52
|
+
* Returns a set of offer parameters: an external connection id, the offer itself,
|
|
53
|
+
* a session to renegotiate the connection in future, and a setAnswer callback to
|
|
54
|
+
* call with an answer once you have one.
|
|
55
|
+
*/
|
|
56
|
+
createExternalOffer(options?: OfferOptions): Promise<MockRTCExternalOfferParams>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Takes an offer for a WebRTC connection elsewhere, and creates an answer to create
|
|
60
|
+
* an external connection to this peer.
|
|
61
|
+
*
|
|
62
|
+
* External connections are used for proxying traffic. They do not do anything
|
|
63
|
+
* by default (so they ignore this peer's configured steps) but a mock connection
|
|
64
|
+
* can be connected to an external connection using methods like
|
|
65
|
+
* {@link MockRTCHandlerBuilder.thenPassThrough thenPassThrough}.
|
|
66
|
+
*
|
|
67
|
+
* Returns a set of answer parameters: an external connection id, the answer itself,
|
|
68
|
+
* and a session to renegotiate the connection in future.
|
|
69
|
+
*/
|
|
70
|
+
answerExternalOffer(
|
|
71
|
+
offer: RTCSessionDescriptionInit,
|
|
72
|
+
options?: AnswerOptions
|
|
73
|
+
): Promise<MockRTCExternalAnswerParams>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Takes a connection id, and returns the associated session.
|
|
77
|
+
*
|
|
78
|
+
* This is useful for advanced use cases, where keeping the session returned by other
|
|
79
|
+
* setup methods is inconvenient, and it's easier to keep ids and look up sessions
|
|
80
|
+
* on demand instead.
|
|
81
|
+
*/
|
|
82
|
+
getSession(id: string): MockRTCSession;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Retrieve an array of all data channel messages that this peer has received on
|
|
86
|
+
* all connections.
|
|
87
|
+
*/
|
|
88
|
+
getAllMessages(): Promise<Array<string | Buffer>>;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Retrieve an array of all data channel messages on a specific channel that this
|
|
92
|
+
* peer has received on all connections.
|
|
93
|
+
*/
|
|
94
|
+
getMessagesOnChannel(channelName: string): Promise<Array<string | Buffer>>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Once a connection has been created, you can access its session API. This allows
|
|
99
|
+
* for renegotiation of an existing session, while persisting the same connection
|
|
100
|
+
* and ongoing handling process.
|
|
101
|
+
*/
|
|
102
|
+
export interface MockRTCSession {
|
|
103
|
+
/**
|
|
104
|
+
* For most use cases explicitly using the session ID isn't necessary.
|
|
105
|
+
*
|
|
106
|
+
* For some advanced use cases though, it's more convenient to store session ids and use
|
|
107
|
+
* peer.getSession, rather than using the session property from the setup methods directly.
|
|
108
|
+
*/
|
|
109
|
+
readonly sessionId: string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Create a new offer for this session, to renegotiate the existing connection.
|
|
113
|
+
*/
|
|
114
|
+
createOffer(options?: OfferOptions): Promise<RTCSessionDescriptionInit>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Provide an answer to complete an offer for this session, to renegotiate the existing connection.
|
|
118
|
+
*/
|
|
119
|
+
completeOffer(answer: RTCSessionDescriptionInit): Promise<void>;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get an answer given an offer from elsewhere, to renegotiate the existing connection.
|
|
123
|
+
*/
|
|
124
|
+
answerOffer(offer: RTCSessionDescriptionInit, options?: AnswerOptions): Promise<RTCSessionDescriptionInit>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface MockRTCOfferParams {
|
|
128
|
+
offer: RTCSessionDescriptionInit;
|
|
129
|
+
setAnswer: (answer: RTCSessionDescriptionInit) => Promise<void>;
|
|
130
|
+
session: MockRTCSession;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface MockRTCAnswerParams {
|
|
134
|
+
answer: RTCSessionDescriptionInit;
|
|
135
|
+
session: MockRTCSession;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface OfferOptions {
|
|
139
|
+
/**
|
|
140
|
+
* A raw SDP string that should be mirrored (best efforts) where possible to
|
|
141
|
+
* create an equivalent offer, including the same media with the same params.
|
|
142
|
+
*/
|
|
143
|
+
mirrorSDP?: string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* When using mirrorSDP, for SDP that only defines video/audio media we will
|
|
147
|
+
* receive an offer with no data stream attached. This can be a problem for
|
|
148
|
+
* proxied connections, which need a data stream to hook up the external
|
|
149
|
+
* connection later. If addDataStream is set to true, a data stream will always
|
|
150
|
+
* be created even if not present in the mirrored SDP.
|
|
151
|
+
*
|
|
152
|
+
* This option has no effect if mirrorSDP is not set.
|
|
153
|
+
*/
|
|
154
|
+
addDataStream?: boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface AnswerOptions {
|
|
158
|
+
/**
|
|
159
|
+
* A raw SDP string that should be mirrored (best efforts) where possible to
|
|
160
|
+
* create an equivalent answer, including the same media params.
|
|
161
|
+
*/
|
|
162
|
+
mirrorSDP?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface MockRTCExternalOfferParams {
|
|
166
|
+
id: string; // Used for external attach control messages
|
|
167
|
+
offer: RTCSessionDescriptionInit;
|
|
168
|
+
setAnswer: (answer: RTCSessionDescriptionInit) => Promise<void>;
|
|
169
|
+
session: MockRTCSession;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface MockRTCExternalAnswerParams {
|
|
173
|
+
id: string; // Used for external attach control messagesz
|
|
174
|
+
answer: RTCSessionDescriptionInit;
|
|
175
|
+
session: MockRTCSession;
|
|
176
|
+
}
|
package/src/mockrtc.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MockRTCHandlerBuilder } from "./handling/handler-builder";
|
|
7
|
+
import type { MockRTCPeer } from "./mockrtc-peer";
|
|
8
|
+
|
|
9
|
+
export interface MockRTCPeerBuilder extends MockRTCHandlerBuilder<MockRTCPeer> {}
|
|
10
|
+
|
|
11
|
+
export interface MockRTCOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Whether or not all DataChannel messages should be saved for later examination.
|
|
14
|
+
* This can be useful in quick testing, but may use large amounts of data if
|
|
15
|
+
* enabled when proxying lots of traffic.
|
|
16
|
+
*
|
|
17
|
+
* Defaults to false.
|
|
18
|
+
*/
|
|
19
|
+
recordMessages?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MockRTC {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start creating a mock WebRTC peer. This method returns a builder, who
|
|
26
|
+
* must be configured with the mock peer's settings. Once configured the
|
|
27
|
+
* peer can be created by calling any `.thenX()` method to define the
|
|
28
|
+
* peer's behaviour.
|
|
29
|
+
*/
|
|
30
|
+
buildPeer(): MockRTCPeerBuilder;
|
|
31
|
+
|
|
32
|
+
start(): Promise<void>;
|
|
33
|
+
|
|
34
|
+
stop(): Promise<void>;
|
|
35
|
+
|
|
36
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as stream from 'stream';
|
|
7
|
+
import { gql } from 'graphql-tag';
|
|
8
|
+
import { PluggableAdmin } from 'mockttp';
|
|
9
|
+
import type { IResolvers } from "@graphql-tools/utils";
|
|
10
|
+
|
|
11
|
+
import { HandlerStep, StepLookup } from '../handling/handler-steps';
|
|
12
|
+
import { MockRTCOptions } from '../mockrtc';
|
|
13
|
+
import { MockRTCServer } from './mockrtc-server';
|
|
14
|
+
import { AnswerOptions, OfferOptions } from '../mockrtc-peer';
|
|
15
|
+
|
|
16
|
+
const { deserialize } = PluggableAdmin.Serialization;
|
|
17
|
+
type SerializedValue<T> = PluggableAdmin.Serialization.SerializedValue<T>;
|
|
18
|
+
|
|
19
|
+
export interface SessionData {
|
|
20
|
+
id: string;
|
|
21
|
+
description: RTCSessionDescriptionInit
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class MockRTCAdminPlugin implements PluggableAdmin.AdminPlugin<MockRTCOptions, {}> {
|
|
25
|
+
|
|
26
|
+
private mockRTCServer!: MockRTCServer;
|
|
27
|
+
|
|
28
|
+
start(options: MockRTCOptions) {
|
|
29
|
+
this.mockRTCServer = new MockRTCServer(options);
|
|
30
|
+
return this.mockRTCServer.start();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
reset() {}
|
|
34
|
+
|
|
35
|
+
stop() {
|
|
36
|
+
return this.mockRTCServer.stop();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
schema = gql`
|
|
40
|
+
extend type Mutation {
|
|
41
|
+
createPeer(data: RTCHandlerData!): MockedPeer!
|
|
42
|
+
|
|
43
|
+
createOffer(peerId: ID!, sessionId: ID, options: Raw): Session!
|
|
44
|
+
createExternalOffer(peerId: ID!, options: Raw): Session!
|
|
45
|
+
completeOffer(peerId: ID!, sessionId: ID!, answer: SessionDescriptionInput!): Void
|
|
46
|
+
|
|
47
|
+
answerOffer(peerId: ID!, sessionId: ID, offer: SessionDescriptionInput!, options: Raw): Session!
|
|
48
|
+
answerExternalOffer(peerId: ID!, offer: SessionDescriptionInput!, options: Raw): Session!
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
input RTCHandlerData {
|
|
52
|
+
steps: [Raw!]!
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type MockedPeer {
|
|
56
|
+
peerId: ID!
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
input SessionDescriptionInput {
|
|
60
|
+
type: String!
|
|
61
|
+
sdp: String!
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type SessionDescriptionResult {
|
|
65
|
+
type: String!
|
|
66
|
+
sdp: String!
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type Session {
|
|
70
|
+
id: ID!
|
|
71
|
+
description: SessionDescriptionResult
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
extend type Query {
|
|
75
|
+
getSeenMessages(peerId: ID!, channelName: String): [Raw!]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
scalar HandlerStep
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
buildResolvers(adminStream: stream.Duplex, ruleParams: {}): IResolvers {
|
|
82
|
+
return {
|
|
83
|
+
Mutation: {
|
|
84
|
+
createPeer: (__: any, { data: { steps } }: { data: {
|
|
85
|
+
steps: Array<SerializedValue<HandlerStep>>
|
|
86
|
+
} }) => {
|
|
87
|
+
return this.mockRTCServer.buildPeerFromData(
|
|
88
|
+
steps.map((stepData) =>
|
|
89
|
+
deserialize(stepData, adminStream, ruleParams, StepLookup)
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
createOffer: async (__: any, { peerId, sessionId, options }: {
|
|
94
|
+
peerId: string,
|
|
95
|
+
sessionId?: string,
|
|
96
|
+
options?: OfferOptions
|
|
97
|
+
}): Promise<SessionData> => {
|
|
98
|
+
const peer = this.mockRTCServer.getPeer(peerId);
|
|
99
|
+
if (!peer) throw new Error("Id matches no active peer");
|
|
100
|
+
|
|
101
|
+
if (sessionId) {
|
|
102
|
+
const session = peer.getSession(sessionId);
|
|
103
|
+
return {
|
|
104
|
+
id: sessionId,
|
|
105
|
+
description: await session.createOffer(options)
|
|
106
|
+
};
|
|
107
|
+
} else {
|
|
108
|
+
const offerParams = await peer.createOffer(options);
|
|
109
|
+
return {
|
|
110
|
+
id: offerParams.session.sessionId,
|
|
111
|
+
description: offerParams.offer
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
createExternalOffer: async (__: any, { peerId, options }: {
|
|
116
|
+
peerId: string,
|
|
117
|
+
options?: OfferOptions
|
|
118
|
+
}): Promise<SessionData> => {
|
|
119
|
+
const peer = this.mockRTCServer.getPeer(peerId);
|
|
120
|
+
if (!peer) throw new Error("Id matches no active peer");
|
|
121
|
+
|
|
122
|
+
const offerParams = await peer.createExternalOffer(options);
|
|
123
|
+
return {
|
|
124
|
+
id: offerParams.id,
|
|
125
|
+
description: offerParams.offer
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
completeOffer: async (__: any, { peerId, sessionId, answer } : {
|
|
129
|
+
peerId: string,
|
|
130
|
+
sessionId: string,
|
|
131
|
+
answer: RTCSessionDescriptionInit
|
|
132
|
+
}): Promise<void> => {
|
|
133
|
+
const session = this.mockRTCServer.getPeer(peerId).getSession(sessionId);
|
|
134
|
+
await session.completeOffer(answer);
|
|
135
|
+
},
|
|
136
|
+
answerOffer: async (__: any, { peerId, sessionId, offer, options } : {
|
|
137
|
+
peerId: string,
|
|
138
|
+
sessionId?: string,
|
|
139
|
+
offer: RTCSessionDescriptionInit,
|
|
140
|
+
options?: AnswerOptions
|
|
141
|
+
}): Promise<SessionData> => {
|
|
142
|
+
const peer = this.mockRTCServer.getPeer(peerId);
|
|
143
|
+
if (!peer) throw new Error("Id matches no active peer");
|
|
144
|
+
|
|
145
|
+
if (sessionId) {
|
|
146
|
+
const session = peer.getSession(sessionId);
|
|
147
|
+
const answer = await session.answerOffer(offer, options);
|
|
148
|
+
return { id: sessionId, description: answer };
|
|
149
|
+
} else {
|
|
150
|
+
const answerParams = await peer.answerOffer(offer, options);
|
|
151
|
+
return {
|
|
152
|
+
id: answerParams.session.sessionId,
|
|
153
|
+
description: answerParams.answer
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
answerExternalOffer: async (__: any, { peerId, offer, options } : {
|
|
158
|
+
peerId: string,
|
|
159
|
+
offer: RTCSessionDescriptionInit,
|
|
160
|
+
options?: AnswerOptions
|
|
161
|
+
}): Promise<SessionData> => {
|
|
162
|
+
const peer = this.mockRTCServer.getPeer(peerId);
|
|
163
|
+
if (!peer) throw new Error("Id matches no active peer");
|
|
164
|
+
|
|
165
|
+
const answerParams = await peer.answerExternalOffer(offer, options);
|
|
166
|
+
return {
|
|
167
|
+
id: answerParams.id,
|
|
168
|
+
description: answerParams.answer
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
Query: {
|
|
173
|
+
getSeenMessages: async (__: any, { peerId, channelName }: {
|
|
174
|
+
peerId: string,
|
|
175
|
+
channelName?: string
|
|
176
|
+
}) => {
|
|
177
|
+
const peer = this.mockRTCServer.getPeer(peerId);
|
|
178
|
+
if (!peer) throw new Error("Id matches no active peer");
|
|
179
|
+
|
|
180
|
+
const messages = await (channelName != undefined
|
|
181
|
+
? peer.getMessagesOnChannel(channelName)
|
|
182
|
+
: peer.getAllMessages()
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return messages.map((message) => {
|
|
186
|
+
if (Buffer.isBuffer(message)) {
|
|
187
|
+
return { type: 'buffer', value: message.toString('base64') };
|
|
188
|
+
} else {
|
|
189
|
+
return message;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PluggableAdmin } from 'mockttp';
|
|
7
|
+
import { MockRTCAdminPlugin } from './mockrtc-admin-plugin';
|
|
8
|
+
|
|
9
|
+
export class MockRTCAdminServer extends PluggableAdmin.AdminServer<{ webrtc: MockRTCAdminPlugin }> {
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
super({
|
|
13
|
+
adminPlugins: { webrtc: MockRTCAdminPlugin }
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2022 Tim Perry <tim@httptoolkit.tech>
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
MockRTCPeer,
|
|
10
|
+
MockRTCPeerOptions,
|
|
11
|
+
MockRTCSession,
|
|
12
|
+
MockRTCAnswerParams,
|
|
13
|
+
MockRTCOfferParams,
|
|
14
|
+
MockRTCExternalAnswerParams,
|
|
15
|
+
MockRTCExternalOfferParams,
|
|
16
|
+
OfferOptions,
|
|
17
|
+
AnswerOptions
|
|
18
|
+
} from "../mockrtc-peer";
|
|
19
|
+
import { HandlerStep } from '../handling/handler-steps';
|
|
20
|
+
|
|
21
|
+
import { RTCConnection } from '../webrtc/rtc-connection';
|
|
22
|
+
import { MockRTCConnection } from '../webrtc/mockrtc-connection';
|
|
23
|
+
import { DataChannelStream } from '../webrtc/datachannel-stream';
|
|
24
|
+
|
|
25
|
+
export class MockRTCServerPeer implements MockRTCPeer {
|
|
26
|
+
|
|
27
|
+
readonly peerId = randomUUID();
|
|
28
|
+
|
|
29
|
+
// A list of all currently open connections managed by this peer
|
|
30
|
+
private readonly connections: { [id: string]: RTCConnection } = {};
|
|
31
|
+
|
|
32
|
+
// A subset of the connections: external connections with no assigned internal connection
|
|
33
|
+
private readonly unassignedExternalConnections: { [id: string]: RTCConnection } = {};
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private handlerSteps: HandlerStep[],
|
|
37
|
+
private options: MockRTCPeerOptions = {}
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
private trackConnection(conn: RTCConnection) {
|
|
41
|
+
this.connections[conn.id] = conn;
|
|
42
|
+
conn.once('connection-closed', () => {
|
|
43
|
+
delete this.connections[conn.id];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private getExternalConnection = (id: string) => {
|
|
48
|
+
const externalConn = this.unassignedExternalConnections[id];
|
|
49
|
+
if (!externalConn) throw new Error(`Attempted to connect unknown external conn ${id}`);
|
|
50
|
+
delete this.unassignedExternalConnections[id];
|
|
51
|
+
return externalConn;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async createExternalOffer(options: OfferOptions = {}): Promise<MockRTCExternalOfferParams> {
|
|
55
|
+
const externalConn = new RTCConnection();
|
|
56
|
+
this.unassignedExternalConnections[externalConn.id] = externalConn;
|
|
57
|
+
this.trackConnection(externalConn);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
id: externalConn.id,
|
|
61
|
+
offer: await externalConn.sessionApi.createOffer(options),
|
|
62
|
+
session: externalConn.sessionApi,
|
|
63
|
+
setAnswer: async (answer: RTCSessionDescriptionInit) => {
|
|
64
|
+
externalConn.sessionApi.completeOffer(answer);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async answerExternalOffer(
|
|
70
|
+
offer: RTCSessionDescriptionInit,
|
|
71
|
+
options?: AnswerOptions
|
|
72
|
+
): Promise<MockRTCExternalAnswerParams> {
|
|
73
|
+
const externalConn = new RTCConnection();
|
|
74
|
+
this.unassignedExternalConnections[externalConn.id] = externalConn;
|
|
75
|
+
this.trackConnection(externalConn);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
id: externalConn.id,
|
|
79
|
+
answer: await externalConn.sessionApi.answerOffer(offer, options),
|
|
80
|
+
session: externalConn.sessionApi
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private createConnection() {
|
|
85
|
+
const conn = new MockRTCConnection(this.getExternalConnection);
|
|
86
|
+
this.trackConnection(conn);
|
|
87
|
+
|
|
88
|
+
this.handleConnection(conn).catch((error) => {
|
|
89
|
+
console.error("Error handling WebRTC connection:", error);
|
|
90
|
+
conn.close().catch(() => {});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (this.options.recordMessages) {
|
|
94
|
+
conn.on('channel-open', (channel: DataChannelStream) => {
|
|
95
|
+
const channelLabel = channel.label;
|
|
96
|
+
const messageLog = (this.messages[channelLabel] ??= []);
|
|
97
|
+
|
|
98
|
+
channel.on('data', d => {
|
|
99
|
+
messageLog.push(d);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return conn;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async createOffer(options: OfferOptions = {}): Promise<MockRTCOfferParams> {
|
|
108
|
+
const conn = this.createConnection();
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
offer: await conn.sessionApi.createOffer(options),
|
|
112
|
+
session: conn.sessionApi,
|
|
113
|
+
setAnswer: async (answer) => {
|
|
114
|
+
conn.sessionApi.completeOffer(answer);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async answerOffer(offer: RTCSessionDescriptionInit, options: AnswerOptions = {}): Promise<MockRTCAnswerParams> {
|
|
120
|
+
const conn = this.createConnection();
|
|
121
|
+
return {
|
|
122
|
+
answer: await conn.sessionApi.answerOffer(offer, options),
|
|
123
|
+
session: conn.sessionApi
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getSession(id: string): MockRTCSession {
|
|
128
|
+
return this.connections[id].sessionApi;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async handleConnection(conn: MockRTCConnection) {
|
|
132
|
+
await conn.waitUntilConnected();
|
|
133
|
+
|
|
134
|
+
for (const step of this.handlerSteps) {
|
|
135
|
+
await step.handle(conn);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await conn.close();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async close() {
|
|
142
|
+
await Promise.all(
|
|
143
|
+
Object.values(this.connections).map(c =>
|
|
144
|
+
c.close()
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private messages: { [channelName: string]: Array<string | Buffer> } = {};
|
|
150
|
+
|
|
151
|
+
async getAllMessages() {
|
|
152
|
+
return Object.values(this.messages).flat();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getMessagesOnChannel(channelName: string) {
|
|
156
|
+
return this.messages[channelName].flat();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
}
|