From 5df45726cc372150206689a4d3618677a25831b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20Caba=C3=A7o?= Date: Wed, 3 Apr 2024 20:32:38 +0100 Subject: [PATCH] feat: Add private channel CRUD operations (#280) * feat: Add channel creation endpoint * Fix the test for createChannel. * improve tests * add apikey to url * add other crud operations * * add strong random crypto for channel name generation * abstract http endpoint definition from socket url to single function * remove random name generation * change update request to patch; auth made with header --------- Co-authored-by: Ivan Vasilov --- package-lock.json | 88 ++- package.json | 2 +- src/RealtimeChannel.ts | 11 +- src/RealtimeClient.ts | 106 ++- src/lib/transformers.ts | 7 + test/socket_test.js | 1442 +++++++++++++++++++++------------------ 6 files changed, 968 insertions(+), 688 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e79ffca..8845b6b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -130,9 +130,9 @@ "dev": true }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -213,9 +213,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -2754,9 +2754,9 @@ "dev": true }, "node_modules/eslint/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3952,9 +3952,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4202,6 +4202,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4433,9 +4439,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4736,6 +4742,43 @@ "path-to-regexp": "^1.7.0" } }, + "node_modules/nock": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", + "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/nock/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nock/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/node-gyp-build": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", @@ -5524,6 +5567,15 @@ "node": ">=0.4.0" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -5851,9 +5903,9 @@ "dev": true }, "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" diff --git a/package.json b/package.json index d8f62139..59258c6a 100644 --- a/package.json +++ b/package.json @@ -58,4 +58,4 @@ "typedoc": "^0.22.16", "typescript": "^4.0.3" } -} +} \ No newline at end of file diff --git a/src/RealtimeChannel.ts b/src/RealtimeChannel.ts index e2555bbe..ac4c6053 100644 --- a/src/RealtimeChannel.ts +++ b/src/RealtimeChannel.ts @@ -11,7 +11,7 @@ import type { RealtimePresenceState, } from './RealtimePresence' import * as Transformers from './lib/transformers' - +import { httpEndpointURL } from './lib/transformers' export type RealtimeChannelOptions = { config: { /** @@ -191,7 +191,8 @@ export default class RealtimeChannel { this.presence = new RealtimePresence(this) - this.broadcastEndpointURL = this._broadcastEndpointURL() + this.broadcastEndpointURL = + httpEndpointURL(this.socket.endPoint) + '/api/broadcast' } /** Subscribe registers your client with the server */ @@ -528,12 +529,6 @@ export default class RealtimeChannel { } /** @internal */ - _broadcastEndpointURL(): string { - let url = this.socket.endPoint - url = url.replace(/^ws/i, 'http') - url = url.replace(/(\/socket\/websocket|\/socket|\/websocket)\/?$/i, '') - return url.replace(/\/+$/, '') + '/api/broadcast' - } async _fetchWithTimeout( url: string, diff --git a/src/RealtimeClient.ts b/src/RealtimeClient.ts index c075ca87..7d2ece9a 100755 --- a/src/RealtimeClient.ts +++ b/src/RealtimeClient.ts @@ -1,20 +1,22 @@ +import type { WebSocket as WSWebSocket } from 'ws' + import { - VSN, CHANNEL_EVENTS, - TRANSPORTS, - SOCKET_STATES, + CONNECTION_STATE, + DEFAULT_HEADERS, DEFAULT_TIMEOUT, + SOCKET_STATES, + TRANSPORTS, + VSN, WS_CLOSE_NORMAL, - DEFAULT_HEADERS, - CONNECTION_STATE, } from './lib/constants' -import Timer from './lib/timer' import Serializer from './lib/serializer' +import Timer from './lib/timer' + +import { httpEndpointURL } from './lib/transformers' import RealtimeChannel from './RealtimeChannel' import type { RealtimeChannelOptions } from './RealtimeChannel' -import type { WebSocket as WSWebSocket } from 'ws' - type Fetch = typeof fetch export type RealtimeClientOptions = { @@ -66,6 +68,7 @@ export default class RealtimeClient { apiKey: string | null = null channels: RealtimeChannel[] = [] endPoint: string = '' + httpEndpoint: string = '' headers?: { [key: string]: string } = DEFAULT_HEADERS params?: { [key: string]: string } = {} timeout: number = DEFAULT_TIMEOUT @@ -99,6 +102,7 @@ export default class RealtimeClient { * Initializes the Socket. * * @param endPoint The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (inherited host & protocol) + * @param httpEndpoint The string HTTP endpoint, ie, "https://example.com", "/" (inherited host & protocol) * @param options.transport The Websocket Transport, for example WebSocket. * @param options.timeout The default timeout in milliseconds to trigger push timeouts. * @param options.params The optional params to pass when connecting. @@ -111,7 +115,7 @@ export default class RealtimeClient { */ constructor(endPoint: string, options?: RealtimeClientOptions) { this.endPoint = `${endPoint}/${TRANSPORTS.websocket}` - + this.httpEndpoint = httpEndpointURL(endPoint) if (options?.transport) { this.transport = options.transport } else { @@ -317,6 +321,90 @@ export default class RealtimeClient { }) } + /** + * Creates a private channel to be used with the provided name. + * + * @param name Channel name to create + */ + createPrivateChannel(name: string): Promise { + const url = `${this.httpEndpoint}/channels` + return this.fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.accessToken}`, + }, + body: JSON.stringify({ name }), + }).then((response) => { + if (!response.ok && response.status !== 200) { + throw new Error(response.statusText) + } + return name + }) + } + + /** + * Deletes a private channel + * + * @param name Channel name to delete. + */ + deletePrivateChannel(name: string): Promise { + const url = `${this.httpEndpoint}/channels/${name}` + return this.fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.accessToken}`, + }, + body: JSON.stringify({ name }), + }).then((response) => { + if (!response.ok && response.status !== 202) { + throw new Error(response.statusText) + } + return true + }) + } + + /** + * Update a private channel + * + * @param name Channel name to update. + * @param new_name New channel name. + */ + updatePrivateChannel(name: string, new_name: string): Promise { + const url = `${this.httpEndpoint}/channels/${name}` + return this.fetch(url, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.accessToken}`, + }, + body: JSON.stringify({ name: new_name }), + }).then((response) => { + if (!response.ok && response.status !== 202) { + throw new Error(response.statusText) + } + return new_name + }) + } + + /** + * Lists private channels + */ + listPrivateChannels(): Promise { + const url = `${this.httpEndpoint}/channels` + return this.fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }).then((response) => { + if (!response.ok && response.status !== 200) { + throw new Error(response.statusText) + } + return response.json() + }) + } /** * Use either custom fetch, if provided, or default fetch to make HTTP requests * diff --git a/src/lib/transformers.ts b/src/lib/transformers.ts index 6dfa8c9d..b170d91c 100644 --- a/src/lib/transformers.ts +++ b/src/lib/transformers.ts @@ -245,3 +245,10 @@ export const toTimestampString = (value: RecordValue): RecordValue => { return value } + +export const httpEndpointURL = (socketUrl: string): string => { + let url = socketUrl + url = url.replace(/^ws/i, 'http') + url = url.replace(/(\/socket\/websocket|\/socket|\/websocket)\/?$/i, '') + return url.replace(/\/+$/, '') +} diff --git a/test/socket_test.js b/test/socket_test.js index ab8568a7..a58c613b 100755 --- a/test/socket_test.js +++ b/test/socket_test.js @@ -1,856 +1,994 @@ import assert from 'assert' import { Server as WebSocketServer, WebSocket } from 'mock-socket' import sinon from 'sinon' -import { WebSocket as WSWebSocket } from 'ws' import { RealtimeClient } from '../dist/main' - let socket describe('constructor', () => { - before(() => { - window.XMLHttpRequest = sinon.useFakeXMLHttpRequest() - }) - - afterEach(() => { - socket.disconnect() - }) + before(() => { + window.XMLHttpRequest = sinon.useFakeXMLHttpRequest() + }) + + afterEach(() => { + socket.disconnect() + }) + + after(() => { + window.XMLHttpRequest = null + }) + + it('sets defaults', () => { + socket = new RealtimeClient('wss://example.com/socket') + + assert.equal(socket.channels.length, 0) + assert.equal(socket.sendBuffer.length, 0) + assert.equal(socket.ref, 0) + assert.equal(socket.endPoint, 'wss://example.com/socket/websocket') + assert.deepEqual(socket.stateChangeCallbacks, { + open: [], + close: [], + error: [], + message: [], + }) + assert.equal(socket.transport, null) + assert.equal(socket.timeout, 10000) + assert.equal(socket.heartbeatIntervalMs, 30000) + assert.equal(typeof socket.logger, 'function') + assert.equal(typeof socket.reconnectAfterMs, 'function') + }) + + it('overrides some defaults with options', () => { + const customTransport = function transport() {} + const customLogger = function logger() {} + const customReconnect = function reconnect() {} + + socket = new RealtimeClient('wss://example.com/socket', { + timeout: 40000, + heartbeatIntervalMs: 60000, + transport: customTransport, + logger: customLogger, + reconnectAfterMs: customReconnect, + params: { one: 'two' }, + }) + + assert.equal(socket.timeout, 40000) + assert.equal(socket.heartbeatIntervalMs, 60000) + assert.equal(socket.transport, customTransport) + assert.equal(socket.logger, customLogger) + assert.equal(socket.reconnectAfterMs, customReconnect) + assert.deepEqual(socket.params, { one: 'two' }) + }) + + describe('with Websocket', () => { + let mockServer - after(() => { - window.XMLHttpRequest = null + before(() => { + mockServer = new WebSocketServer('wss://example.com/') }) - it('sets defaults', () => { - socket = new RealtimeClient('wss://example.com/socket') - - assert.equal(socket.channels.length, 0) - assert.equal(socket.sendBuffer.length, 0) - assert.equal(socket.ref, 0) - assert.equal(socket.endPoint, 'wss://example.com/socket/websocket') - assert.deepEqual(socket.stateChangeCallbacks, { - open: [], - close: [], - error: [], - message: [], - }) - assert.equal(socket.transport, null) - assert.equal(socket.timeout, 10000) - assert.equal(socket.heartbeatIntervalMs, 30000) - assert.equal(typeof socket.logger, 'function') - assert.equal(typeof socket.reconnectAfterMs, 'function') + after((done) => { + mockServer.stop(() => { + window.WebSocket = null + done() + }) }) - it('overrides some defaults with options', () => { - const customTransport = function transport() { } - const customLogger = function logger() { } - const customReconnect = function reconnect() { } - - socket = new RealtimeClient('wss://example.com/socket', { - timeout: 40000, - heartbeatIntervalMs: 60000, - transport: customTransport, - logger: customLogger, - reconnectAfterMs: customReconnect, - params: { one: 'two' }, - }) - - assert.equal(socket.timeout, 40000) - assert.equal(socket.heartbeatIntervalMs, 60000) - assert.equal(socket.transport, customTransport) - assert.equal(socket.logger, customLogger) - assert.equal(socket.reconnectAfterMs, customReconnect) - assert.deepEqual(socket.params, { one: 'two' }) + afterEach(() => { + socket.disconnect() }) - describe('with Websocket', () => { - let mockServer - - before(() => { - mockServer = new WebSocketServer('wss://example.com/') - }) - - after((done) => { - mockServer.stop(() => { - window.WebSocket = null - done() - }) - }) - - afterEach(() => { - socket.disconnect() - }) - - it('defaults to Websocket transport if available', () => { - socket = new RealtimeClient('wss://example.com/socket') - assert.equal(socket.transport, null) - }) + it('defaults to Websocket transport if available', () => { + socket = new RealtimeClient('wss://example.com/socket') + assert.equal(socket.transport, null) }) + }) }) describe('endpointURL', () => { - afterEach(() => { - socket.disconnect() - }) - - it('returns endpoint for given full url', () => { - socket = new RealtimeClient('wss://example.org/chat') - assert.equal( - socket._endPointURL(), - 'wss://example.org/chat/websocket?vsn=1.0.0' - ) - }) - - it('returns endpoint with parameters', () => { - socket = new RealtimeClient('ws://example.org/chat', { params: { foo: 'bar' } }) - assert.equal( - socket._endPointURL(), - 'ws://example.org/chat/websocket?foo=bar&vsn=1.0.0' - ) - }) - - it('returns endpoint with apikey', () => { - socket = new RealtimeClient('ws://example.org/chat', { - params: { apikey: '123456789' }, - }) - assert.equal( - socket._endPointURL(), - 'ws://example.org/chat/websocket?apikey=123456789&vsn=1.0.0' - ) - }) + afterEach(() => { + socket.disconnect() + }) + + it('returns endpoint for given full url', () => { + socket = new RealtimeClient('wss://example.org/chat') + assert.equal( + socket._endPointURL(), + 'wss://example.org/chat/websocket?vsn=1.0.0' + ) + }) + + it('returns endpoint with parameters', () => { + socket = new RealtimeClient('ws://example.org/chat', { + params: { foo: 'bar' }, + }) + assert.equal( + socket._endPointURL(), + 'ws://example.org/chat/websocket?foo=bar&vsn=1.0.0' + ) + }) + + it('returns endpoint with apikey', () => { + socket = new RealtimeClient('ws://example.org/chat', { + params: { apikey: '123456789' }, + }) + assert.equal( + socket._endPointURL(), + 'ws://example.org/chat/websocket?apikey=123456789&vsn=1.0.0' + ) + }) }) describe('connect with WebSocket', () => { - let mockServer + let mockServer - before(() => { - mockServer = new WebSocketServer('wss://example.com/') - }) + before(() => { + mockServer = new WebSocketServer('wss://example.com/') + }) - after((done) => { - mockServer.stop(() => { - window.WebSocket = null - done() - }) + after((done) => { + mockServer.stop(() => { + window.WebSocket = null + done() }) + }) - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket') - }) + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket') + }) - afterEach(() => { - socket.disconnect() - }) + afterEach(() => { + socket.disconnect() + }) - it('establishes websocket connection with endpoint', () => { - socket.connect() + it('establishes websocket connection with endpoint', () => { + socket.connect() - let conn = socket.conn - assert.equal(conn.url, socket._endPointURL()) - }) + let conn = socket.conn + assert.equal(conn.url, socket._endPointURL()) + }) - it('is idempotent', () => { - socket.connect() + it('is idempotent', () => { + socket.connect() - let conn = socket.conn + let conn = socket.conn - socket.connect() + socket.connect() - assert.deepStrictEqual(conn, socket.conn) - }) + assert.deepStrictEqual(conn, socket.conn) + }) }) describe('disconnect', () => { - let mockServer + let mockServer - before(() => { - mockServer = new WebSocketServer('wss://example.com/') - }) + before(() => { + mockServer = new WebSocketServer('wss://example.com/') + }) - after((done) => { - mockServer.stop(() => { - window.WebSocket = null - done() - }) + after((done) => { + mockServer.stop(() => { + window.WebSocket = null + done() }) + }) - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket') - }) + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket') + }) - afterEach(() => { - socket.disconnect() - }) + afterEach(() => { + socket.disconnect() + }) - it('removes existing connection', () => { - socket.connect() - socket.disconnect() + it('removes existing connection', () => { + socket.connect() + socket.disconnect() - assert.equal(socket.conn, null) - }) + assert.equal(socket.conn, null) + }) - it('calls callback', () => { - let count = 0 - socket.connect() - socket.disconnect() - count++ + it('calls callback', () => { + let count = 0 + socket.connect() + socket.disconnect() + count++ - assert.equal(count, 1) - }) + assert.equal(count, 1) + }) - it('calls connection close callback', () => { - socket.connect() - const spy = sinon.spy(socket.conn, 'close') + it('calls connection close callback', () => { + socket.connect() + const spy = sinon.spy(socket.conn, 'close') - socket.disconnect('code', 'reason') + socket.disconnect('code', 'reason') - assert(spy.calledWith('code', 'reason')) - }) + assert(spy.calledWith('code', 'reason')) + }) - it('does not throw when no connection', () => { - assert.doesNotThrow(() => { - socket.disconnect() - }) + it('does not throw when no connection', () => { + assert.doesNotThrow(() => { + socket.disconnect() }) + }) }) describe('connectionState', () => { - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket') - }) - - afterEach(() => { - socket.disconnect() - }) - - it('defaults to closed', () => { - assert.equal(socket.connectionState(), 'closed') - }) - - // TODO: fix for WSWebSocket - it.skip('returns closed if readyState unrecognized', () => { - socket.connect() - - socket.conn.readyState = 5678 - assert.equal(socket.connectionState(), 'closed') - }) - - // TODO: fix for WSWebSocket - it.skip('returns connecting', () => { - socket.connect() - - socket.conn.readyState = 0 - assert.equal(socket.connectionState(), 'connecting') - assert.ok(!socket.isConnected(), 'is not connected') - }) - - // TODO: fix for WSWebSocket - it.skip('returns open', () => { - socket.connect() - - socket.conn.readyState = 1 - assert.equal(socket.connectionState(), 'open') - assert.ok(socket.isConnected(), 'is connected') - }) - - // TODO: fix for WSWebSocket - it.skip('returns closing', () => { - socket.connect() - - socket.conn.readyState = 2 - assert.equal(socket.connectionState(), 'closing') - assert.ok(!socket.isConnected(), 'is not connected') - }) - - // TODO: fix for WSWebSocket - it.skip('returns closed', () => { - socket.connect() - - socket.conn.readyState = 3 - assert.equal(socket.connectionState(), 'closed') - assert.ok(!socket.isConnected(), 'is not connected') - }) + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket') + }) + + afterEach(() => { + socket.disconnect() + }) + + it('defaults to closed', () => { + assert.equal(socket.connectionState(), 'closed') + }) + + // TODO: fix for WSWebSocket + it.skip('returns closed if readyState unrecognized', () => { + socket.connect() + + socket.conn.readyState = 5678 + assert.equal(socket.connectionState(), 'closed') + }) + + // TODO: fix for WSWebSocket + it.skip('returns connecting', () => { + socket.connect() + + socket.conn.readyState = 0 + assert.equal(socket.connectionState(), 'connecting') + assert.ok(!socket.isConnected(), 'is not connected') + }) + + // TODO: fix for WSWebSocket + it.skip('returns open', () => { + socket.connect() + + socket.conn.readyState = 1 + assert.equal(socket.connectionState(), 'open') + assert.ok(socket.isConnected(), 'is connected') + }) + + // TODO: fix for WSWebSocket + it.skip('returns closing', () => { + socket.connect() + + socket.conn.readyState = 2 + assert.equal(socket.connectionState(), 'closing') + assert.ok(!socket.isConnected(), 'is not connected') + }) + + // TODO: fix for WSWebSocket + it.skip('returns closed', () => { + socket.connect() + + socket.conn.readyState = 3 + assert.equal(socket.connectionState(), 'closed') + assert.ok(!socket.isConnected(), 'is not connected') + }) }) describe('channel', () => { - let channel + let channel - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket') - }) + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket') + }) - afterEach(() => { - socket.disconnect() - }) + afterEach(() => { + socket.disconnect() + }) - it('returns channel with given topic and params', () => { - channel = socket.channel('topic', { one: 'two' }) + it('returns channel with given topic and params', () => { + channel = socket.channel('topic', { one: 'two' }) - assert.deepStrictEqual(channel.socket, socket) - assert.equal(channel.topic, 'realtime:topic') - assert.deepEqual(channel.params, { config: { broadcast: { ack: false, self: false }, presence: { key: '' } }, one: 'two' }) + assert.deepStrictEqual(channel.socket, socket) + assert.equal(channel.topic, 'realtime:topic') + assert.deepEqual(channel.params, { + config: { broadcast: { ack: false, self: false }, presence: { key: '' } }, + one: 'two', }) + }) - it('adds channel to sockets channels list', () => { - assert.equal(socket.channels.length, 0) + it('adds channel to sockets channels list', () => { + assert.equal(socket.channels.length, 0) - channel = socket.channel('topic', { one: 'two' }) + channel = socket.channel('topic', { one: 'two' }) - assert.equal(socket.channels.length, 1) + assert.equal(socket.channels.length, 1) - const [foundChannel] = socket.channels - assert.deepStrictEqual(foundChannel, channel) - }) + const [foundChannel] = socket.channels + assert.deepStrictEqual(foundChannel, channel) + }) - it('gets all channels', () => { - assert.equal(socket.getChannels().length, 0) + it('gets all channels', () => { + assert.equal(socket.getChannels().length, 0) - const chan1 = socket.channel('chan1', { one: 'two' }) - const chan2 = socket.channel('chan2', { one: 'two' }) + const chan1 = socket.channel('chan1', { one: 'two' }) + const chan2 = socket.channel('chan2', { one: 'two' }) - assert.deepEqual(socket.getChannels(), [chan1, chan2]) - }) + assert.deepEqual(socket.getChannels(), [chan1, chan2]) + }) - it('removes a channel', async () => { - const connectStub = sinon.stub(socket, 'connect') - const disconnectStub = sinon.stub(socket, 'disconnect') + it('removes a channel', async () => { + const connectStub = sinon.stub(socket, 'connect') + const disconnectStub = sinon.stub(socket, 'disconnect') - channel = socket.channel('topic', { one: 'two' }).subscribe() + channel = socket.channel('topic', { one: 'two' }).subscribe() - assert.equal(socket.channels.length, 1) - assert.ok(connectStub.called) + assert.equal(socket.channels.length, 1) + assert.ok(connectStub.called) - await socket.removeChannel(channel) + await socket.removeChannel(channel) - assert.equal(socket.channels.length, 0) - assert.ok(disconnectStub.called) - }) + assert.equal(socket.channels.length, 0) + assert.ok(disconnectStub.called) + }) - it('removes all channels', async () => { - const disconnectStub = sinon.stub(socket, 'disconnect') + it('removes all channels', async () => { + const disconnectStub = sinon.stub(socket, 'disconnect') - socket.channel('chan1', { one: 'two' }).subscribe() - socket.channel('chan2', { one: 'two' }).subscribe() + socket.channel('chan1', { one: 'two' }).subscribe() + socket.channel('chan2', { one: 'two' }).subscribe() - assert.equal(socket.channels.length, 2) + assert.equal(socket.channels.length, 2) - await socket.removeAllChannels() + await socket.removeAllChannels() - assert.equal(socket.channels.length, 0) - assert.ok(disconnectStub.called) - }) + assert.equal(socket.channels.length, 0) + assert.ok(disconnectStub.called) + }) }) describe('leaveOpenTopic', () => { - let channel1 - let channel2 - - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket') - }) - - afterEach(() => { - channel1.unsubscribe() - channel2.unsubscribe() - socket.disconnect() - }) - - it('enforces client to subscribe to unique topics', () => { - channel1 = socket.channel('topic', { one: 'two' }) - channel2 = socket.channel('topic', { one: 'two' }) - channel1.subscribe() - channel2.subscribe() - - assert.equal(socket.channels.length, 1) - assert.equal(socket.channels[0].topic, 'realtime:topic') - }) + let channel1 + let channel2 + + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket') + }) + + afterEach(() => { + channel1.unsubscribe() + channel2.unsubscribe() + socket.disconnect() + }) + + it('enforces client to subscribe to unique topics', () => { + channel1 = socket.channel('topic', { one: 'two' }) + channel2 = socket.channel('topic', { one: 'two' }) + channel1.subscribe() + channel2.subscribe() + + assert.equal(socket.channels.length, 1) + assert.equal(socket.channels[0].topic, 'realtime:topic') + }) }) describe('remove', () => { - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket') - }) + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket') + }) - afterEach(() => { - socket.disconnect() - }) + afterEach(() => { + socket.disconnect() + }) - it('removes given channel from channels', () => { - const channel1 = socket.channel('topic-1') - const channel2 = socket.channel('topic-2') + it('removes given channel from channels', () => { + const channel1 = socket.channel('topic-1') + const channel2 = socket.channel('topic-2') - sinon.stub(channel1, '_joinRef').returns(1) - sinon.stub(channel2, '_joinRef').returns(2) + sinon.stub(channel1, '_joinRef').returns(1) + sinon.stub(channel2, '_joinRef').returns(2) - socket._remove(channel1) + socket._remove(channel1) - assert.equal(socket.channels.length, 1) + assert.equal(socket.channels.length, 1) - const [foundChannel] = socket.channels - assert.deepStrictEqual(foundChannel, channel2) - }) + const [foundChannel] = socket.channels + assert.deepStrictEqual(foundChannel, channel2) + }) }) describe('push', () => { - const data = { - topic: 'topic', - event: 'event', - payload: 'payload', - ref: 'ref', - } - const json = - '{"topic":"topic","event":"event","payload":"payload","ref":"ref"}' + const data = { + topic: 'topic', + event: 'event', + payload: 'payload', + ref: 'ref', + } + const json = + '{"topic":"topic","event":"event","payload":"payload","ref":"ref"}' - before(() => { - window.XMLHttpRequest = sinon.useFakeXMLHttpRequest() - }) + before(() => { + window.XMLHttpRequest = sinon.useFakeXMLHttpRequest() + }) - after(() => { - window.XMLHttpRequest = null - }) + after(() => { + window.XMLHttpRequest = null + }) - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket') - }) + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket') + }) - afterEach(() => { - socket.disconnect() - }) + afterEach(() => { + socket.disconnect() + }) - // TODO: fix for WSWebSocket - it.skip('sends data to connection when connected', () => { - socket.connect() - socket.conn.readyState = 1 // open + // TODO: fix for WSWebSocket + it.skip('sends data to connection when connected', () => { + socket.connect() + socket.conn.readyState = 1 // open - const spy = sinon.spy(socket.conn, 'send') + const spy = sinon.spy(socket.conn, 'send') - socket.push(data) + socket.push(data) - assert.ok(spy.calledWith(json)) - }) + assert.ok(spy.calledWith(json)) + }) - // TODO: fix for WSWebSocket - it.skip('buffers data when not connected', () => { - socket.connect() - socket.conn.readyState = 0 // connecting + // TODO: fix for WSWebSocket + it.skip('buffers data when not connected', () => { + socket.connect() + socket.conn.readyState = 0 // connecting - const spy = sinon.spy(socket.conn, 'send') + const spy = sinon.spy(socket.conn, 'send') - assert.equal(socket.sendBuffer.length, 0) + assert.equal(socket.sendBuffer.length, 0) - socket.push(data) + socket.push(data) - assert.ok(spy.neverCalledWith(json)) - assert.equal(socket.sendBuffer.length, 1) + assert.ok(spy.neverCalledWith(json)) + assert.equal(socket.sendBuffer.length, 1) - const [callback] = socket.sendBuffer - callback() - assert.ok(spy.calledWith(json)) - }) + const [callback] = socket.sendBuffer + callback() + assert.ok(spy.calledWith(json)) + }) }) describe('makeRef', () => { - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket') - }) - - afterEach(() => { - socket.disconnect() - }) - - it('returns next message ref', () => { - assert.strictEqual(socket.ref, 0) - assert.strictEqual(socket._makeRef(), '1') - assert.strictEqual(socket.ref, 1) - assert.strictEqual(socket._makeRef(), '2') - assert.strictEqual(socket.ref, 2) - }) - - it('restarts for overflow', () => { - socket.ref = Number.MAX_SAFE_INTEGER + 1 - - assert.strictEqual(socket._makeRef(), '0') - assert.strictEqual(socket.ref, 0) - }) + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket') + }) + + afterEach(() => { + socket.disconnect() + }) + + it('returns next message ref', () => { + assert.strictEqual(socket.ref, 0) + assert.strictEqual(socket._makeRef(), '1') + assert.strictEqual(socket.ref, 1) + assert.strictEqual(socket._makeRef(), '2') + assert.strictEqual(socket.ref, 2) + }) + + it('restarts for overflow', () => { + socket.ref = Number.MAX_SAFE_INTEGER + 1 + + assert.strictEqual(socket._makeRef(), '0') + assert.strictEqual(socket.ref, 0) + }) }) describe('setAuth', () => { - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket') - }) + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket') + }) + + afterEach(() => { + socket.removeAllChannels() + }) + + it("sets access token, updates channels' join payload, and pushes token to channels", () => { + const channel1 = socket.channel('test-topic') + const channel2 = socket.channel('test-topic') + const channel3 = socket.channel('test-topic') + + channel1.joinedOnce = true + channel1.state = 'joined' + channel2.joinedOnce = false + channel2.state = 'closed' + channel3.joinedOnce = true + channel3.state = 'joined' + + const pushStub1 = sinon.stub(channel1, '_push') + const pushStub2 = sinon.stub(channel2, '_push') + const pushStub3 = sinon.stub(channel3, '_push') + + const payloadStub1 = sinon.stub(channel1, 'updateJoinPayload') + const payloadStub2 = sinon.stub(channel2, 'updateJoinPayload') + const payloadStub3 = sinon.stub(channel3, 'updateJoinPayload') + + socket.setAuth('token123') + + assert.strictEqual(socket.accessToken, 'token123') + assert.ok( + pushStub1.calledWith('access_token', { + access_token: 'token123', + }) + ) + assert.ok( + !pushStub2.calledWith('access_token', { + access_token: 'token123', + }) + ) + assert.ok( + pushStub3.calledWith('access_token', { + access_token: 'token123', + }) + ) + assert.ok(payloadStub1.calledWith({ access_token: 'token123' })) + assert.ok(payloadStub2.calledWith({ access_token: 'token123' })) + assert.ok(payloadStub3.calledWith({ access_token: 'token123' })) + }) +}) - afterEach(() => { - socket.removeAllChannels() - }) +describe('sendHeartbeat', () => { + before(() => { + window.XMLHttpRequest = sinon.useFakeXMLHttpRequest() + }) + + after(() => { + window.XMLHttpRequest = null + }) + + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket') + socket.connect() + }) + + afterEach(() => { + socket.disconnect() + }) + + // TODO: fix for WSWebSocket + it.skip("closes socket when heartbeat is not ack'd within heartbeat window", () => { + let closed = false + socket.conn.readyState = 1 // open + socket.conn.onclose = () => (closed = true) + socket.sendHeartbeat() + assert.equal(closed, false) + + socket.sendHeartbeat() + assert.equal(closed, true) + }) + + // TODO: fix for WSWebSocket + it.skip('pushes heartbeat data when connected', () => { + socket.conn.readyState = 1 // open + + const spy = sinon.spy(socket.conn, 'send') + const data = + '{"topic":"phoenix","event":"heartbeat","payload":{},"ref":"1"}' + + socket.sendHeartbeat() + assert.ok(spy.calledWith(data)) + }) + + // TODO: fix for WSWebSocket + it.skip('no ops when not connected', () => { + socket.conn.readyState = 0 // connecting + + const spy = sinon.spy(socket.conn, 'send') + const data = + '{"topic":"phoenix","event":"heartbeat","payload":{},"ref":"1"}' + + socket.sendHeartbeat() + assert.ok(spy.neverCalledWith(data)) + }) +}) - it("sets access token, updates channels' join payload, and pushes token to channels", () => { - const channel1 = socket.channel('test-topic') - const channel2 = socket.channel('test-topic') - const channel3 = socket.channel('test-topic') - - channel1.joinedOnce = true - channel1.state = 'joined' - channel2.joinedOnce = false - channel2.state = 'closed' - channel3.joinedOnce = true - channel3.state = 'joined' - - const pushStub1 = sinon.stub(channel1, '_push') - const pushStub2 = sinon.stub(channel2, '_push') - const pushStub3 = sinon.stub(channel3, '_push') - - const payloadStub1 = sinon.stub(channel1, 'updateJoinPayload') - const payloadStub2 = sinon.stub(channel2, 'updateJoinPayload') - const payloadStub3 = sinon.stub(channel3, 'updateJoinPayload') - - socket.setAuth('token123') - - assert.strictEqual(socket.accessToken, 'token123') - assert.ok(pushStub1.calledWith('access_token', { - access_token: 'token123', - })) - assert.ok(!pushStub2.calledWith('access_token', { - access_token: 'token123', - })) - assert.ok(pushStub3.calledWith('access_token', { - access_token: 'token123', - })) - assert.ok(payloadStub1.calledWith({ access_token: 'token123' })) - assert.ok(payloadStub2.calledWith({ access_token: 'token123' })) - assert.ok(payloadStub3.calledWith({ access_token: 'token123' })) - }) +describe('flushSendBuffer', () => { + before(() => { + window.XMLHttpRequest = sinon.useFakeXMLHttpRequest() + }) + + after(() => { + window.XMLHttpRequest = null + }) + + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket') + socket.connect() + }) + + afterEach(() => { + socket.disconnect() + }) + + // TODO: fix for WSWebSocket + it.skip('calls callbacks in buffer when connected', () => { + socket.conn.readyState = 1 // open + const spy1 = sinon.spy() + const spy2 = sinon.spy() + const spy3 = sinon.spy() + socket.sendBuffer.push(spy1) + socket.sendBuffer.push(spy2) + + socket.flushSendBuffer() + + assert.ok(spy1.calledOnce) + assert.ok(spy2.calledOnce) + assert.equal(spy3.callCount, 0) + }) + + // TODO: fix for WSWebSocket + it.skip('empties sendBuffer', () => { + socket.conn.readyState = 1 // open + socket.sendBuffer.push(() => {}) + + socket.flushSendBuffer() + + assert.deepEqual(socket.sendBuffer.length, 0) + }) }) -describe('sendHeartbeat', () => { - before(() => { - window.XMLHttpRequest = sinon.useFakeXMLHttpRequest() - }) +describe('_onConnOpen', () => { + let mockServer - after(() => { - window.XMLHttpRequest = null - }) + before(() => { + mockServer = new WebSocketServer('wss://example.com/') + }) - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket') - socket.connect() + after((done) => { + mockServer.stop(() => { + window.WebSocket = null + done() }) + }) - afterEach(() => { - socket.disconnect() + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket', { + reconnectAfterMs: () => 100000, }) + socket.connect() + }) - // TODO: fix for WSWebSocket - it.skip("closes socket when heartbeat is not ack'd within heartbeat window", () => { - let closed = false - socket.conn.readyState = 1 // open - socket.conn.onclose = () => (closed = true) - socket.sendHeartbeat() - assert.equal(closed, false) + afterEach(() => { + socket.disconnect() + }) - socket.sendHeartbeat() - assert.equal(closed, true) - }) + // TODO: fix for WSWebSocket - // TODO: fix for WSWebSocket - it.skip('pushes heartbeat data when connected', () => { - socket.conn.readyState = 1 // open + it.skip('flushes the send buffer', () => { + socket.conn.readyState = 1 // open + const spy = sinon.spy() + socket.sendBuffer.push(spy) - const spy = sinon.spy(socket.conn, 'send') - const data = - '{"topic":"phoenix","event":"heartbeat","payload":{},"ref":"1"}' + socket._onConnOpen() - socket.sendHeartbeat() - assert.ok(spy.calledWith(data)) - }) + assert.ok(spy.calledOnce) + }) - // TODO: fix for WSWebSocket - it.skip('no ops when not connected', () => { - socket.conn.readyState = 0 // connecting + it('resets reconnectTimer', () => { + const spy = sinon.spy(socket.reconnectTimer, 'reset') - const spy = sinon.spy(socket.conn, 'send') - const data = - '{"topic":"phoenix","event":"heartbeat","payload":{},"ref":"1"}' + socket._onConnOpen() - socket.sendHeartbeat() - assert.ok(spy.neverCalledWith(data)) - }) + assert.ok(spy.calledOnce) + }) }) -describe('flushSendBuffer', () => { - before(() => { - window.XMLHttpRequest = sinon.useFakeXMLHttpRequest() - }) +describe('_onConnClose', () => { + let mockServer - after(() => { - window.XMLHttpRequest = null - }) + before(() => { + mockServer = new WebSocketServer('wss://example.com/') + }) - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket') - socket.connect() + after((done) => { + mockServer.stop(() => { + window.WebSocket = null + done() }) + }) - afterEach(() => { - socket.disconnect() + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket', { + reconnectAfterMs: () => 100000, }) + socket.connect() + }) - // TODO: fix for WSWebSocket - it.skip('calls callbacks in buffer when connected', () => { - socket.conn.readyState = 1 // open - const spy1 = sinon.spy() - const spy2 = sinon.spy() - const spy3 = sinon.spy() - socket.sendBuffer.push(spy1) - socket.sendBuffer.push(spy2) + afterEach(() => { + socket.disconnect() + }) - socket.flushSendBuffer() + it('schedules reconnectTimer timeout', () => { + const spy = sinon.spy(socket.reconnectTimer, 'scheduleTimeout') - assert.ok(spy1.calledOnce) - assert.ok(spy2.calledOnce) - assert.equal(spy3.callCount, 0) - }) + socket._onConnClose() - // TODO: fix for WSWebSocket - it.skip('empties sendBuffer', () => { - socket.conn.readyState = 1 // open - socket.sendBuffer.push(() => { }) + assert.ok(spy.calledOnce) + }) - socket.flushSendBuffer() + it('triggers channel error', () => { + const channel = socket.channel('topic') + const spy = sinon.spy(channel, '_trigger') - assert.deepEqual(socket.sendBuffer.length, 0) - }) -}) + socket._onConnClose() -describe('_onConnOpen', () => { - let mockServer + assert.ok(spy.calledWith('phx_error')) + }) +}) - before(() => { - mockServer = new WebSocketServer('wss://example.com/') - }) +describe('_onConnError', () => { + let mockServer - after((done) => { - mockServer.stop(() => { - window.WebSocket = null - done() - }) - }) + before(() => { + mockServer = new WebSocketServer('wss://example.com/') + }) - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket', { - reconnectAfterMs: () => 100000, - }) - socket.connect() + after((done) => { + mockServer.stop(() => { + window.WebSocket = null + done() }) + }) - afterEach(() => { - socket.disconnect() + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket', { + reconnectAfterMs: () => 100000, }) + socket.connect() + }) - // TODO: fix for WSWebSocket - - it.skip('flushes the send buffer', () => { - socket.conn.readyState = 1 // open - const spy = sinon.spy() - socket.sendBuffer.push(spy) + afterEach(() => { + socket.disconnect() + }) - socket._onConnOpen() + it('triggers channel error', () => { + const channel = socket.channel('topic') + const spy = sinon.spy(channel, '_trigger') - assert.ok(spy.calledOnce) - }) + socket._onConnError('error') - it('resets reconnectTimer', () => { - const spy = sinon.spy(socket.reconnectTimer, 'reset') - - socket._onConnOpen() - - assert.ok(spy.calledOnce) - }) + assert.ok(spy.calledWith('phx_error')) + }) }) -describe('_onConnClose', () => { - let mockServer - - before(() => { - mockServer = new WebSocketServer('wss://example.com/') - }) +describe('onConnMessage', () => { + let mockServer - after((done) => { - mockServer.stop(() => { - window.WebSocket = null - done() - }) - }) + before(() => { + mockServer = new WebSocketServer('wss://example.com/') + }) - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket', { - reconnectAfterMs: () => 100000, - }) - socket.connect() + after((done) => { + mockServer.stop(() => { + window.WebSocket = null + done() }) + }) - afterEach(() => { - socket.disconnect() + beforeEach(() => { + socket = new RealtimeClient('wss://example.com/socket', { + reconnectAfterMs: () => 100000, }) + socket.connect() + }) - it('schedules reconnectTimer timeout', () => { - const spy = sinon.spy(socket.reconnectTimer, 'scheduleTimeout') + afterEach(() => { + socket.disconnect() + }) - socket._onConnClose() + it('parses raw message and triggers channel event', () => { + const message = + '{"topic":"realtime:topic","event":"INSERT","payload":{"type":"INSERT"},"ref":"ref"}' + const data = { data: message } - assert.ok(spy.calledOnce) - }) + const targetChannel = socket.channel('topic') + const otherChannel = socket.channel('off-topic') - it('triggers channel error', () => { - const channel = socket.channel('topic') - const spy = sinon.spy(channel, '_trigger') + const targetSpy = sinon.spy(targetChannel, '_trigger') + const otherSpy = sinon.spy(otherChannel, '_trigger') - socket._onConnClose() + socket.pendingHeartbeatRef = '3' + socket._onConnMessage(data) - assert.ok(spy.calledWith('phx_error')) - }) + // assert.ok(targetSpy.calledWith('INSERT', {type: 'INSERT'}, 'ref')) + assert.strictEqual(targetSpy.callCount, 1) + assert.strictEqual(otherSpy.callCount, 0) + assert.strictEqual(socket.pendingHeartbeatRef, null) + }) }) -describe('_onConnError', () => { - let mockServer +describe('custom encoder and decoder', () => { + afterEach(() => { + socket.disconnect() + }) - before(() => { - mockServer = new WebSocketServer('wss://example.com/') - }) + it('encodes to JSON by default', () => { + socket = new RealtimeClient('wss://example.com/socket') + let payload = { foo: 'bar' } - after((done) => { - mockServer.stop(() => { - window.WebSocket = null - done() - }) + socket.encode(payload, (encoded) => { + assert.deepStrictEqual(encoded, JSON.stringify(payload)) }) + }) - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket', { - reconnectAfterMs: () => 100000, - }) - socket.connect() + it('allows custom encoding when using WebSocket transport', () => { + let encoder = (payload, callback) => callback('encode works') + socket = new RealtimeClient('wss://example.com/socket', { + transport: WebSocket, + encode: encoder, }) - afterEach(() => { - socket.disconnect() + socket.encode({ foo: 'bar' }, (encoded) => { + assert.deepStrictEqual(encoded, 'encode works') }) + }) - it('triggers channel error', () => { - const channel = socket.channel('topic') - const spy = sinon.spy(channel, '_trigger') - - socket._onConnError('error') + it('decodes JSON by default', () => { + socket = new RealtimeClient('wss://example.com/socket') + let payload = JSON.stringify({ foo: 'bar' }) - assert.ok(spy.calledWith('phx_error')) + socket.decode(payload, (decoded) => { + assert.deepStrictEqual(decoded, { foo: 'bar' }) }) -}) + }) -describe('onConnMessage', () => { - let mockServer + it('decodes ArrayBuffer by default', () => { + socket = new RealtimeClient('wss://example.com/socket') + const buffer = new Uint8Array([ + 2, 20, 6, 114, 101, 97, 108, 116, 105, 109, 101, 58, 112, 117, 98, 108, + 105, 99, 58, 116, 101, 115, 116, 73, 78, 83, 69, 82, 84, 123, 34, 102, + 111, 111, 34, 58, 34, 98, 97, 114, 34, 125, + ]).buffer - before(() => { - mockServer = new WebSocketServer('wss://example.com/') + socket.decode(buffer, (decoded) => { + assert.deepStrictEqual(decoded, { + ref: null, + topic: 'realtime:public:test', + event: 'INSERT', + payload: { foo: 'bar' }, + }) }) + }) - after((done) => { - mockServer.stop(() => { - window.WebSocket = null - done() - }) + it('allows custom decoding when using WebSocket transport', () => { + let decoder = (payload, callback) => callback('decode works') + socket = new RealtimeClient('wss://example.com/socket', { + transport: WebSocket, + decode: decoder, }) - beforeEach(() => { - socket = new RealtimeClient('wss://example.com/socket', { - reconnectAfterMs: () => 100000, - }) - socket.connect() + socket.decode('...esoteric format...', (decoded) => { + assert.deepStrictEqual(decoded, 'decode works') }) + }) +}) - afterEach(() => { - socket.disconnect() +describe('createPrivateChannel', () => { + let client, fetch + beforeEach(() => { + const apikey = 'abc123' + fetch = (url, opts) => { + if ( + url == `http://localhost:4000/channels` && + opts.method == 'POST' && + opts.headers['Content-Type'] == 'application/json' && + opts.headers['Authorization'] == `Bearer ${apikey}` + ) { + return Promise.resolve({ ok: true, response: 200 }) + } + return Promise.reject({ ok: false, response: 400 }) + } + client = new RealtimeClient('ws://localhost:4000/socket', { + params: { apikey }, + fetch: fetch, }) + }) - it('parses raw message and triggers channel event', () => { - const message = - '{"topic":"realtime:topic","event":"INSERT","payload":{"type":"INSERT"},"ref":"ref"}' - const data = { data: message } - - const targetChannel = socket.channel('topic') - const otherChannel = socket.channel('off-topic') - - const targetSpy = sinon.spy(targetChannel, '_trigger') - const otherSpy = sinon.spy(otherChannel, '_trigger') - - socket.pendingHeartbeatRef = '3' - socket._onConnMessage(data) + afterEach(() => { + client.disconnect() + }) - // assert.ok(targetSpy.calledWith('INSERT', {type: 'INSERT'}, 'ref')) - assert.strictEqual(targetSpy.callCount, 1) - assert.strictEqual(otherSpy.callCount, 0) - assert.strictEqual(socket.pendingHeartbeatRef, null) - }) + it('returns same channel name when set', async () => { + let result = await client.createPrivateChannel('topic') + assert.equal(result, 'topic') + }) }) -describe('custom encoder and decoder', () => { - afterEach(() => { - socket.disconnect() +describe('deletePrivateChannel', () => { + let name = 'topic' + let client, fetch + beforeEach(() => { + const apikey = 'abc123' + fetch = (url, opts) => { + if ( + url == `http://localhost:4000/channels/${name}` && + opts.method == 'DELETE' && + opts.headers['Authorization'] == `Bearer ${apikey}` + ) { + return Promise.resolve({ ok: true, response: 202 }) + } + return Promise.reject({ ok: false, response: 400 }) + } + client = new RealtimeClient('ws://localhost:4000/socket', { + params: { apikey }, + fetch: fetch, }) + }) - it('encodes to JSON by default', () => { - socket = new RealtimeClient('wss://example.com/socket') - let payload = { foo: 'bar' } + afterEach(() => { + client.disconnect() + }) - socket.encode(payload, (encoded) => { - assert.deepStrictEqual(encoded, JSON.stringify(payload)) - }) - }) - - it('allows custom encoding when using WebSocket transport', () => { - let encoder = (payload, callback) => callback('encode works') - socket = new RealtimeClient('wss://example.com/socket', { - transport: WebSocket, - encode: encoder, - }) + it('returns true when succesful', async () => { + let result = await client.deletePrivateChannel(name) + assert.ok(result) + }) +}) - socket.encode({ foo: 'bar' }, (encoded) => { - assert.deepStrictEqual(encoded, 'encode works') - }) +describe('updatePrivateChannel', () => { + let name = 'topic' + let client, fetch + beforeEach(() => { + const apikey = 'abc123' + fetch = (url, opts) => { + if ( + url == `http://localhost:4000/channels/${name}` && + opts.method == 'PATCH' && + opts.headers['Authorization'] == `Bearer ${apikey}` + ) { + return Promise.resolve({ ok: true, response: 202 }) + } + return Promise.reject({ ok: false, response: 400 }) + } + client = new RealtimeClient('ws://localhost:4000/socket', { + params: { apikey }, + fetch: fetch, }) + }) - it('decodes JSON by default', () => { - socket = new RealtimeClient('wss://example.com/socket') - let payload = JSON.stringify({ foo: 'bar' }) + afterEach(() => { + client.disconnect() + }) - socket.decode(payload, (decoded) => { - assert.deepStrictEqual(decoded, { foo: 'bar' }) - }) - }) + it('returns new name when succesful', async () => { + let new_name = 'new_name' + let result = await client.updatePrivateChannel(name, new_name) + assert.equal(result, new_name) + }) +}) - it('decodes ArrayBuffer by default', () => { - socket = new RealtimeClient('wss://example.com/socket') - const buffer = new Uint8Array([2, 20, 6, 114, 101, 97, 108, 116, 105, - 109, 101, 58, 112, 117, 98, 108, 105, 99, 58, 116, 101, 115, 116, 73, - 78, 83, 69, 82, 84, 123, 34, 102, 111, 111, 34, 58, 34, 98, 97, 114, 34, 125]).buffer - - socket.decode(buffer, decoded => { - assert.deepStrictEqual( - decoded, { - ref: null, - topic: "realtime:public:test", - event: "INSERT", - payload: { foo: 'bar' } - } - ) +describe('listPrivateChannels', () => { + let name = 'topic' + let client, fetch + beforeEach(() => { + const apikey = 'abc123' + fetch = (url, opts) => { + if ( + url == `http://localhost:4000/channels` && + opts.method == 'GET' && + opts.headers['Authorization'] == `Bearer ${apikey}` + ) { + return Promise.resolve({ + ok: true, + response: 200, + json: () => Promise.resolve([{ name }]), }) + } + return Promise.reject({ ok: false, response: 400 }) + } + client = new RealtimeClient('ws://localhost:4000/socket', { + params: { apikey }, + fetch: fetch, }) + }) - it('allows custom decoding when using WebSocket transport', () => { - let decoder = (payload, callback) => callback('decode works') - socket = new RealtimeClient('wss://example.com/socket', { - transport: WebSocket, - decode: decoder, - }) + afterEach(() => { + client.disconnect() + }) - socket.decode('...esoteric format...', (decoded) => { - assert.deepStrictEqual(decoded, 'decode works') - }) - }) + it('returns new name when succesful', async () => { + let result = await client.listPrivateChannels() + assert.deepEqual(result, [{ name }]) + }) })