Skip to content
File

Blob: src/workerd/api/tests/websocket-constructor-test.js

javascript313 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import {
6 deepStrictEqual,
7 doesNotThrow,
8 strictEqual,
9 throws,
10} from 'node:assert';
11 
12// Test that WebSocket constructor handles empty protocols array correctly.
13// Per the WebSocket spec, an empty protocols array should be valid and equivalent
14// to not specifying any protocols.
15// See: https://github.com/cloudflare/workerd/issues/5822
16export const emptyProtocolsArray = {
17 async test() {
18 const ws = new WebSocket('wss://example.com/', []);
19 strictEqual(ws.url, 'wss://example.com/');
20 strictEqual(ws.protocol, '');
21 strictEqual(ws.readyState, WebSocket.CONNECTING);
22 ws.close();
23 },
24};
25 
26// Test that a single protocol string works
27export const singleProtocolString = {
28 async test() {
29 const ws = new WebSocket('wss://example.com/', 'chat');
30 strictEqual(ws.url, 'wss://example.com/');
31 ws.close();
32 },
33};
34 
35// Test that invalid protocol tokens still throw SyntaxError
36export const invalidProtocolToken = {
37 async test() {
38 throws(
39 () => new WebSocket('wss://example.com/', 'invalid protocol with spaces'),
40 {
41 name: 'SyntaxError',
42 }
43 );
44 },
45};
46 
47// Test that duplicate valid protocols throw SyntaxError
48export const duplicateProtocols = {
49 async test() {
50 throws(() => new WebSocket('wss://example.com/', ['chat', 'chat']), {
51 name: 'SyntaxError',
52 });
53 },
54};
55 
56// Test that close() throws SyntaxError when reason exceeds 123 bytes (UTF-8).
57// Per WHATWG WebSocket spec and RFC 6455 Section 5.5, the close frame body
58// must not exceed 125 bytes (2-byte code + up to 123 bytes of reason).
59export const closeReasonTooLong = {
60 async test() {
61 const ws = new WebSocket('wss://example.com/');
62 // 124 ASCII bytes => 124 UTF-8 bytes, which exceeds the 123-byte limit.
63 const longReason = 'a'.repeat(124);
64 throws(() => ws.close(1000, longReason), {
65 name: 'SyntaxError',
66 });
67 ws.close();
68 },
69};
70 
71// Test that close() accepts a reason of exactly 123 bytes.
72export const closeReasonExact123Bytes = {
73 async test() {
74 const ws = new WebSocket('wss://example.com/');
75 const reason123 = 'a'.repeat(123);
76 doesNotThrow(() => ws.close(1000, reason123));
77 },
78};
79 
80// Test that close() counts UTF-8 bytes, not characters.
81// U+00E9 (é) is 2 bytes in UTF-8. 62 of them = 124 bytes > 123 limit.
82export const closeReasonMultibyteExceeds = {
83 async test() {
84 const ws = new WebSocket('wss://example.com/');
85 const multibyteReason = '\u00e9'.repeat(62); // 62 chars × 2 bytes = 124 bytes
86 throws(() => ws.close(1000, multibyteReason), {
87 name: 'SyntaxError',
88 });
89 ws.close();
90 },
91};
92 
93// Test that close() replaces lone surrogates with U+FFFD per the USVString spec.
94// This is tested end-to-end by the WPT Close-reason-unpaired-surrogates.any.js test.
95// Here we verify the CloseEvent constructor also applies USVString conversion.
96export const closeReasonUSVString = {
97 async test() {
98 // CloseEvent.reason is typed as USVString per the HTML spec,
99 // so lone surrogates must be replaced with U+FFFD.
100 const evt = new CloseEvent('close', { code: 1000, reason: '\uD807' });
101 strictEqual(
102 evt.reason,
103 '\uFFFD',
104 'CloseEvent reason should replace lone surrogate with U+FFFD'
105 );
106 
107 // Multiple surrogates in a string.
108 const evt2 = new CloseEvent('close', {
109 code: 1000,
110 reason: 'hello\uD800world\uDC00end',
111 });
112 strictEqual(
113 evt2.reason,
114 'hello\uFFFDworld\uFFFDend',
115 'CloseEvent reason should replace each lone surrogate with U+FFFD'
116 );
117 
118 // Properly paired surrogates should pass through unchanged.
119 const evt3 = new CloseEvent('close', {
120 code: 1000,
121 reason: '\uD83D\uDE00',
122 });
123 strictEqual(
124 evt3.reason,
125 '\uD83D\uDE00',
126 'Properly paired surrogates should not be replaced'
127 );
128 },
129};
130 
131// Test that close() with code 1000 succeeds (Normal Closure).
132export const closeCode1000Succeeds = {
133 async test() {
134 const ws = new WebSocket('wss://example.com/');
135 doesNotThrow(() => ws.close(1000));
136 },
137};
138 
139// Test that close() with codes in the 3000-4999 range succeeds.
140export const closeCode3000To4999Succeeds = {
141 async test() {
142 for (const code of [3000, 3500, 4000, 4999]) {
143 const ws = new WebSocket('wss://example.com/');
144 doesNotThrow(() => ws.close(code));
145 }
146 },
147};
148 
149// Test that close() rejects codes outside the spec-allowed set.
150// Per WHATWG WebSocket spec, only 1000 and 3000-4999 are valid.
151export const closeCodeSpecInvalid = {
152 async test() {
153 for (const code of [999, 1001, 1004, 1005, 1006, 1015, 2999, 5000]) {
154 const ws = new WebSocket('wss://example.com/');
155 throws(
156 () => ws.close(code),
157 { name: 'InvalidAccessError' },
158 `close(${code}) should throw InvalidAccessError`
159 );
160 ws.close();
161 }
162 },
163};
164 
165// Test that binaryType defaults to "blob" when websocket_standard_binary_type flag is on.
166export const binaryTypeDefaultsToBlob = {
167 async test() {
168 const ws = new WebSocket('wss://example.com/');
169 strictEqual(ws.binaryType, 'blob');
170 ws.close();
171 },
172};
173 
174// Test that binaryType on WebSocketPair also defaults to "blob".
175export const binaryTypeWebSocketPairDefault = {
176 async test() {
177 const pair = new WebSocketPair();
178 strictEqual(pair[0].binaryType, 'blob');
179 strictEqual(pair[1].binaryType, 'blob');
180 // WebSocketPair sockets require accept() before close(), but we only
181 // need to verify the default value so no cleanup is needed.
182 },
183};
184 
185// Test that binaryType setter accepts "arraybuffer" and "blob".
186export const binaryTypeSetterValid = {
187 async test() {
188 const ws = new WebSocket('wss://example.com/');
189 strictEqual(ws.binaryType, 'blob');
190 
191 ws.binaryType = 'arraybuffer';
192 strictEqual(ws.binaryType, 'arraybuffer');
193 
194 ws.binaryType = 'blob';
195 strictEqual(ws.binaryType, 'blob');
196 ws.close();
197 },
198};
199 
200// Test that binaryType setter silently ignores invalid values per spec.
201export const binaryTypeSetterInvalid = {
202 async test() {
203 const ws = new WebSocket('wss://example.com/');
204 strictEqual(ws.binaryType, 'blob');
205 
206 ws.binaryType = 'notBlobOrArrayBuffer';
207 strictEqual(ws.binaryType, 'blob');
208 
209 ws.binaryType = '';
210 strictEqual(ws.binaryType, 'blob');
211 
212 ws.binaryType = 'arraybuffer';
213 strictEqual(ws.binaryType, 'arraybuffer');
214 
215 ws.binaryType = 'BLOB';
216 strictEqual(ws.binaryType, 'arraybuffer');
217 ws.close();
218 },
219};
220 
221// Test that binary messages are delivered as Blob when binaryType is "blob".
222export const binaryMessageDeliveredAsBlob = {
223 async test() {
224 const pair = new WebSocketPair();
225 const [client, server] = pair;
226 server.accept();
227 
228 const data = new Uint8Array([1, 2, 3, 4, 5]);
229 const received = new Promise((resolve) => {
230 server.addEventListener('message', (event) => resolve(event.data));
231 });
232 
233 client.accept();
234 client.send(data);
235 
236 const msg = await received;
237 strictEqual(msg instanceof Blob, true);
238 deepStrictEqual(new Uint8Array(await msg.arrayBuffer()), data);
239 
240 client.close();
241 server.close();
242 },
243};
244 
245// Test that binary messages are delivered as ArrayBuffer when binaryType is "arraybuffer".
246export const binaryMessageDeliveredAsArrayBuffer = {
247 async test() {
248 const pair = new WebSocketPair();
249 const [client, server] = pair;
250 server.accept();
251 server.binaryType = 'arraybuffer';
252 
253 const data = new Uint8Array([10, 20, 30]);
254 const received = new Promise((resolve) => {
255 server.addEventListener('message', (event) => resolve(event.data));
256 });
257 
258 client.accept();
259 client.send(data);
260 
261 const msg = await received;
262 strictEqual(msg instanceof ArrayBuffer, true);
263 deepStrictEqual(new Uint8Array(msg), data);
264 
265 client.close();
266 server.close();
267 },
268};
269 
270// Test that switching binaryType mid-stream changes delivery format.
271export const binaryTypeSwitchMidStream = {
272 async test() {
273 const pair = new WebSocketPair();
274 const [client, server] = pair;
275 server.accept();
276 client.accept();
277 
278 // Default is "blob" with the compat flag.
279 strictEqual(server.binaryType, 'blob');
280 
281 // First message: delivered as Blob.
282 {
283 const received = new Promise((resolve) => {
284 server.addEventListener('message', (event) => resolve(event.data), {
285 once: true,
286 });
287 });
288 client.send(new Uint8Array([1]));
289 const msg = await received;
290 strictEqual(msg instanceof Blob, true);
291 }
292 
293 // Switch to arraybuffer.
294 server.binaryType = 'arraybuffer';
295 
296 // Second message: delivered as ArrayBuffer.
297 {
298 const received = new Promise((resolve) => {
299 server.addEventListener('message', (event) => resolve(event.data), {
300 once: true,
301 });
302 });
303 client.send(new Uint8Array([2]));
304 const msg = await received;
305 strictEqual(msg instanceof ArrayBuffer, true);
306 deepStrictEqual(new Uint8Array(msg), new Uint8Array([2]));
307 }
308 
309 client.close();
310 server.close();
311 },
312};