summaryrefslogtreecommitdiffstats
path: root/test/websocket
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
commit0b6210cd37b68b94252cb798598b12974a20e1c1 (patch)
treee371686554a877842d95aa94f100bee552ff2a8e /test/websocket
parentInitial commit. (diff)
downloadnode-undici-upstream.tar.xz
node-undici-upstream.zip
Adding upstream version 5.28.2+dfsg1+~cs23.11.12.3.upstream/5.28.2+dfsg1+_cs23.11.12.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test/websocket')
-rw-r--r--test/websocket/close.js130
-rw-r--r--test/websocket/constructor.js48
-rw-r--r--test/websocket/custom-headers.js30
-rw-r--r--test/websocket/diagnostics-channel.js71
-rw-r--r--test/websocket/events.js204
-rw-r--r--test/websocket/fragments.js40
-rw-r--r--test/websocket/frame.js24
-rw-r--r--test/websocket/opening-handshake.js215
-rw-r--r--test/websocket/ping-pong.js46
-rw-r--r--test/websocket/receive.js60
-rw-r--r--test/websocket/send.js216
-rw-r--r--test/websocket/websocketinit.js45
12 files changed, 1129 insertions, 0 deletions
diff --git a/test/websocket/close.js b/test/websocket/close.js
new file mode 100644
index 0000000..4d314a4
--- /dev/null
+++ b/test/websocket/close.js
@@ -0,0 +1,130 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { WebSocket } = require('../..')
+
+test('Close', (t) => {
+ t.plan(6)
+
+ t.test('Close with code', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('close', (code) => {
+ t.equal(code, 1000)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.addEventListener('open', () => ws.close(1000))
+ })
+
+ t.test('Close with code and reason', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('close', (code, reason) => {
+ t.equal(code, 1000)
+ t.same(reason, Buffer.from('Goodbye'))
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.addEventListener('open', () => ws.close(1000, 'Goodbye'))
+ })
+
+ t.test('Close with invalid code', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.addEventListener('open', () => {
+ t.throws(
+ () => ws.close(2999),
+ {
+ name: 'InvalidAccessError',
+ constructor: DOMException
+ }
+ )
+
+ t.throws(
+ () => ws.close(5000),
+ {
+ name: 'InvalidAccessError',
+ constructor: DOMException
+ }
+ )
+
+ ws.close()
+ })
+ })
+
+ t.test('Close with invalid reason', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ t.throws(
+ () => ws.close(1000, 'a'.repeat(124)),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ ws.close(1000)
+ })
+ })
+
+ t.test('Close with no code or reason', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('close', (code, reason) => {
+ t.equal(code, 1005)
+ t.same(reason, Buffer.alloc(0))
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.addEventListener('open', () => ws.close())
+ })
+
+ t.test('Close with a 3000 status code', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('close', (code, reason) => {
+ t.equal(code, 3000)
+ t.same(reason, Buffer.alloc(0))
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.addEventListener('open', () => ws.close(3000))
+ })
+})
diff --git a/test/websocket/constructor.js b/test/websocket/constructor.js
new file mode 100644
index 0000000..dd87dea
--- /dev/null
+++ b/test/websocket/constructor.js
@@ -0,0 +1,48 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocket } = require('../..')
+
+test('Constructor', (t) => {
+ t.throws(
+ () => new WebSocket('abc'),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ t.throws(
+ () => new WebSocket('wss://echo.websocket.events/#a'),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ t.throws(
+ () => new WebSocket('wss://echo.websocket.events', ''),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ t.throws(
+ () => new WebSocket('wss://echo.websocket.events', ['chat', 'chat']),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ t.throws(
+ () => new WebSocket('wss://echo.websocket.events', ['<>@,;:\\"/[]?={}\t']),
+ {
+ name: 'SyntaxError',
+ constructor: DOMException
+ }
+ )
+
+ t.end()
+})
diff --git a/test/websocket/custom-headers.js b/test/websocket/custom-headers.js
new file mode 100644
index 0000000..01f1830
--- /dev/null
+++ b/test/websocket/custom-headers.js
@@ -0,0 +1,30 @@
+'use strict'
+
+const { test } = require('tap')
+const assert = require('assert')
+const { Agent, WebSocket } = require('../..')
+
+test('Setting custom headers', (t) => {
+ t.plan(1)
+
+ const headers = {
+ 'x-khafra-hello': 'hi',
+ Authorization: 'Bearer base64orsomethingitreallydoesntmatter'
+ }
+
+ class TestAgent extends Agent {
+ dispatch (options) {
+ t.match(options.headers, headers)
+
+ return false
+ }
+ }
+
+ const ws = new WebSocket('wss://echo.websocket.events', {
+ headers,
+ dispatcher: new TestAgent()
+ })
+
+ // We don't want to make a request, just ensure the headers are set.
+ ws.onclose = ws.onerror = ws.onmessage = assert.fail
+})
diff --git a/test/websocket/diagnostics-channel.js b/test/websocket/diagnostics-channel.js
new file mode 100644
index 0000000..c3bf05a
--- /dev/null
+++ b/test/websocket/diagnostics-channel.js
@@ -0,0 +1,71 @@
+'use strict'
+
+const t = require('tap')
+const dc = require('diagnostics_channel')
+const { WebSocketServer } = require('ws')
+const { WebSocket } = require('../..')
+
+t.test('diagnostics channel', { jobs: 1 }, (t) => {
+ t.plan(2)
+
+ t.test('undici:websocket:open', (t) => {
+ t.plan(3)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.close(1000, 'goodbye')
+ })
+
+ const listener = ({ extensions, protocol }) => {
+ t.equal(extensions, null)
+ t.equal(protocol, 'chat')
+ }
+
+ t.teardown(() => {
+ dc.channel('undici:websocket:open').unsubscribe(listener)
+ return server.close()
+ })
+
+ const { port } = server.address()
+
+ dc.channel('undici:websocket:open').subscribe(listener)
+
+ const ws = new WebSocket(`ws://localhost:${port}`, 'chat')
+
+ ws.addEventListener('open', () => {
+ t.pass('Emitted open')
+ })
+ })
+
+ t.test('undici:websocket:close', (t) => {
+ t.plan(4)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.close(1000, 'goodbye')
+ })
+
+ const listener = ({ websocket, code, reason }) => {
+ t.type(websocket, WebSocket)
+ t.equal(code, 1000)
+ t.equal(reason, 'goodbye')
+ }
+
+ t.teardown(() => {
+ dc.channel('undici:websocket:close').unsubscribe(listener)
+ return server.close()
+ })
+
+ const { port } = server.address()
+
+ dc.channel('undici:websocket:close').subscribe(listener)
+
+ const ws = new WebSocket(`ws://localhost:${port}`, 'chat')
+
+ ws.addEventListener('close', () => {
+ t.pass('Emitted open')
+ })
+ })
+})
diff --git a/test/websocket/events.js b/test/websocket/events.js
new file mode 100644
index 0000000..e5b565c
--- /dev/null
+++ b/test/websocket/events.js
@@ -0,0 +1,204 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { MessageEvent, CloseEvent, ErrorEvent } = require('../../lib/websocket/events')
+const { WebSocket } = require('../..')
+
+test('MessageEvent', (t) => {
+ t.throws(() => new MessageEvent(), TypeError, 'no arguments')
+ t.throws(() => new MessageEvent('').initMessageEvent(), TypeError)
+
+ const noInitEvent = new MessageEvent('message')
+
+ t.equal(noInitEvent.origin, '')
+ t.equal(noInitEvent.data, null)
+ t.equal(noInitEvent.lastEventId, '')
+ t.equal(noInitEvent.source, null)
+ t.ok(Array.isArray(noInitEvent.ports))
+ t.ok(Object.isFrozen(noInitEvent.ports))
+ t.type(new MessageEvent('').initMessageEvent('message'), MessageEvent)
+
+ t.end()
+})
+
+test('CloseEvent', (t) => {
+ t.throws(() => new CloseEvent(), TypeError)
+
+ const noInitEvent = new CloseEvent('close')
+
+ t.equal(noInitEvent.wasClean, false)
+ t.equal(noInitEvent.code, 0)
+ t.equal(noInitEvent.reason, '')
+
+ t.end()
+})
+
+test('ErrorEvent', (t) => {
+ t.throws(() => new ErrorEvent(), TypeError)
+
+ const noInitEvent = new ErrorEvent('error')
+
+ t.equal(noInitEvent.message, '')
+ t.equal(noInitEvent.filename, '')
+ t.equal(noInitEvent.lineno, 0)
+ t.equal(noInitEvent.colno, 0)
+ t.equal(noInitEvent.error, undefined)
+
+ t.end()
+})
+
+test('Event handlers', (t) => {
+ t.plan(4)
+
+ const server = new WebSocketServer({ port: 0 })
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ function listen () {}
+
+ t.teardown(server.close.bind(server))
+ t.teardown(() => ws.close())
+
+ t.test('onopen', (t) => {
+ t.plan(3)
+
+ t.equal(ws.onopen, null)
+ ws.onopen = 3
+ t.equal(ws.onopen, null)
+ ws.onopen = listen
+ t.equal(ws.onopen, listen)
+ })
+
+ t.test('onerror', (t) => {
+ t.plan(3)
+
+ t.equal(ws.onerror, null)
+ ws.onerror = 3
+ t.equal(ws.onerror, null)
+ ws.onerror = listen
+ t.equal(ws.onerror, listen)
+ })
+
+ t.test('onclose', (t) => {
+ t.plan(3)
+
+ t.equal(ws.onclose, null)
+ ws.onclose = 3
+ t.equal(ws.onclose, null)
+ ws.onclose = listen
+ t.equal(ws.onclose, listen)
+ })
+
+ t.test('onmessage', (t) => {
+ t.plan(3)
+
+ t.equal(ws.onmessage, null)
+ ws.onmessage = 3
+ t.equal(ws.onmessage, null)
+ ws.onmessage = listen
+ t.equal(ws.onmessage, listen)
+ })
+})
+
+test('CloseEvent WPTs ported', (t) => {
+ t.test('initCloseEvent', (t) => {
+ // Taken from websockets/interfaces/CloseEvent/historical.html
+ t.notOk('initCloseEvent' in CloseEvent.prototype)
+ t.notOk('initCloseEvent' in new CloseEvent('close'))
+
+ t.end()
+ })
+
+ t.test('CloseEvent constructor', (t) => {
+ // Taken from websockets/interfaces/CloseEvent/constructor.html
+
+ {
+ const event = new CloseEvent('foo')
+
+ t.ok(event instanceof CloseEvent, 'should be a CloseEvent')
+ t.equal(event.type, 'foo')
+ t.notOk(event.bubbles, 'bubbles')
+ t.notOk(event.cancelable, 'cancelable')
+ t.notOk(event.wasClean, 'wasClean')
+ t.equal(event.code, 0)
+ t.equal(event.reason, '')
+ }
+
+ {
+ const event = new CloseEvent('foo', {
+ bubbles: true,
+ cancelable: true,
+ wasClean: true,
+ code: 7,
+ reason: 'x'
+ })
+ t.ok(event instanceof CloseEvent, 'should be a CloseEvent')
+ t.equal(event.type, 'foo')
+ t.ok(event.bubbles, 'bubbles')
+ t.ok(event.cancelable, 'cancelable')
+ t.ok(event.wasClean, 'wasClean')
+ t.equal(event.code, 7)
+ t.equal(event.reason, 'x')
+ }
+
+ t.end()
+ })
+
+ t.end()
+})
+
+test('ErrorEvent WPTs ported', (t) => {
+ t.test('Synthetic ErrorEvent', (t) => {
+ // Taken from html/webappapis/scripting/events/event-handler-processing-algorithm-error/document-synthetic-errorevent.html
+
+ {
+ const e = new ErrorEvent('error')
+ t.equal(e.message, '')
+ t.equal(e.filename, '')
+ t.equal(e.lineno, 0)
+ t.equal(e.colno, 0)
+ t.equal(e.error, undefined)
+ }
+
+ {
+ const e = new ErrorEvent('error', { error: null })
+ t.equal(e.error, null)
+ }
+
+ {
+ const e = new ErrorEvent('error', { error: undefined })
+ t.equal(e.error, undefined)
+ }
+
+ {
+ const e = new ErrorEvent('error', { error: 'foo' })
+ t.equal(e.error, 'foo')
+ }
+
+ t.end()
+ })
+
+ t.test('webidl', (t) => {
+ // Taken from webidl/ecmascript-binding/no-regexp-special-casing.any.js
+
+ const regExp = new RegExp()
+ regExp.message = 'some message'
+
+ const errorEvent = new ErrorEvent('type', regExp)
+
+ t.equal(errorEvent.message, 'some message')
+
+ t.end()
+ })
+
+ t.test('initErrorEvent', (t) => {
+ // Taken from workers/Worker_dispatchEvent_ErrorEvent.htm
+
+ const e = new ErrorEvent('error')
+ t.notOk('initErrorEvent' in e, 'should not be supported')
+
+ t.end()
+ })
+
+ t.end()
+})
diff --git a/test/websocket/fragments.js b/test/websocket/fragments.js
new file mode 100644
index 0000000..d51db4b
--- /dev/null
+++ b/test/websocket/fragments.js
@@ -0,0 +1,40 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { WebSocket } = require('../..')
+const diagnosticsChannel = require('diagnostics_channel')
+
+test('Fragmented frame with a ping frame in the middle of it', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ const socket = ws._socket
+
+ socket.write(Buffer.from([0x01, 0x03, 0x48, 0x65, 0x6c])) // Text frame "Hel"
+ socket.write(Buffer.from([0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])) // ping "Hello"
+ socket.write(Buffer.from([0x80, 0x02, 0x6c, 0x6f])) // Text frame "lo"
+ })
+
+ t.teardown(() => {
+ for (const client of server.clients) {
+ client.close()
+ }
+
+ server.close()
+ })
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('message', ({ data }) => {
+ t.same(data, 'Hello')
+
+ ws.close()
+ })
+
+ diagnosticsChannel.channel('undici:websocket:ping').subscribe(
+ ({ payload }) => t.same(payload, Buffer.from('Hello'))
+ )
+})
diff --git a/test/websocket/frame.js b/test/websocket/frame.js
new file mode 100644
index 0000000..b4b73b7
--- /dev/null
+++ b/test/websocket/frame.js
@@ -0,0 +1,24 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebsocketFrameSend } = require('../../lib/websocket/frame')
+const { opcodes } = require('../../lib/websocket/constants')
+
+test('Writing 16-bit frame length value at correct offset when buffer has a non-zero byteOffset', (t) => {
+ /*
+ When writing 16-bit frame lengths, a `DataView` was being used without setting a `byteOffset` into the buffer:
+ i.e. `new DataView(buffer.buffer)` instead of `new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)`.
+ Small `Buffers` returned by `allocUnsafe` are usually returned from the buffer pool, and thus have a non-zero `byteOffset`.
+ Invalid frames were therefore being returned in that case.
+ */
+ t.plan(3)
+
+ const payloadLength = 126 // 126 bytes is the smallest payload to trigger a 16-bit length field
+ const smallBuffer = Buffer.allocUnsafe(1) // make it very likely that the next buffer returned by allocUnsafe DOESN'T have a zero byteOffset
+ const payload = Buffer.allocUnsafe(payloadLength).fill(0)
+ const frame = new WebsocketFrameSend(payload).createFrame(opcodes.BINARY)
+
+ t.equal(frame[2], payloadLength >>> 8)
+ t.equal(frame[3], payloadLength & 0xff)
+ t.equal(smallBuffer.length, 1) // ensure smallBuffer can't be garbage-collected too soon
+})
diff --git a/test/websocket/opening-handshake.js b/test/websocket/opening-handshake.js
new file mode 100644
index 0000000..b9a7989
--- /dev/null
+++ b/test/websocket/opening-handshake.js
@@ -0,0 +1,215 @@
+'use strict'
+
+const { test } = require('tap')
+const { createServer } = require('http')
+const { WebSocketServer } = require('ws')
+const { WebSocket } = require('../..')
+
+test('WebSocket connecting to server that isn\'t a Websocket server', (t) => {
+ t.plan(5)
+
+ const server = createServer((req, res) => {
+ t.equal(req.headers.connection, 'upgrade')
+ t.equal(req.headers.upgrade, 'websocket')
+ t.ok(req.headers['sec-websocket-key'])
+ t.equal(req.headers['sec-websocket-version'], '13')
+
+ res.end()
+ server.unref()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ // Server isn't a websocket server
+ ws.onmessage = ws.onopen = t.fail
+
+ ws.addEventListener('error', t.pass)
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Open event is emitted', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.close(1000)
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.onmessage = ws.onerror = t.fail
+ ws.addEventListener('open', t.pass)
+})
+
+test('Multiple protocols are joined by a comma', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws, req) => {
+ t.equal(req.headers['sec-websocket-protocol'], 'chat, echo')
+
+ ws.close(1000)
+ server.close()
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, ['chat', 'echo'])
+
+ ws.addEventListener('open', () => ws.close())
+})
+
+test('Server doesn\'t send Sec-WebSocket-Protocol header when protocols are used', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.statusCode = 101
+
+ req.socket.destroy()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, 'chat')
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Server sends invalid Upgrade header', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.setHeader('Upgrade', 'NotWebSocket')
+ res.statusCode = 101
+
+ req.socket.destroy()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Server sends invalid Connection header', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.setHeader('Upgrade', 'websocket')
+ res.setHeader('Connection', 'downgrade')
+ res.statusCode = 101
+
+ req.socket.destroy()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Server sends invalid Sec-WebSocket-Accept header', (t) => {
+ t.plan(1)
+
+ const server = createServer((req, res) => {
+ res.setHeader('Upgrade', 'websocket')
+ res.setHeader('Connection', 'upgrade')
+ res.setHeader('Sec-WebSocket-Accept', 'abc')
+ res.statusCode = 101
+
+ req.socket.destroy()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Server sends invalid Sec-WebSocket-Extensions header', (t) => {
+ const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+ const { createHash } = require('crypto')
+
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ const key = req.headers['sec-websocket-key']
+ t.ok(key)
+
+ const accept = createHash('sha1').update(key + uid).digest('base64')
+
+ res.setHeader('Upgrade', 'websocket')
+ res.setHeader('Connection', 'upgrade')
+ res.setHeader('Sec-WebSocket-Accept', accept)
+ res.setHeader('Sec-WebSocket-Extensions', 'InvalidExtension')
+ res.statusCode = 101
+
+ res.end()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
+
+test('Server sends invalid Sec-WebSocket-Extensions header', (t) => {
+ const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+ const { createHash } = require('crypto')
+
+ t.plan(2)
+
+ const server = createServer((req, res) => {
+ const key = req.headers['sec-websocket-key']
+ t.ok(key)
+
+ const accept = createHash('sha1').update(key + uid).digest('base64')
+
+ res.setHeader('Upgrade', 'websocket')
+ res.setHeader('Connection', 'upgrade')
+ res.setHeader('Sec-WebSocket-Accept', accept)
+ res.setHeader('Sec-WebSocket-Protocol', 'echo') // <--
+ res.statusCode = 101
+
+ res.end()
+ }).listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, 'chat')
+
+ ws.onopen = t.fail
+
+ ws.addEventListener('error', ({ error }) => {
+ t.ok(error)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+})
diff --git a/test/websocket/ping-pong.js b/test/websocket/ping-pong.js
new file mode 100644
index 0000000..b7c4694
--- /dev/null
+++ b/test/websocket/ping-pong.js
@@ -0,0 +1,46 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const diagnosticsChannel = require('diagnostics_channel')
+const { WebSocket } = require('../..')
+
+test('Receives ping and parses body', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.ping('Hello, world')
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.onerror = ws.onmessage = t.fail
+
+ diagnosticsChannel.channel('undici:websocket:ping').subscribe(({ payload }) => {
+ t.same(payload, Buffer.from('Hello, world'))
+ ws.close()
+ })
+})
+
+test('Receives pong and parses body', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.pong('Pong')
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+ ws.onerror = ws.onmessage = t.fail
+
+ diagnosticsChannel.channel('undici:websocket:pong').subscribe(({ payload }) => {
+ t.same(payload, Buffer.from('Pong'))
+ ws.close()
+ })
+})
diff --git a/test/websocket/receive.js b/test/websocket/receive.js
new file mode 100644
index 0000000..a669022
--- /dev/null
+++ b/test/websocket/receive.js
@@ -0,0 +1,60 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { WebSocket } = require('../..')
+
+test('Receiving a frame with a payload length > 2^31-1 bytes', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ const socket = ws._socket
+
+ socket.write(Buffer.from([0x81, 0x7F, 0xCA, 0xE5, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]))
+ })
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ t.teardown(() => {
+ ws.close()
+ server.close()
+ })
+
+ ws.onmessage = t.fail
+
+ ws.addEventListener('error', (event) => {
+ t.type(event.error, Error) // error event is emitted
+ })
+})
+
+test('Receiving an ArrayBuffer', (t) => {
+ t.plan(3)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (data, isBinary) => {
+ ws.send(data, { binary: true })
+
+ ws.close(1000)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.binaryType = 'what'
+ t.equal(ws.binaryType, 'blob')
+
+ ws.binaryType = 'arraybuffer' // <--
+ ws.send('Hello')
+ })
+
+ ws.addEventListener('message', ({ data }) => {
+ t.type(data, ArrayBuffer)
+ t.same(Buffer.from(data), Buffer.from('Hello'))
+ })
+})
diff --git a/test/websocket/send.js b/test/websocket/send.js
new file mode 100644
index 0000000..ac295fd
--- /dev/null
+++ b/test/websocket/send.js
@@ -0,0 +1,216 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { Blob } = require('buffer')
+const { WebSocket } = require('../..')
+
+// the following three tests exercise different code paths because of the three
+// different ways a payload length may be specified in a WebSocket frame
+// (https://datatracker.ietf.org/doc/html/rfc6455#section-5.2)
+
+test('Sending >= 2^16 bytes', (t) => {
+ t.plan(3)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (m, isBinary) => {
+ ws.send(m, { binary: isBinary })
+ })
+ })
+
+ const payload = Buffer.allocUnsafe(2 ** 16).fill('Hello')
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send(payload)
+ })
+
+ ws.addEventListener('message', async ({ data }) => {
+ t.type(data, Blob)
+ t.equal(data.size, payload.length)
+ t.same(Buffer.from(await data.arrayBuffer()), payload)
+
+ ws.close()
+ server.close()
+ })
+})
+
+test('Sending >= 126, < 2^16 bytes', (t) => {
+ t.plan(3)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (m, isBinary) => {
+ ws.send(m, { binary: isBinary })
+ })
+ })
+
+ const payload = Buffer.allocUnsafe(126).fill('Hello')
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send(payload)
+ })
+
+ ws.addEventListener('message', async ({ data }) => {
+ t.type(data, Blob)
+ t.equal(data.size, payload.length)
+ t.same(Buffer.from(await data.arrayBuffer()), payload)
+
+ ws.close()
+ server.close()
+ })
+})
+
+test('Sending < 126 bytes', (t) => {
+ t.plan(3)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (m, isBinary) => {
+ ws.send(m, { binary: isBinary })
+ })
+ })
+
+ const payload = Buffer.allocUnsafe(125).fill('Hello')
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send(payload)
+ })
+
+ ws.addEventListener('message', async ({ data }) => {
+ t.type(data, Blob)
+ t.equal(data.size, payload.length)
+ t.same(Buffer.from(await data.arrayBuffer()), payload)
+
+ ws.close()
+ server.close()
+ })
+})
+
+test('Sending data after close', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ t.pass()
+
+ ws.on('message', t.fail)
+ })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.close()
+ ws.send('Some message')
+
+ t.pass()
+ })
+
+ ws.addEventListener('error', t.fail)
+})
+
+test('Sending data before connected', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ t.throws(
+ () => ws.send('Not sent'),
+ {
+ name: 'InvalidStateError',
+ constructor: DOMException
+ }
+ )
+
+ t.equal(ws.readyState, WebSocket.CONNECTING)
+})
+
+test('Sending data to a server', (t) => {
+ t.plan(3)
+
+ t.test('Send with string', (t) => {
+ t.plan(2)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (data, isBinary) => {
+ t.notOk(isBinary, 'Received text frame')
+ t.same(data, Buffer.from('message'))
+
+ ws.close(1000)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send('message')
+ })
+ })
+
+ t.test('Send with ArrayBuffer', (t) => {
+ t.plan(2)
+
+ const message = new TextEncoder().encode('message')
+ const ab = new ArrayBuffer(7)
+ new Uint8Array(ab).set(message)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (data, isBinary) => {
+ t.ok(isBinary)
+ t.same(new Uint8Array(data), message)
+
+ ws.close(1000)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send(ab)
+ })
+ })
+
+ t.test('Send with Blob', (t) => {
+ t.plan(2)
+
+ const blob = new Blob(['hello'])
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.on('message', (data, isBinary) => {
+ t.ok(isBinary)
+ t.same(data, Buffer.from('hello'))
+
+ ws.close(1000)
+ })
+ })
+
+ t.teardown(server.close.bind(server))
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`)
+
+ ws.addEventListener('open', () => {
+ ws.send(blob)
+ })
+ })
+})
diff --git a/test/websocket/websocketinit.js b/test/websocket/websocketinit.js
new file mode 100644
index 0000000..4dda3b4
--- /dev/null
+++ b/test/websocket/websocketinit.js
@@ -0,0 +1,45 @@
+'use strict'
+
+const { test } = require('tap')
+const { WebSocketServer } = require('ws')
+const { WebSocket, Dispatcher, Agent } = require('../..')
+
+test('WebSocketInit', (t) => {
+ t.plan(2)
+
+ class WsDispatcher extends Dispatcher {
+ constructor () {
+ super()
+ this.agent = new Agent()
+ }
+
+ dispatch () {
+ t.pass()
+ return this.agent.dispatch(...arguments)
+ }
+ }
+
+ t.test('WebSocketInit as 2nd param', (t) => {
+ t.plan(1)
+
+ const server = new WebSocketServer({ port: 0 })
+
+ server.on('connection', (ws) => {
+ ws.send(Buffer.from('hello, world'))
+ })
+
+ t.teardown(server.close.bind(server))
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
+ dispatcher: new WsDispatcher()
+ })
+
+ ws.onerror = t.fail
+
+ ws.addEventListener('message', async (event) => {
+ t.equal(await event.data.text(), 'hello, world')
+ server.close()
+ ws.close()
+ })
+ })
+})