@structured-id/ui-core 1.3.0 → 3.0.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 +22 -10
- package/package.json +25 -43
- package/src/components/SidStatusBadge.vue +1 -5
- package/src/composables/useAccountTransport.test.ts +2 -1
- package/src/composables/useGrpc.test.ts +17 -14
- package/src/composables/useGrpc.ts +4 -2
- package/src/index.ts +2 -120
- package/src/profile/components/ConsentManager.vue +5 -1
- package/src/profile/components/ProfileEditor.vue +1 -1
- package/src/profile/components/SecuritySettings.vue +5 -1
- package/src/profile/components/SessionList.vue +2 -1
- package/src/profile/components/SidRegistrationForm.test.ts +173 -1
- package/src/profile/components/SidRegistrationForm.vue +88 -32
- package/src/profile/components/index.ts +1 -4
- package/src/profile/composables/index.ts +1 -0
- package/src/profile/composables/operationPlan.test.ts +155 -0
- package/src/profile/composables/operationPlan.ts +218 -0
- package/src/profile/composables/useAccountApi.ts +4 -4
- package/src/profile/composables/useAuth.test.ts +312 -31
- package/src/profile/composables/useAuth.ts +176 -30
- package/src/profile/composables/useAuthApi.opaque.test.ts +13 -6
- package/src/profile/composables/useAuthApi.ts +40 -18
- package/src/profile/composables/useCurrentPasswordRequirement.test.ts +196 -0
- package/src/profile/composables/useCurrentPasswordRequirement.ts +95 -0
- package/src/profile/composables/useEnrollmentApi.test.ts +7 -3
- package/src/profile/composables/useEnrollmentApi.ts +3 -5
- package/src/profile/composables/useIdentityApi.ts +4 -4
- package/src/profile/composables/useSecurityLevel.test.ts +1 -1
- package/src/profile/composables/useSecurityLevel.ts +1 -1
- package/src/quasar/SidPasswordCeremony.test.ts +253 -0
- package/src/quasar/SidPasswordCeremony.vue +456 -0
- package/src/quasar/index.ts +2 -0
- package/src/utils/rpcRefusal.test.ts +4 -4
- package/src/utils/rpcRefusal.ts +8 -4
- package/dist/browser-vp/index.cjs +0 -349
- package/dist/browser-vp/index.d.cts +0 -276
- package/dist/browser-vp/index.d.ts +0 -276
- package/dist/browser-vp/index.js +0 -349
- package/dist/chunk-C3Q766CY.cjs +0 -125
- package/dist/chunk-J5YXRXB5.js +0 -125
- package/dist/index.cjs +0 -23581
- package/dist/index.d.cts +0 -11539
- package/dist/index.d.ts +0 -11539
- package/dist/index.js +0 -23581
- package/dist/storage-4CB2JTLW.cjs +0 -22
- package/dist/storage-THRWREZP.js +0 -22
- package/src/browser-vp/crypto.ts +0 -253
- package/src/browser-vp/index.test.ts +0 -10
- package/src/browser-vp/index.ts +0 -116
- package/src/browser-vp/prf.ts +0 -179
- package/src/browser-vp/storage.test.ts +0 -9
- package/src/browser-vp/storage.ts +0 -163
- package/src/browser-vp/types.ts +0 -95
- package/src/browser-vp/vp.test.ts +0 -43
- package/src/browser-vp/vp.ts +0 -111
- package/src/generated/google/api/http.ts +0 -687
- package/src/generated/google/api/httpbody.ts +0 -159
- package/src/generated/google/protobuf/any.ts +0 -326
- package/src/generated/google/protobuf/descriptor.ts +0 -4865
- package/src/generated/google/protobuf/duration.ts +0 -230
- package/src/generated/google/protobuf/empty.ts +0 -94
- package/src/generated/google/protobuf/struct.ts +0 -484
- package/src/generated/google/protobuf/timestamp.ts +0 -287
- package/src/generated/google/rpc/error_details.ts +0 -1386
- package/src/generated/google/rpc/status.ts +0 -127
- package/src/generated/sid/v1/account/account.client.ts +0 -527
- package/src/generated/sid/v1/account/account.ts +0 -3261
- package/src/generated/sid/v1/admin/admin.client.ts +0 -139
- package/src/generated/sid/v1/admin/admin.ts +0 -781
- package/src/generated/sid/v1/admin/admin_roles.client.ts +0 -181
- package/src/generated/sid/v1/admin/admin_roles.ts +0 -1606
- package/src/generated/sid/v1/admin/analytics.client.ts +0 -106
- package/src/generated/sid/v1/admin/analytics.ts +0 -1017
- package/src/generated/sid/v1/admin/branding.client.ts +0 -166
- package/src/generated/sid/v1/admin/branding.ts +0 -1906
- package/src/generated/sid/v1/admin/enrollment.client.ts +0 -177
- package/src/generated/sid/v1/admin/enrollment.ts +0 -1766
- package/src/generated/sid/v1/admin/events.client.ts +0 -195
- package/src/generated/sid/v1/admin/events.ts +0 -1900
- package/src/generated/sid/v1/admin/flow.client.ts +0 -183
- package/src/generated/sid/v1/admin/flow.ts +0 -1552
- package/src/generated/sid/v1/admin/keys.client.ts +0 -107
- package/src/generated/sid/v1/admin/keys.ts +0 -931
- package/src/generated/sid/v1/admin/login_experience.client.ts +0 -160
- package/src/generated/sid/v1/admin/login_experience.ts +0 -1061
- package/src/generated/sid/v1/admin/migration.client.ts +0 -127
- package/src/generated/sid/v1/admin/migration.ts +0 -1948
- package/src/generated/sid/v1/admin/notifications.client.ts +0 -134
- package/src/generated/sid/v1/admin/notifications.ts +0 -1273
- package/src/generated/sid/v1/admin/operations.client.ts +0 -241
- package/src/generated/sid/v1/admin/operations.ts +0 -2633
- package/src/generated/sid/v1/admin/organizations.client.ts +0 -57
- package/src/generated/sid/v1/admin/organizations.ts +0 -332
- package/src/generated/sid/v1/admin/provisioning.client.ts +0 -232
- package/src/generated/sid/v1/admin/provisioning.ts +0 -1383
- package/src/generated/sid/v1/admin/scopes.client.ts +0 -171
- package/src/generated/sid/v1/admin/scopes.ts +0 -1324
- package/src/generated/sid/v1/admin/security.client.ts +0 -410
- package/src/generated/sid/v1/admin/security.ts +0 -4077
- package/src/generated/sid/v1/admin/sessions.client.ts +0 -114
- package/src/generated/sid/v1/admin/sessions.ts +0 -923
- package/src/generated/sid/v1/admin/settings.client.ts +0 -351
- package/src/generated/sid/v1/admin/settings.ts +0 -2458
- package/src/generated/sid/v1/admin/user_profile.client.ts +0 -57
- package/src/generated/sid/v1/admin/user_profile.ts +0 -730
- package/src/generated/sid/v1/admin/webhooks.client.ts +0 -269
- package/src/generated/sid/v1/admin/webhooks.ts +0 -2804
- package/src/generated/sid/v1/attestation/attestation.client.ts +0 -115
- package/src/generated/sid/v1/attestation/attestation.ts +0 -813
- package/src/generated/sid/v1/authn/auth.client.ts +0 -943
- package/src/generated/sid/v1/authn/auth.ts +0 -9719
- package/src/generated/sid/v1/authn/flows.client.ts +0 -413
- package/src/generated/sid/v1/authn/flows.ts +0 -3205
- package/src/generated/sid/v1/authn/issuer.client.ts +0 -489
- package/src/generated/sid/v1/authn/issuer.ts +0 -1600
- package/src/generated/sid/v1/authn/password_history.client.ts +0 -57
- package/src/generated/sid/v1/authn/password_history.ts +0 -578
- package/src/generated/sid/v1/authn/system_integration.client.ts +0 -73
- package/src/generated/sid/v1/authn/system_integration.ts +0 -218
- package/src/generated/sid/v1/authz/authz.client.ts +0 -459
- package/src/generated/sid/v1/authz/authz.ts +0 -6524
- package/src/generated/sid/v1/authz/forward_auth.client.ts +0 -97
- package/src/generated/sid/v1/authz/forward_auth.ts +0 -79
- package/src/generated/sid/v1/authz/governance.client.ts +0 -102
- package/src/generated/sid/v1/authz/governance.ts +0 -875
- package/src/generated/sid/v1/common/errors.ts +0 -710
- package/src/generated/sid/v1/common/pagination.ts +0 -152
- package/src/generated/sid/v1/common/transport.ts +0 -227
- package/src/generated/sid/v1/common/types.ts +0 -151
- package/src/generated/sid/v1/events/event_stream.client.ts +0 -67
- package/src/generated/sid/v1/events/event_stream.ts +0 -375
- package/src/generated/sid/v1/federation/upstream.client.ts +0 -142
- package/src/generated/sid/v1/federation/upstream.ts +0 -1736
- package/src/generated/sid/v1/identity/identity.client.ts +0 -467
- package/src/generated/sid/v1/identity/identity.ts +0 -5676
- package/src/generated/sid/v1/ids/ids.ts +0 -395
- package/src/generated/sid/v1/machine/machine_user.client.ts +0 -219
- package/src/generated/sid/v1/machine/machine_user.ts +0 -2364
- package/src/generated/sid/v1/machine/pat.client.ts +0 -130
- package/src/generated/sid/v1/machine/pat.ts +0 -1083
- package/src/generated/sid/v1/projects/projects.client.ts +0 -404
- package/src/generated/sid/v1/projects/projects.ts +0 -4446
- package/src/generated/sid/v1/scim/scim.client.ts +0 -520
- package/src/generated/sid/v1/scim/scim.ts +0 -3182
- package/src/generated/sid/v1/test/test.client.ts +0 -232
- package/src/generated/sid/v1/test/test.ts +0 -1136
- package/src/transport/adaptive.test.ts +0 -728
- package/src/transport/adaptive.ts +0 -308
- package/src/transport/framing.test.ts +0 -227
- package/src/transport/framing.ts +0 -98
- package/src/transport/webtransport.test.ts +0 -728
- package/src/transport/webtransport.ts +0 -323
|
@@ -1,728 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { RpcError } from "@protobuf-ts/runtime-rpc";
|
|
3
|
-
import type { MethodInfo, RpcOptions } from "@protobuf-ts/runtime-rpc";
|
|
4
|
-
import {
|
|
5
|
-
RequestHeader,
|
|
6
|
-
ResponseHeader,
|
|
7
|
-
} from "../generated/sid/v1/common/transport";
|
|
8
|
-
import {
|
|
9
|
-
WebTransportConnection,
|
|
10
|
-
WebTransportRpcTransport,
|
|
11
|
-
} from "./webtransport";
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Mock WebTransport browser API
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
interface MockBidiStream {
|
|
18
|
-
readable: ReadableStream<Uint8Array>;
|
|
19
|
-
writable: WritableStream<Uint8Array>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Build a mock bidirectional stream where server sends `responseChunks`. */
|
|
23
|
-
function makeMockBidiStream(responseChunks: Uint8Array[]): {
|
|
24
|
-
stream: MockBidiStream;
|
|
25
|
-
writtenChunks: Uint8Array[];
|
|
26
|
-
} {
|
|
27
|
-
const writtenChunks: Uint8Array[] = [];
|
|
28
|
-
|
|
29
|
-
// Writable side: collects written bytes
|
|
30
|
-
const writable = new WritableStream<Uint8Array>({
|
|
31
|
-
write(chunk) {
|
|
32
|
-
writtenChunks.push(chunk);
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Readable side: yields responseChunks one by one
|
|
37
|
-
let i = 0;
|
|
38
|
-
const readable = new ReadableStream<Uint8Array>({
|
|
39
|
-
pull(controller) {
|
|
40
|
-
if (i < responseChunks.length) {
|
|
41
|
-
controller.enqueue(responseChunks[i++]);
|
|
42
|
-
} else {
|
|
43
|
-
controller.close();
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
return { stream: { readable, writable }, writtenChunks };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Build a ResponseHeader frame (length-prefix + proto bytes). */
|
|
52
|
-
function buildResponseFrame(header: ResponseHeader): Uint8Array {
|
|
53
|
-
const payload = ResponseHeader.toBinary(header);
|
|
54
|
-
const lenBuf = new Uint8Array(4);
|
|
55
|
-
new DataView(lenBuf.buffer).setUint32(0, payload.length, false);
|
|
56
|
-
const frame = new Uint8Array(4 + payload.length);
|
|
57
|
-
frame.set(lenBuf);
|
|
58
|
-
frame.set(payload, 4);
|
|
59
|
-
return frame;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Build a body frame (length-prefix + raw bytes). */
|
|
63
|
-
function buildBodyFrame(body: Uint8Array): Uint8Array {
|
|
64
|
-
const lenBuf = new Uint8Array(4);
|
|
65
|
-
new DataView(lenBuf.buffer).setUint32(0, body.length, false);
|
|
66
|
-
const frame = new Uint8Array(4 + body.length);
|
|
67
|
-
frame.set(lenBuf);
|
|
68
|
-
frame.set(body, 4);
|
|
69
|
-
return frame;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Parse a length-prefix frame from a flat byte array (for assertion helpers). */
|
|
73
|
-
function parseFrame(
|
|
74
|
-
bytes: Uint8Array,
|
|
75
|
-
offset: number,
|
|
76
|
-
): { payload: Uint8Array; next: number } {
|
|
77
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
78
|
-
const len = view.getUint32(offset, false);
|
|
79
|
-
const payload = bytes.slice(offset + 4, offset + 4 + len);
|
|
80
|
-
return { payload, next: offset + 4 + len };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Concatenate all chunks into a single Uint8Array. */
|
|
84
|
-
function concat(chunks: Uint8Array[]): Uint8Array {
|
|
85
|
-
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
86
|
-
const out = new Uint8Array(total);
|
|
87
|
-
let offset = 0;
|
|
88
|
-
for (const c of chunks) {
|
|
89
|
-
out.set(c, offset);
|
|
90
|
-
offset += c.length;
|
|
91
|
-
}
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// Test fixture: minimal MethodInfo
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
interface Msg {
|
|
100
|
-
value: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function makeMethod(): MethodInfo<Msg, Msg> {
|
|
104
|
-
return {
|
|
105
|
-
service: { typeName: "sid.v1.TestService", methods: [], options: {} },
|
|
106
|
-
name: "TestRpc",
|
|
107
|
-
localName: "testRpc",
|
|
108
|
-
idempotency: undefined,
|
|
109
|
-
serverStreaming: false,
|
|
110
|
-
clientStreaming: false,
|
|
111
|
-
I: {
|
|
112
|
-
typeName: "TestInput",
|
|
113
|
-
toBinary: (msg: Msg) => new TextEncoder().encode(JSON.stringify(msg)),
|
|
114
|
-
fromBinary: (bytes: Uint8Array) =>
|
|
115
|
-
JSON.parse(new TextDecoder().decode(bytes)) as Msg,
|
|
116
|
-
},
|
|
117
|
-
O: {
|
|
118
|
-
typeName: "TestOutput",
|
|
119
|
-
toBinary: (msg: Msg) => new TextEncoder().encode(JSON.stringify(msg)),
|
|
120
|
-
fromBinary: (bytes: Uint8Array) =>
|
|
121
|
-
JSON.parse(new TextDecoder().decode(bytes)) as Msg,
|
|
122
|
-
},
|
|
123
|
-
options: {},
|
|
124
|
-
} as unknown as MethodInfo<Msg, Msg>;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
// WebTransportConnection tests
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
|
|
131
|
-
describe("WebTransportConnection", () => {
|
|
132
|
-
let mockWt: {
|
|
133
|
-
ready: Promise<void>;
|
|
134
|
-
closed: Promise<void>;
|
|
135
|
-
close: ReturnType<typeof vi.fn>;
|
|
136
|
-
createBidirectionalStream: ReturnType<typeof vi.fn>;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
beforeEach(() => {
|
|
140
|
-
mockWt = {
|
|
141
|
-
ready: Promise.resolve(),
|
|
142
|
-
closed: new Promise(() => {}), // never resolves during test
|
|
143
|
-
close: vi.fn(),
|
|
144
|
-
createBidirectionalStream: vi.fn(),
|
|
145
|
-
};
|
|
146
|
-
(globalThis as Record<string, unknown>).WebTransport = vi
|
|
147
|
-
.fn()
|
|
148
|
-
.mockImplementation(() => mockWt);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
afterEach(() => {
|
|
152
|
-
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("connects successfully and marks connected", async () => {
|
|
156
|
-
const conn = new WebTransportConnection({
|
|
157
|
-
url: "https://wt.sid.example.com",
|
|
158
|
-
});
|
|
159
|
-
expect(conn.connected).toBe(false);
|
|
160
|
-
|
|
161
|
-
await conn.connect();
|
|
162
|
-
expect(conn.connected).toBe(true);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("connect() is idempotent — does not open second connection", async () => {
|
|
166
|
-
const conn = new WebTransportConnection({
|
|
167
|
-
url: "https://wt.sid.example.com",
|
|
168
|
-
});
|
|
169
|
-
await conn.connect();
|
|
170
|
-
await conn.connect();
|
|
171
|
-
await conn.connect();
|
|
172
|
-
|
|
173
|
-
expect(
|
|
174
|
-
(globalThis as Record<string, unknown>).WebTransport,
|
|
175
|
-
).toHaveBeenCalledTimes(1);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("close() disconnects and allows reconnect", async () => {
|
|
179
|
-
const conn = new WebTransportConnection({
|
|
180
|
-
url: "https://wt.sid.example.com",
|
|
181
|
-
});
|
|
182
|
-
await conn.connect();
|
|
183
|
-
|
|
184
|
-
conn.close();
|
|
185
|
-
expect(conn.connected).toBe(false);
|
|
186
|
-
expect(mockWt.close).toHaveBeenCalled();
|
|
187
|
-
|
|
188
|
-
// Can reconnect after close
|
|
189
|
-
await conn.connect();
|
|
190
|
-
expect(conn.connected).toBe(true);
|
|
191
|
-
expect(
|
|
192
|
-
(globalThis as Record<string, unknown>).WebTransport,
|
|
193
|
-
).toHaveBeenCalledTimes(2);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("detects remote disconnection via wt.closed", async () => {
|
|
197
|
-
let resolveClosedPromise!: () => void;
|
|
198
|
-
mockWt.closed = new Promise<void>((resolve) => {
|
|
199
|
-
resolveClosedPromise = resolve;
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const conn = new WebTransportConnection({
|
|
203
|
-
url: "https://wt.sid.example.com",
|
|
204
|
-
});
|
|
205
|
-
await conn.connect();
|
|
206
|
-
expect(conn.connected).toBe(true);
|
|
207
|
-
|
|
208
|
-
// Simulate server closing the connection
|
|
209
|
-
resolveClosedPromise();
|
|
210
|
-
await Promise.resolve(); // let microtasks run
|
|
211
|
-
|
|
212
|
-
expect(conn.connected).toBe(false);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("passes cert hash as serverCertificateHashes when provided", async () => {
|
|
216
|
-
const certHash = "deadbeef1234567890abcdef";
|
|
217
|
-
const conn = new WebTransportConnection({
|
|
218
|
-
url: "https://wt.sid.example.com",
|
|
219
|
-
certHash,
|
|
220
|
-
});
|
|
221
|
-
await conn.connect();
|
|
222
|
-
|
|
223
|
-
const WebTransportCtor = (globalThis as Record<string, unknown>)
|
|
224
|
-
.WebTransport as ReturnType<typeof vi.fn>;
|
|
225
|
-
const initOpts = WebTransportCtor.mock.calls[0][1] as {
|
|
226
|
-
serverCertificateHashes?: unknown[];
|
|
227
|
-
};
|
|
228
|
-
expect(initOpts.serverCertificateHashes).toBeDefined();
|
|
229
|
-
expect(
|
|
230
|
-
(initOpts.serverCertificateHashes as Array<{ algorithm: string }>)[0]
|
|
231
|
-
.algorithm,
|
|
232
|
-
).toBe("sha-256");
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("does not pass serverCertificateHashes when no cert hash", async () => {
|
|
236
|
-
const conn = new WebTransportConnection({
|
|
237
|
-
url: "https://wt.sid.example.com",
|
|
238
|
-
});
|
|
239
|
-
await conn.connect();
|
|
240
|
-
|
|
241
|
-
const WebTransportCtor = (globalThis as Record<string, unknown>)
|
|
242
|
-
.WebTransport as ReturnType<typeof vi.fn>;
|
|
243
|
-
const initOpts = WebTransportCtor.mock.calls[0][1] as Record<
|
|
244
|
-
string,
|
|
245
|
-
unknown
|
|
246
|
-
>;
|
|
247
|
-
expect(initOpts.serverCertificateHashes).toBeUndefined();
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("connect() throws when wt.ready rejects", async () => {
|
|
251
|
-
mockWt.ready = Promise.reject(new Error("TLS handshake failed"));
|
|
252
|
-
|
|
253
|
-
const conn = new WebTransportConnection({
|
|
254
|
-
url: "https://wt.sid.example.com",
|
|
255
|
-
});
|
|
256
|
-
await expect(conn.connect()).rejects.toThrow("TLS handshake failed");
|
|
257
|
-
expect(conn.connected).toBe(false);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("concurrent connect() calls share the same promise (no duplicate connections)", async () => {
|
|
261
|
-
// Delay wt.ready so the connection is in-progress when second connect() is called
|
|
262
|
-
let resolveReady!: () => void;
|
|
263
|
-
mockWt.ready = new Promise<void>((resolve) => {
|
|
264
|
-
resolveReady = resolve;
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
const conn = new WebTransportConnection({
|
|
268
|
-
url: "https://wt.sid.example.com",
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// Start two connects concurrently — neither has resolved yet
|
|
272
|
-
const p1 = conn.connect();
|
|
273
|
-
const p2 = conn.connect();
|
|
274
|
-
|
|
275
|
-
// Both should be the same promise (or at least both resolve to same transport)
|
|
276
|
-
resolveReady();
|
|
277
|
-
const [wt1, wt2] = await Promise.all([p1, p2]);
|
|
278
|
-
|
|
279
|
-
expect(wt1).toBe(wt2);
|
|
280
|
-
expect(
|
|
281
|
-
(globalThis as Record<string, unknown>).WebTransport,
|
|
282
|
-
).toHaveBeenCalledTimes(1);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("close() is safe when not connected (no-op)", () => {
|
|
286
|
-
const conn = new WebTransportConnection({
|
|
287
|
-
url: "https://wt.sid.example.com",
|
|
288
|
-
});
|
|
289
|
-
// Never connected — close() should not throw
|
|
290
|
-
expect(() => conn.close()).not.toThrow();
|
|
291
|
-
expect(conn.connected).toBe(false);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("wt.closed after close() does not set connected=false again (already null)", async () => {
|
|
295
|
-
let resolveClosedPromise!: () => void;
|
|
296
|
-
mockWt.closed = new Promise<void>((resolve) => {
|
|
297
|
-
resolveClosedPromise = resolve;
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
const conn = new WebTransportConnection({
|
|
301
|
-
url: "https://wt.sid.example.com",
|
|
302
|
-
});
|
|
303
|
-
await conn.connect();
|
|
304
|
-
|
|
305
|
-
// Manually close before server closes the connection
|
|
306
|
-
conn.close();
|
|
307
|
-
expect(conn.connected).toBe(false);
|
|
308
|
-
|
|
309
|
-
// Now the server closes — wt.closed resolves, but this.transport !== wt (it's null)
|
|
310
|
-
// The handler should be a no-op (not throw, not change state)
|
|
311
|
-
resolveClosedPromise();
|
|
312
|
-
await Promise.resolve();
|
|
313
|
-
|
|
314
|
-
// Still disconnected, no error
|
|
315
|
-
expect(conn.connected).toBe(false);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it("connect() can be retried after failure (connecting state is reset)", async () => {
|
|
319
|
-
// First attempt fails
|
|
320
|
-
mockWt.ready = Promise.reject(new Error("connect failed"));
|
|
321
|
-
const conn = new WebTransportConnection({
|
|
322
|
-
url: "https://wt.sid.example.com",
|
|
323
|
-
});
|
|
324
|
-
await expect(conn.connect()).rejects.toThrow("connect failed");
|
|
325
|
-
|
|
326
|
-
// Second attempt succeeds (new mockWt with resolved ready)
|
|
327
|
-
const mockWt2 = {
|
|
328
|
-
ready: Promise.resolve(),
|
|
329
|
-
closed: new Promise<void>(() => {}),
|
|
330
|
-
close: vi.fn(),
|
|
331
|
-
createBidirectionalStream: vi.fn(),
|
|
332
|
-
};
|
|
333
|
-
(globalThis as Record<string, unknown>).WebTransport = vi
|
|
334
|
-
.fn()
|
|
335
|
-
.mockImplementation(() => mockWt2);
|
|
336
|
-
|
|
337
|
-
await conn.connect();
|
|
338
|
-
expect(conn.connected).toBe(true);
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// ---------------------------------------------------------------------------
|
|
343
|
-
// WebTransportRpcTransport tests
|
|
344
|
-
// ---------------------------------------------------------------------------
|
|
345
|
-
|
|
346
|
-
describe("WebTransportRpcTransport", () => {
|
|
347
|
-
const method = makeMethod();
|
|
348
|
-
const input: Msg = { value: "hello" };
|
|
349
|
-
const options: RpcOptions = { meta: {} };
|
|
350
|
-
|
|
351
|
-
let mockWt: {
|
|
352
|
-
ready: Promise<void>;
|
|
353
|
-
closed: Promise<void>;
|
|
354
|
-
close: ReturnType<typeof vi.fn>;
|
|
355
|
-
createBidirectionalStream: ReturnType<typeof vi.fn>;
|
|
356
|
-
};
|
|
357
|
-
let conn: WebTransportConnection;
|
|
358
|
-
|
|
359
|
-
beforeEach(() => {
|
|
360
|
-
mockWt = {
|
|
361
|
-
ready: Promise.resolve(),
|
|
362
|
-
closed: new Promise(() => {}),
|
|
363
|
-
close: vi.fn(),
|
|
364
|
-
createBidirectionalStream: vi.fn(),
|
|
365
|
-
};
|
|
366
|
-
(globalThis as Record<string, unknown>).WebTransport = vi
|
|
367
|
-
.fn()
|
|
368
|
-
.mockImplementation(() => mockWt);
|
|
369
|
-
conn = new WebTransportConnection({ url: "https://wt.sid.example.com" });
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
afterEach(() => {
|
|
373
|
-
conn.close();
|
|
374
|
-
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it("sends RequestHeader with correct method path and gets response", async () => {
|
|
378
|
-
await conn.connect();
|
|
379
|
-
|
|
380
|
-
const responseBody = new TextEncoder().encode(
|
|
381
|
-
JSON.stringify({ value: "world" }),
|
|
382
|
-
);
|
|
383
|
-
|
|
384
|
-
const respHeader: ResponseHeader = {
|
|
385
|
-
statusCode: 0,
|
|
386
|
-
statusMessage: "",
|
|
387
|
-
metadata: {},
|
|
388
|
-
};
|
|
389
|
-
const { stream, writtenChunks } = makeMockBidiStream([
|
|
390
|
-
buildResponseFrame(respHeader),
|
|
391
|
-
buildBodyFrame(responseBody),
|
|
392
|
-
]);
|
|
393
|
-
mockWt.createBidirectionalStream.mockResolvedValue(stream);
|
|
394
|
-
|
|
395
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
396
|
-
const call = transport.unary(method, input, options);
|
|
397
|
-
const response = await call.response;
|
|
398
|
-
|
|
399
|
-
expect(response).toEqual({ value: "world" });
|
|
400
|
-
|
|
401
|
-
// Verify RequestHeader was sent correctly
|
|
402
|
-
const sent = concat(writtenChunks);
|
|
403
|
-
const { payload: reqHeaderBytes } = parseFrame(sent, 0);
|
|
404
|
-
const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
|
|
405
|
-
expect(sentHeader.method).toBe("sid.v1.TestService/TestRpc");
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
it("sends metadata from RpcOptions.meta in RequestHeader", async () => {
|
|
409
|
-
await conn.connect();
|
|
410
|
-
|
|
411
|
-
const responseBody = new TextEncoder().encode(
|
|
412
|
-
JSON.stringify({ value: "ok" }),
|
|
413
|
-
);
|
|
414
|
-
const respHeader: ResponseHeader = {
|
|
415
|
-
statusCode: 0,
|
|
416
|
-
statusMessage: "",
|
|
417
|
-
metadata: {},
|
|
418
|
-
};
|
|
419
|
-
const { stream, writtenChunks } = makeMockBidiStream([
|
|
420
|
-
buildResponseFrame(respHeader),
|
|
421
|
-
buildBodyFrame(responseBody),
|
|
422
|
-
]);
|
|
423
|
-
mockWt.createBidirectionalStream.mockResolvedValue(stream);
|
|
424
|
-
|
|
425
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
426
|
-
const callWithMeta = transport.unary(method, input, {
|
|
427
|
-
meta: { authorization: "Bearer token123", "x-trace-id": "abc" },
|
|
428
|
-
});
|
|
429
|
-
await callWithMeta.response;
|
|
430
|
-
|
|
431
|
-
const sent = concat(writtenChunks);
|
|
432
|
-
const { payload: reqHeaderBytes } = parseFrame(sent, 0);
|
|
433
|
-
const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
|
|
434
|
-
expect(sentHeader.metadata["authorization"]).toBe("Bearer token123");
|
|
435
|
-
expect(sentHeader.metadata["x-trace-id"]).toBe("abc");
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it("resolves headers from ResponseHeader.metadata", async () => {
|
|
439
|
-
await conn.connect();
|
|
440
|
-
|
|
441
|
-
const responseBody = new TextEncoder().encode(
|
|
442
|
-
JSON.stringify({ value: "ok" }),
|
|
443
|
-
);
|
|
444
|
-
const respHeader: ResponseHeader = {
|
|
445
|
-
statusCode: 0,
|
|
446
|
-
statusMessage: "",
|
|
447
|
-
metadata: { "x-request-id": "req-456" },
|
|
448
|
-
};
|
|
449
|
-
const { stream } = makeMockBidiStream([
|
|
450
|
-
buildResponseFrame(respHeader),
|
|
451
|
-
buildBodyFrame(responseBody),
|
|
452
|
-
]);
|
|
453
|
-
mockWt.createBidirectionalStream.mockResolvedValue(stream);
|
|
454
|
-
|
|
455
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
456
|
-
const call = transport.unary(method, input, options);
|
|
457
|
-
const headers = await call.headers;
|
|
458
|
-
|
|
459
|
-
expect(headers["x-request-id"]).toBe("req-456");
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it("joins array metadata values with comma in RequestHeader", async () => {
|
|
463
|
-
await conn.connect();
|
|
464
|
-
|
|
465
|
-
const responseBody = new TextEncoder().encode(
|
|
466
|
-
JSON.stringify({ value: "ok" }),
|
|
467
|
-
);
|
|
468
|
-
const respHeader: ResponseHeader = {
|
|
469
|
-
statusCode: 0,
|
|
470
|
-
statusMessage: "",
|
|
471
|
-
metadata: {},
|
|
472
|
-
};
|
|
473
|
-
const { stream, writtenChunks } = makeMockBidiStream([
|
|
474
|
-
buildResponseFrame(respHeader),
|
|
475
|
-
buildBodyFrame(responseBody),
|
|
476
|
-
]);
|
|
477
|
-
mockWt.createBidirectionalStream.mockResolvedValue(stream);
|
|
478
|
-
|
|
479
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
480
|
-
const call = transport.unary(method, input, {
|
|
481
|
-
meta: { accept: ["application/json", "text/plain"] },
|
|
482
|
-
});
|
|
483
|
-
await call.response;
|
|
484
|
-
|
|
485
|
-
const sent = concat(writtenChunks);
|
|
486
|
-
const { payload: reqHeaderBytes } = parseFrame(sent, 0);
|
|
487
|
-
const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
|
|
488
|
-
expect(sentHeader.metadata["accept"]).toBe("application/json,text/plain");
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
it("includes Date timeout in RequestHeader.timeoutMs (future date)", async () => {
|
|
492
|
-
await conn.connect();
|
|
493
|
-
|
|
494
|
-
const responseBody = new TextEncoder().encode(
|
|
495
|
-
JSON.stringify({ value: "ok" }),
|
|
496
|
-
);
|
|
497
|
-
const respHeader: ResponseHeader = {
|
|
498
|
-
statusCode: 0,
|
|
499
|
-
statusMessage: "",
|
|
500
|
-
metadata: {},
|
|
501
|
-
};
|
|
502
|
-
const { stream, writtenChunks } = makeMockBidiStream([
|
|
503
|
-
buildResponseFrame(respHeader),
|
|
504
|
-
buildBodyFrame(responseBody),
|
|
505
|
-
]);
|
|
506
|
-
mockWt.createBidirectionalStream.mockResolvedValue(stream);
|
|
507
|
-
|
|
508
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
509
|
-
const deadlineMs = 5000;
|
|
510
|
-
const deadline = new Date(Date.now() + deadlineMs);
|
|
511
|
-
const call = transport.unary(method, input, {
|
|
512
|
-
meta: {},
|
|
513
|
-
timeout: deadline,
|
|
514
|
-
});
|
|
515
|
-
await call.response;
|
|
516
|
-
|
|
517
|
-
const sent = concat(writtenChunks);
|
|
518
|
-
const { payload: reqHeaderBytes } = parseFrame(sent, 0);
|
|
519
|
-
const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
|
|
520
|
-
// timeoutMs should be roughly 5000 (some drift allowed)
|
|
521
|
-
expect(sentHeader.timeoutMs).toBeGreaterThan(0);
|
|
522
|
-
expect(sentHeader.timeoutMs).toBeLessThanOrEqual(deadlineMs);
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
it("clamps past Date timeout to 0 in RequestHeader.timeoutMs", async () => {
|
|
526
|
-
await conn.connect();
|
|
527
|
-
|
|
528
|
-
const responseBody = new TextEncoder().encode(
|
|
529
|
-
JSON.stringify({ value: "ok" }),
|
|
530
|
-
);
|
|
531
|
-
const respHeader: ResponseHeader = {
|
|
532
|
-
statusCode: 0,
|
|
533
|
-
statusMessage: "",
|
|
534
|
-
metadata: {},
|
|
535
|
-
};
|
|
536
|
-
const { stream, writtenChunks } = makeMockBidiStream([
|
|
537
|
-
buildResponseFrame(respHeader),
|
|
538
|
-
buildBodyFrame(responseBody),
|
|
539
|
-
]);
|
|
540
|
-
mockWt.createBidirectionalStream.mockResolvedValue(stream);
|
|
541
|
-
|
|
542
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
543
|
-
const pastDate = new Date(Date.now() - 10000); // 10 seconds ago
|
|
544
|
-
const call = transport.unary(method, input, {
|
|
545
|
-
meta: {},
|
|
546
|
-
timeout: pastDate,
|
|
547
|
-
});
|
|
548
|
-
await call.response;
|
|
549
|
-
|
|
550
|
-
const sent = concat(writtenChunks);
|
|
551
|
-
const { payload: reqHeaderBytes } = parseFrame(sent, 0);
|
|
552
|
-
const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
|
|
553
|
-
expect(sentHeader.timeoutMs).toBe(0);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it("includes numeric timeout in RequestHeader.timeoutMs", async () => {
|
|
557
|
-
await conn.connect();
|
|
558
|
-
|
|
559
|
-
const responseBody = new TextEncoder().encode(
|
|
560
|
-
JSON.stringify({ value: "ok" }),
|
|
561
|
-
);
|
|
562
|
-
const respHeader: ResponseHeader = {
|
|
563
|
-
statusCode: 0,
|
|
564
|
-
statusMessage: "",
|
|
565
|
-
metadata: {},
|
|
566
|
-
};
|
|
567
|
-
const { stream, writtenChunks } = makeMockBidiStream([
|
|
568
|
-
buildResponseFrame(respHeader),
|
|
569
|
-
buildBodyFrame(responseBody),
|
|
570
|
-
]);
|
|
571
|
-
mockWt.createBidirectionalStream.mockResolvedValue(stream);
|
|
572
|
-
|
|
573
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
574
|
-
const call = transport.unary(method, input, { meta: {}, timeout: 3000 });
|
|
575
|
-
await call.response;
|
|
576
|
-
|
|
577
|
-
const sent = concat(writtenChunks);
|
|
578
|
-
const { payload: reqHeaderBytes } = parseFrame(sent, 0);
|
|
579
|
-
const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
|
|
580
|
-
expect(sentHeader.timeoutMs).toBe(3000);
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
it("uses 'RPC error' as message when statusMessage is empty", async () => {
|
|
584
|
-
await conn.connect();
|
|
585
|
-
|
|
586
|
-
const respHeader: ResponseHeader = {
|
|
587
|
-
statusCode: 5, // NOT_FOUND
|
|
588
|
-
statusMessage: "", // empty — code should fall back to "RPC error"
|
|
589
|
-
metadata: {},
|
|
590
|
-
};
|
|
591
|
-
const { stream } = makeMockBidiStream([buildResponseFrame(respHeader)]);
|
|
592
|
-
mockWt.createBidirectionalStream.mockResolvedValue(stream);
|
|
593
|
-
|
|
594
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
595
|
-
const call = transport.unary(method, input, options);
|
|
596
|
-
|
|
597
|
-
const err = await call.response.catch((e: unknown) => e);
|
|
598
|
-
expect(err).toBeInstanceOf(RpcError);
|
|
599
|
-
expect((err as RpcError).code).toBe("NOT_FOUND");
|
|
600
|
-
expect((err as RpcError).message).toBe("RPC error");
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it("throws RpcError on non-zero gRPC status code", async () => {
|
|
604
|
-
await conn.connect();
|
|
605
|
-
|
|
606
|
-
const respHeader: ResponseHeader = {
|
|
607
|
-
statusCode: 5, // NOT_FOUND
|
|
608
|
-
statusMessage: "record not found",
|
|
609
|
-
metadata: {},
|
|
610
|
-
};
|
|
611
|
-
const { stream } = makeMockBidiStream([buildResponseFrame(respHeader)]);
|
|
612
|
-
mockWt.createBidirectionalStream.mockResolvedValue(stream);
|
|
613
|
-
|
|
614
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
615
|
-
const call = transport.unary(method, input, options);
|
|
616
|
-
|
|
617
|
-
const err = await call.response.catch((e: unknown) => e);
|
|
618
|
-
expect(err).toBeInstanceOf(RpcError);
|
|
619
|
-
expect((err as RpcError).code).toBe("NOT_FOUND");
|
|
620
|
-
expect((err as RpcError).message).toBe("record not found");
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
it("maps all gRPC status codes correctly", async () => {
|
|
624
|
-
const cases: Array<{ code: number; name: string }> = [
|
|
625
|
-
{ code: 1, name: "CANCELLED" },
|
|
626
|
-
{ code: 2, name: "UNKNOWN" },
|
|
627
|
-
{ code: 3, name: "INVALID_ARGUMENT" },
|
|
628
|
-
{ code: 4, name: "DEADLINE_EXCEEDED" },
|
|
629
|
-
{ code: 7, name: "PERMISSION_DENIED" },
|
|
630
|
-
{ code: 14, name: "UNAVAILABLE" },
|
|
631
|
-
{ code: 16, name: "UNAUTHENTICATED" },
|
|
632
|
-
];
|
|
633
|
-
|
|
634
|
-
for (const { code, name } of cases) {
|
|
635
|
-
await conn.connect();
|
|
636
|
-
const respHeader: ResponseHeader = {
|
|
637
|
-
statusCode: code,
|
|
638
|
-
statusMessage: name,
|
|
639
|
-
metadata: {},
|
|
640
|
-
};
|
|
641
|
-
const { stream } = makeMockBidiStream([buildResponseFrame(respHeader)]);
|
|
642
|
-
mockWt.createBidirectionalStream.mockResolvedValue(stream);
|
|
643
|
-
|
|
644
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
645
|
-
const call = transport.unary(method, input, options);
|
|
646
|
-
const err = await call.response.catch((e: unknown) => e);
|
|
647
|
-
|
|
648
|
-
expect((err as RpcError).code).toBe(name);
|
|
649
|
-
conn.close();
|
|
650
|
-
conn = new WebTransportConnection({ url: "https://wt.sid.example.com" });
|
|
651
|
-
}
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
it("falls back to UNKNOWN for unrecognized gRPC status code", async () => {
|
|
655
|
-
await conn.connect();
|
|
656
|
-
|
|
657
|
-
const respHeader: ResponseHeader = {
|
|
658
|
-
statusCode: 999, // not in the grpcCodeName table
|
|
659
|
-
statusMessage: "mystery error",
|
|
660
|
-
metadata: {},
|
|
661
|
-
};
|
|
662
|
-
const { stream } = makeMockBidiStream([buildResponseFrame(respHeader)]);
|
|
663
|
-
mockWt.createBidirectionalStream.mockResolvedValue(stream);
|
|
664
|
-
|
|
665
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
666
|
-
const call = transport.unary(method, input, options);
|
|
667
|
-
|
|
668
|
-
const err = await call.response.catch((e: unknown) => e);
|
|
669
|
-
expect(err).toBeInstanceOf(RpcError);
|
|
670
|
-
expect((err as RpcError).code).toBe("UNKNOWN");
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
it("throws UNAVAILABLE if connection not yet established", async () => {
|
|
674
|
-
// conn not connected
|
|
675
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
676
|
-
const call = transport.unary(method, input, options);
|
|
677
|
-
|
|
678
|
-
const err = await call.response.catch((e: unknown) => e);
|
|
679
|
-
expect(err).toBeInstanceOf(RpcError);
|
|
680
|
-
expect((err as RpcError).code).toBe("UNAVAILABLE");
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
it("throws INTERNAL on stream error (non-RpcError)", async () => {
|
|
684
|
-
await conn.connect();
|
|
685
|
-
mockWt.createBidirectionalStream.mockRejectedValue(
|
|
686
|
-
new Error("stream creation failed"),
|
|
687
|
-
);
|
|
688
|
-
|
|
689
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
690
|
-
const call = transport.unary(method, input, options);
|
|
691
|
-
|
|
692
|
-
const err = await call.response.catch((e: unknown) => e);
|
|
693
|
-
expect(err).toBeInstanceOf(RpcError);
|
|
694
|
-
expect((err as RpcError).code).toBe("INTERNAL");
|
|
695
|
-
expect((err as RpcError).message).toBe("stream creation failed");
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
it("uses String(err) as message when thrown value is not an Error", async () => {
|
|
699
|
-
await conn.connect();
|
|
700
|
-
// Simulate a non-Error non-RpcError thrown value (rare but possible)
|
|
701
|
-
mockWt.createBidirectionalStream.mockRejectedValue("stream aborted");
|
|
702
|
-
|
|
703
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
704
|
-
const call = transport.unary(method, input, options);
|
|
705
|
-
|
|
706
|
-
const err = await call.response.catch((e: unknown) => e);
|
|
707
|
-
expect(err).toBeInstanceOf(RpcError);
|
|
708
|
-
expect((err as RpcError).code).toBe("INTERNAL");
|
|
709
|
-
expect((err as RpcError).message).toBe("stream aborted");
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
it("serverStreaming throws RpcError", () => {
|
|
713
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
714
|
-
expect(() => transport.serverStreaming(method, input, options)).toThrow(
|
|
715
|
-
RpcError,
|
|
716
|
-
);
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
it("clientStreaming throws RpcError", () => {
|
|
720
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
721
|
-
expect(() => transport.clientStreaming(method, options)).toThrow(RpcError);
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
it("duplex throws RpcError", () => {
|
|
725
|
-
const transport = new WebTransportRpcTransport(conn);
|
|
726
|
-
expect(() => transport.duplex(method, options)).toThrow(RpcError);
|
|
727
|
-
});
|
|
728
|
-
});
|