From dbb609006a90a40ccbc6b84606f6f863fba05c98 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 5 May 2022 00:51:17 +0100 Subject: [PATCH 001/111] Add Ably as dev dependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 0b4b58d4..9032febe 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/node": "^17.0.21", "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/parser": "^5.14.0", + "ably": "^1.2.20", "eslint": "^8.11.0", "jest": "^27.5.1", "rollup": "^2.70.1", From bb6c747c9291e948b37a6852ba308ed2280ead36 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 5 May 2022 00:51:44 +0100 Subject: [PATCH 002/111] Add typing to interact with window.Ably --- typings/index.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 221c1dc7..5379bffa 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,4 +1,7 @@ +import { Types as AblyTypes } from 'ably'; + declare let Pusher: any; +declare let Ably: { Realtime: AblyTypes.RealtimeCallbacks }; declare let io: any; declare let Vue: any; declare let axios: any; From 404456c690c7fafa71903dd2245f83f8b35c83e9 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 5 May 2022 00:52:03 +0100 Subject: [PATCH 003/111] Add AblyChannel implementation --- src/channel/ably-channel.ts | 155 ++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/channel/ably-channel.ts diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts new file mode 100644 index 00000000..89b24800 --- /dev/null +++ b/src/channel/ably-channel.ts @@ -0,0 +1,155 @@ +import { EventFormatter } from '../util'; +import { Channel } from './channel'; +import * as AblyImport from 'ably'; + +/** + * This class represents an Ably channel. + */ +export class AblyChannel extends Channel { + /** + * The Ably client instance. + */ + ably: AblyImport.Types.RealtimeCallbacks; + + /** + * The name of the channel. + */ + name: string; + + /** + * Channel options. + */ + options: any; + + /** + * The event formatter. + */ + eventFormatter: EventFormatter; + + /** + * The subscription of the channel. + */ + channel: AblyImport.Types.RealtimeChannelCallbacks; + + /** + * An array containing all registered subscribed listeners. + */ + subscribedListeners: Function[]; + + /** + * An array containing all registered error listeners. + */ + errorListeners: Function[]; + + /** + * Create a new class instance. + */ + constructor(ably: any, name: string, options: any) { + super(); + + this.name = name; + this.ably = ably; + this.options = options; + this.eventFormatter = new EventFormatter(this.options.namespace); + this.subscribedListeners = []; + + this.subscribe(); + } + + /** + * Subscribe to an Ably channel. + */ + subscribe(): any { + this.channel = this.ably.channels.get(this.name); + } + + /** + * Unsubscribe from an Ably channel. + */ + unsubscribe(): void { + this.channel.unsubscribe(); + } + + /** + * Listen for an event on the channel instance. + */ + listen(event: string, callback: Function): AblyChannel { + this.on(this.eventFormatter.format(event), callback); + + return this; + } + + /** + * Listen for all events on the channel instance. + */ + listenToAll(callback: Function): AblyChannel { + this.channel.subscribe(({name, data}) => { + let namespace = this.options.namespace.replace(/\./g, '\\'); + + let formattedEvent = name.startsWith(namespace) ? name.substring(namespace.length + 1) : '.' + event; + + callback(formattedEvent, data); + }); + + return this; + } + + /** + * Stop listening for an event on the channel instance. + */ + stopListening(event: string, callback?: Function): AblyChannel { + if (callback) { + this.channel.unsubscribe(this.eventFormatter.format(event), callback as any); + } else { + this.channel.unsubscribe(this.eventFormatter.format(event)); + } + + return this; + } + + /** + * Stop listening for all events on the channel instance. + */ + stopListeningToAll(callback?: Function): AblyChannel { + if (callback) { + this.channel.unsubscribe(callback as any); + } else { + this.channel.unsubscribe(); + } + + return this; + } + + /** + * Register a callback to be called anytime a subscription succeeds. + */ + subscribed(callback: Function): AblyChannel { + this.subscribedListeners.push(callback); + + return this; + } + + /** + * Register a callback to be called anytime a subscription error occurs. + */ + error(callback: Function): AblyChannel { + this.errorListeners.push(callback); + + return this; + } + + /** + * Bind a channel to an event. + */ + on(event: string, callback: Function): AblyChannel { + this.channel.subscribe(event, callback as any, (err) => { + if (err) { + this.errorListeners.forEach(listener => listener(err)); + } else { + this.subscribedListeners.forEach(listener => listener()); + } + }); + + return this; + } +} From e4279b60f8f19938558254e732a42bac83812a77 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 5 May 2022 00:52:17 +0100 Subject: [PATCH 004/111] Add AblyPresenceChannel implementation --- src/channel/ably-presence-channel.ts | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/channel/ably-presence-channel.ts diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts new file mode 100644 index 00000000..29d52af8 --- /dev/null +++ b/src/channel/ably-presence-channel.ts @@ -0,0 +1,65 @@ +import { AblyChannel } from './ably-channel'; +import { PresenceChannel } from './presence-channel'; + +/** + * This class represents an Ably presence channel. + */ +export class AblyPresenceChannel extends AblyChannel implements PresenceChannel { + /** + * Register a callback to be called anytime the member list changes. + */ + here(callback: Function): AblyPresenceChannel { + this.channel.presence.get((err, members) => { + if(err) { + callback(err); + return; + } + + callback(members); + + }) + + return this; + } + + /** + * Listen for someone joining the channel. + */ + joining(callback: Function): AblyPresenceChannel { + this.channel.presence.subscribe('enter', (member) => { + callback(member); + }); + + return this; + } + + /** + * Listen for someone leaving the channel. + */ + leaving(callback: Function): AblyPresenceChannel { + this.channel.presence.subscribe('leave', (member) => { + callback(member); + }); + + return this; + } + + /** + * Trigger client event on the channel. + */ + whisper(eventName: string, data: any): AblyPresenceChannel { + switch (eventName) { + case 'enter': + this.channel.presence.enter(data); + break; + case 'leave': + this.channel.presence.leave(data); + break; + case 'update': + this.channel.presence.update(data); + break; + } + + return this; + } +} From 96c44c405ea985f072ffd643ba7d7df2e0be6988 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 5 May 2022 00:52:35 +0100 Subject: [PATCH 005/111] Add AblyConnector implementation --- src/connector/ably-connector.ts | 130 ++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/connector/ably-connector.ts diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts new file mode 100644 index 00000000..b67a3b98 --- /dev/null +++ b/src/connector/ably-connector.ts @@ -0,0 +1,130 @@ +import { Connector } from './connector'; +import { AblyChannel } from '../channel/ably-channel'; +import * as AblyImport from 'ably'; +import { PresenceChannel } from '../channel'; +import { AblyPresenceChannel } from '../channel/ably-presence-channel'; + +declare let Ably: typeof AblyImport; + +/** + * This class creates a connector to Ably. + */ +export class AblyConnector extends Connector { + /** + * The Ably instance. + */ + ably: AblyImport.Types.RealtimeCallbacks; + + /** + * All of the subscribed channel names. + */ + channels: Record = {}; + + /** + * Create a fresh Ably connection. + */ + connect(): void { + if (typeof this.options.client !== 'undefined') { + this.ably = this.options.client; + } else { + this.ably = new Ably.Realtime(this.options); + } + } + + /** + * Listen for an event on a channel instance. + */ + listen(name: string, event: string, callback: Function): AblyChannel { + return this.channel(name).listen(event, callback); + } + + /** + * Get a channel instance by name. + */ + channel(name: string): AblyChannel { + if (!this.channels[name]) { + this.channels[name] = new AblyChannel(this.ably, name, this.options); + } + + return this.channels[name]; + } + + /** + * Get a private channel instance by name. + */ + privateChannel(name: string): AblyChannel { + // if (!this.channels['private-' + name]) { + // this.channels['private-' + name] = new AblyPrivateChannel(this.ably, 'private-' + name, this.options); + // } + // + // return this.channels['private-' + name]; + return this.channel(name); + } + + /** + * Get a private encrypted channel instance by name. + */ + encryptedPrivateChannel(name: string): AblyChannel { + // if (!this.channels['private-encrypted-' + name]) { + // this.channels['private-encrypted-' + name] = new AblyEncryptedPrivateChannel( + // this.ably, + // 'private-encrypted-' + name, + // this.options + // ); + // } + // + // return this.channels['private-encrypted-' + name]; + return this.channel(name); + } + + /** + * Get a presence channel instance by name. + */ + presenceChannel(name: string): PresenceChannel { + if (!this.channels['presence-' + name]) { + this.channels['presence-' + name] = new AblyPresenceChannel( + this.ably, + 'presence-' + name, + this.options + ); + } + + return this.channels['presence-' + name] as AblyPresenceChannel; + } + + /** + * Leave the given channel, as well as its private and presence variants. + */ + leave(name: string): void { + let channels = [name, 'private-' + name, 'presence-' + name]; + + channels.forEach((name: string, index: number) => { + this.leaveChannel(name); + }); + } + + /** + * Leave the given channel. + */ + leaveChannel(name: string): void { + if (this.channels[name]) { + this.channels[name].unsubscribe(); + + delete this.channels[name]; + } + } + + /** + * Get the socket ID for the connection. + */ + socketId(): string { + return this.ably.connection.id; + } + + /** + * Disconnect Ably connection. + */ + disconnect(): void { + this.ably.close(); + } +} From 376cce4f2ce9d12cfd34c1b720502cbba43c88f8 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 5 May 2022 00:55:00 +0100 Subject: [PATCH 006/111] Type Ably as any --- typings/index.d.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 5379bffa..d2e6ac82 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,7 +1,5 @@ -import { Types as AblyTypes } from 'ably'; - declare let Pusher: any; -declare let Ably: { Realtime: AblyTypes.RealtimeCallbacks }; +declare let Ably: any; declare let io: any; declare let Vue: any; declare let axios: any; From 8b2a970308f55833c35eab8873355f1364920969 Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 5 May 2022 00:57:37 +0100 Subject: [PATCH 007/111] Accept 'ably' as options.broadcaster --- src/connector/index.ts | 1 + src/echo.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/connector/index.ts b/src/connector/index.ts index 8353a357..96ceb681 100644 --- a/src/connector/index.ts +++ b/src/connector/index.ts @@ -2,3 +2,4 @@ export * from './connector'; export * from './pusher-connector'; export * from './socketio-connector'; export * from './null-connector'; +export * from './ably-connector'; diff --git a/src/echo.ts b/src/echo.ts index 89ab548f..b02ec0ec 100644 --- a/src/echo.ts +++ b/src/echo.ts @@ -1,5 +1,5 @@ import { Channel, PresenceChannel } from './channel'; -import { PusherConnector, SocketIoConnector, NullConnector } from './connector'; +import { AblyConnector, PusherConnector, SocketIoConnector, NullConnector } from './connector'; /** * This class is the primary API for interacting with broadcasting. @@ -38,7 +38,9 @@ export default class Echo { * Create a new connection. */ connect(): void { - if (this.options.broadcaster == 'pusher') { + if (this.options.broadcaster == 'ably') { + this.connector = new AblyConnector(this.options); + } else if (this.options.broadcaster == 'pusher') { this.connector = new PusherConnector(this.options); } else if (this.options.broadcaster == 'socket.io') { this.connector = new SocketIoConnector(this.options); From 02eb2d6ca624de829b286b1fdbfea3269ff1155d Mon Sep 17 00:00:00 2001 From: Owen Pearson Date: Thu, 5 May 2022 01:05:45 +0100 Subject: [PATCH 008/111] Remove unneeded ts declaration --- src/connector/ably-connector.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index b67a3b98..fdf18a58 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -4,8 +4,6 @@ import * as AblyImport from 'ably'; import { PresenceChannel } from '../channel'; import { AblyPresenceChannel } from '../channel/ably-presence-channel'; -declare let Ably: typeof AblyImport; - /** * This class creates a connector to Ably. */ From b6f86a86eed41114285521504bce519532cf6cd7 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Mon, 30 May 2022 01:31:00 +0530 Subject: [PATCH 009/111] Refactored channels, added proper namespaces to channels --- src/connector/ably-connector.ts | 47 +++++++++++++-------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index fdf18a58..c4839a16 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -29,34 +29,40 @@ export class AblyConnector extends Connector { } } + _channel(name: string): AblyChannel { + if (!this.channels[name]) { + this.channels[name] = new AblyChannel(this.ably, name, this.options); + } + + return this.channels[name]; + } + /** * Listen for an event on a channel instance. */ listen(name: string, event: string, callback: Function): AblyChannel { - return this.channel(name).listen(event, callback); + return this._channel(name).listen(event, callback); } /** * Get a channel instance by name. */ channel(name: string): AblyChannel { - if (!this.channels[name]) { - this.channels[name] = new AblyChannel(this.ably, name, this.options); - } - - return this.channels[name]; + return this._channel(`public:${name}`); } /** * Get a private channel instance by name. */ privateChannel(name: string): AblyChannel { - // if (!this.channels['private-' + name]) { - // this.channels['private-' + name] = new AblyPrivateChannel(this.ably, 'private-' + name, this.options); - // } - // - // return this.channels['private-' + name]; - return this.channel(name); + return this._channel(`private:${name}`); + } + + /** + * Get a presence channel instance by name. + */ + presenceChannel(name: string): PresenceChannel { + return this._channel(`presence:${name}`) as AblyPresenceChannel; } /** @@ -75,26 +81,11 @@ export class AblyConnector extends Connector { return this.channel(name); } - /** - * Get a presence channel instance by name. - */ - presenceChannel(name: string): PresenceChannel { - if (!this.channels['presence-' + name]) { - this.channels['presence-' + name] = new AblyPresenceChannel( - this.ably, - 'presence-' + name, - this.options - ); - } - - return this.channels['presence-' + name] as AblyPresenceChannel; - } - /** * Leave the given channel, as well as its private and presence variants. */ leave(name: string): void { - let channels = [name, 'private-' + name, 'presence-' + name]; + let channels = [name, 'private:' + name, 'presence:' + name]; channels.forEach((name: string, index: number) => { this.leaveChannel(name); From d6b94a03048936db80a670ad19d1362eb273e22f Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Mon, 30 May 2022 01:37:44 +0530 Subject: [PATCH 010/111] Refactored namespace for channel leave --- src/connector/ably-connector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index c4839a16..efd934e1 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -85,7 +85,7 @@ export class AblyConnector extends Connector { * Leave the given channel, as well as its private and presence variants. */ leave(name: string): void { - let channels = [name, 'private:' + name, 'presence:' + name]; + let channels = [`public:${name}`, `private:${name}`, `presence:${name}`]; channels.forEach((name: string, index: number) => { this.leaveChannel(name); From b88bb016d3399ddea832d8e04caf21c78e1ed5a8 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 31 May 2022 20:20:56 +0530 Subject: [PATCH 011/111] refactored ablyconnector, removed unnecessary encrypted channel --- src/connector/ably-connector.ts | 51 ++++++++++++++------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index efd934e1..0946c3e3 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -1,7 +1,7 @@ import { Connector } from './connector'; import { AblyChannel } from '../channel/ably-channel'; +import { AblyPrivateChannel } from '../channel/ably-private-channel'; import * as AblyImport from 'ably'; -import { PresenceChannel } from '../channel'; import { AblyPresenceChannel } from '../channel/ably-presence-channel'; /** @@ -29,56 +29,47 @@ export class AblyConnector extends Connector { } } - _channel(name: string): AblyChannel { - if (!this.channels[name]) { - this.channels[name] = new AblyChannel(this.ably, name, this.options); - } - - return this.channels[name]; - } - /** * Listen for an event on a channel instance. */ listen(name: string, event: string, callback: Function): AblyChannel { - return this._channel(name).listen(event, callback); + return this.channel(name).listen(event, callback); } /** * Get a channel instance by name. */ channel(name: string): AblyChannel { - return this._channel(`public:${name}`); + const prefixedName = `public:${name}`; // adding public as a ably namespace prefix + if (!this.channels[prefixedName]) { + this.channels[prefixedName] = new AblyChannel(this.ably, name, this.options); + } + + return this.channels[prefixedName]; } /** * Get a private channel instance by name. */ - privateChannel(name: string): AblyChannel { - return this._channel(`private:${name}`); + privateChannel(name: string): AblyPrivateChannel { + const prefixedName = `private:${name}`; // adding private as a ably namespace prefix + if (!this.channels[prefixedName]) { + this.channels[prefixedName] = new AblyPrivateChannel(this.ably, name, this.options); + } + + return this.channels[prefixedName] as AblyPrivateChannel; } /** * Get a presence channel instance by name. */ - presenceChannel(name: string): PresenceChannel { - return this._channel(`presence:${name}`) as AblyPresenceChannel; - } + presenceChannel(name: string): AblyPresenceChannel { + const prefixedName = `presence:${name}`; // adding presence as a ably namespace prefix + if (!this.channels[prefixedName]) { + this.channels[prefixedName] = new AblyPresenceChannel(this.ably, name, this.options); + } - /** - * Get a private encrypted channel instance by name. - */ - encryptedPrivateChannel(name: string): AblyChannel { - // if (!this.channels['private-encrypted-' + name]) { - // this.channels['private-encrypted-' + name] = new AblyEncryptedPrivateChannel( - // this.ably, - // 'private-encrypted-' + name, - // this.options - // ); - // } - // - // return this.channels['private-encrypted-' + name]; - return this.channel(name); + return this.channels[prefixedName] as AblyPresenceChannel; } /** From b1210e98f24f27c9f5c9f277eae02c2b963739b9 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 31 May 2022 20:21:46 +0530 Subject: [PATCH 012/111] Refactored ably-channel, updated subscribe and error listeners --- src/channel/ably-channel.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index 89b24800..d569b9fe 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -61,6 +61,14 @@ export class AblyChannel extends Channel { */ subscribe(): any { this.channel = this.ably.channels.get(this.name); + this.channel.on(({current, reason}) => { + if (current == 'attached') { + this.subscribedListeners.forEach(listener => listener()); + } else if (reason) { + this.errorListeners.forEach(listener => listener(reason)); + } + }); + this.channel.attach(); } /** @@ -68,13 +76,14 @@ export class AblyChannel extends Channel { */ unsubscribe(): void { this.channel.unsubscribe(); + this.channel.detach(); } /** * Listen for an event on the channel instance. */ listen(event: string, callback: Function): AblyChannel { - this.on(this.eventFormatter.format(event), callback); + this.channel.subscribe(event, callback as any); return this; } @@ -137,19 +146,4 @@ export class AblyChannel extends Channel { return this; } - - /** - * Bind a channel to an event. - */ - on(event: string, callback: Function): AblyChannel { - this.channel.subscribe(event, callback as any, (err) => { - if (err) { - this.errorListeners.forEach(listener => listener(err)); - } else { - this.subscribedListeners.forEach(listener => listener()); - } - }); - - return this; - } } From ef7b636844e1f54039cc488be9840d34e2611d3d Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 31 May 2022 20:22:14 +0530 Subject: [PATCH 013/111] Added ably private channel --- src/channel/ably-private-channel.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/channel/ably-private-channel.ts diff --git a/src/channel/ably-private-channel.ts b/src/channel/ably-private-channel.ts new file mode 100644 index 00000000..c88f778b --- /dev/null +++ b/src/channel/ably-private-channel.ts @@ -0,0 +1,12 @@ +import { AblyChannel } from './ably-channel'; + +export class AblyPrivateChannel extends AblyChannel { + /** + * Trigger client event on the channel. + */ + whisper(eventName: string, data: any): AblyPrivateChannel { + this.channel.publish(`client-${eventName}`, data); + + return this; + } +} \ No newline at end of file From a6cba23baf860c49c7ec62b83d364a0f6bd997b1 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 31 May 2022 20:22:33 +0530 Subject: [PATCH 014/111] Added TODO for AblyPresence Channle whisper --- src/channel/ably-presence-channel.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts index 29d52af8..9377cf88 100644 --- a/src/channel/ably-presence-channel.ts +++ b/src/channel/ably-presence-channel.ts @@ -44,6 +44,7 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel return this; } + // TODO - Need to check for whisper implementation for presence channels /** * Trigger client event on the channel. */ From d2c3884502c24539909a8b7dc66b75416dabea0f Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 31 May 2022 20:36:30 +0530 Subject: [PATCH 015/111] Fixed methods for creating channels --- src/connector/ably-connector.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index 0946c3e3..a3a33527 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -42,7 +42,7 @@ export class AblyConnector extends Connector { channel(name: string): AblyChannel { const prefixedName = `public:${name}`; // adding public as a ably namespace prefix if (!this.channels[prefixedName]) { - this.channels[prefixedName] = new AblyChannel(this.ably, name, this.options); + this.channels[prefixedName] = new AblyChannel(this.ably, prefixedName, this.options); } return this.channels[prefixedName]; @@ -54,7 +54,7 @@ export class AblyConnector extends Connector { privateChannel(name: string): AblyPrivateChannel { const prefixedName = `private:${name}`; // adding private as a ably namespace prefix if (!this.channels[prefixedName]) { - this.channels[prefixedName] = new AblyPrivateChannel(this.ably, name, this.options); + this.channels[prefixedName] = new AblyPrivateChannel(this.ably, prefixedName, this.options); } return this.channels[prefixedName] as AblyPrivateChannel; @@ -66,7 +66,7 @@ export class AblyConnector extends Connector { presenceChannel(name: string): AblyPresenceChannel { const prefixedName = `presence:${name}`; // adding presence as a ably namespace prefix if (!this.channels[prefixedName]) { - this.channels[prefixedName] = new AblyPresenceChannel(this.ably, name, this.options); + this.channels[prefixedName] = new AblyPresenceChannel(this.ably, prefixedName, this.options); } return this.channels[prefixedName] as AblyPresenceChannel; From 322f81b827a1755423fde210c2f10982ed453817 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 31 May 2022 20:49:09 +0530 Subject: [PATCH 016/111] Updated presence channel, reformatted code --- src/channel/ably-presence-channel.ts | 122 ++++++++++++++++----------- 1 file changed, 73 insertions(+), 49 deletions(-) diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts index 9377cf88..6563e66d 100644 --- a/src/channel/ably-presence-channel.ts +++ b/src/channel/ably-presence-channel.ts @@ -5,62 +5,86 @@ import { PresenceChannel } from './presence-channel'; * This class represents an Ably presence channel. */ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel { - /** - * Register a callback to be called anytime the member list changes. - */ - here(callback: Function): AblyPresenceChannel { - this.channel.presence.get((err, members) => { - if(err) { - callback(err); - return; - } + /** + * Register a callback to be called anytime the member list changes. + */ + here(callback: Function): AblyPresenceChannel { + this.channel.presence.get((err, members) => { + if (err) { + callback(null, err); + return; + } - callback(members); + callback(members, null); + }) - }) + return this; + } - return this; - } + /** + * Listen for someone joining the channel. + */ + joining(callback: Function): AblyPresenceChannel { + this.channel.presence.subscribe('enter', (member) => { + callback(member); + }); - /** - * Listen for someone joining the channel. - */ - joining(callback: Function): AblyPresenceChannel { - this.channel.presence.subscribe('enter', (member) => { - callback(member); - }); + return this; + } - return this; - } + /** + * Listen for someone leaving the channel. + */ + leaving(callback: Function): AblyPresenceChannel { + this.channel.presence.subscribe('leave', (member) => { + callback(member); + }); - /** - * Listen for someone leaving the channel. - */ - leaving(callback: Function): AblyPresenceChannel { - this.channel.presence.subscribe('leave', (member) => { - callback(member); - }); + return this; + } - return this; - } + /** + * Enter presence + * @param data - Data to be published while entering the channel + * @param callback - success/error callback (err) => {} + * @returns AblyPresenceChannel + */ + enter(data: any, callback: Function): AblyPresenceChannel { + this.channel.presence.enter(data, callback as any); + return this; + } - // TODO - Need to check for whisper implementation for presence channels - /** - * Trigger client event on the channel. - */ - whisper(eventName: string, data: any): AblyPresenceChannel { - switch (eventName) { - case 'enter': - this.channel.presence.enter(data); - break; - case 'leave': - this.channel.presence.leave(data); - break; - case 'update': - this.channel.presence.update(data); - break; - } + /** + * Leave presence + * @param data - Data to be published while leaving the channel + * @param callback - success/error callback (err) => {} + * @returns AblyPresenceChannel + */ + leave(data: any, callback: Function): AblyPresenceChannel { + this.channel.presence.leave(data, callback as any); + + return this; + } + + /** + * Update presence + * @param data - Update presence with data + * @param callback - success/error callback (err) => {} + * @returns AblyPresenceChannel + */ + update(data: any, callback: Function): AblyPresenceChannel { + this.channel.presence.update(data, callback as any); + + return this; + } + + /** + * Trigger client event on the channel. + */ + whisper(eventName: string, data: any): AblyPresenceChannel { + this.channel.publish(`client-${eventName}`, data); + + return this; + } - return this; - } } From 0e91140acf65391f6458e92dc1daaf3f351a8962 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 31 May 2022 20:49:52 +0530 Subject: [PATCH 017/111] Reformatted ablyChannel class file --- src/channel/ably-channel.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index d569b9fe..8ddb6753 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -61,7 +61,7 @@ export class AblyChannel extends Channel { */ subscribe(): any { this.channel = this.ably.channels.get(this.name); - this.channel.on(({current, reason}) => { + this.channel.on(({ current, reason }) => { if (current == 'attached') { this.subscribedListeners.forEach(listener => listener()); } else if (reason) { @@ -92,7 +92,7 @@ export class AblyChannel extends Channel { * Listen for all events on the channel instance. */ listenToAll(callback: Function): AblyChannel { - this.channel.subscribe(({name, data}) => { + this.channel.subscribe(({ name, data }) => { let namespace = this.options.namespace.replace(/\./g, '\\'); let formattedEvent = name.startsWith(namespace) ? name.substring(namespace.length + 1) : '.' + event; @@ -142,8 +142,8 @@ export class AblyChannel extends Channel { * Register a callback to be called anytime a subscription error occurs. */ error(callback: Function): AblyChannel { - this.errorListeners.push(callback); + this.errorListeners.push(callback); - return this; + return this; } } From 441f7efddf1817b5379af96783315507196423ac Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 00:40:19 +0530 Subject: [PATCH 018/111] Fixed error listener for AblyChannel --- src/channel/ably-channel.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index 8ddb6753..3371db22 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -52,7 +52,8 @@ export class AblyChannel extends Channel { this.options = options; this.eventFormatter = new EventFormatter(this.options.namespace); this.subscribedListeners = []; - + this.errorListeners = []; + this.subscribe(); } From 7a8b28a2e29763f0693a15ce023c985bd7184edd Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 00:41:31 +0530 Subject: [PATCH 019/111] Fixed ably imports for ably-connector --- src/channel/index.ts | 3 +++ src/connector/ably-connector.ts | 12 +++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/channel/index.ts b/src/channel/index.ts index 2e719848..847c02a6 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -1,5 +1,8 @@ export * from './channel'; export * from './presence-channel'; +export * from './ably-channel'; +export * from './ably-private-channel'; +export * from './ably-presence-channel'; export * from './pusher-channel'; export * from './pusher-private-channel'; export * from './pusher-encrypted-private-channel'; diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index a3a33527..f28146f1 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -1,9 +1,11 @@ import { Connector } from './connector'; -import { AblyChannel } from '../channel/ably-channel'; -import { AblyPrivateChannel } from '../channel/ably-private-channel'; -import * as AblyImport from 'ably'; -import { AblyPresenceChannel } from '../channel/ably-presence-channel'; +import * as Ably from 'ably'; +import { + AblyChannel, + AblyPrivateChannel, + AblyPresenceChannel, +} from './../channel'; /** * This class creates a connector to Ably. */ @@ -11,7 +13,7 @@ export class AblyConnector extends Connector { /** * The Ably instance. */ - ably: AblyImport.Types.RealtimeCallbacks; + ably: Ably.Types.RealtimeCallbacks; /** * All of the subscribed channel names. From 28fb62a7ec63bdc5822a4cfd541fa0886f6a31c6 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 01:53:52 +0530 Subject: [PATCH 020/111] Added basic auth JWT utils --- src/channel/ably/utils.ts | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/channel/ably/utils.ts diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts new file mode 100644 index 00000000..26bf28bd --- /dev/null +++ b/src/channel/ably/utils.ts @@ -0,0 +1,43 @@ +export const isNullOrUndefined = (obj) => obj == null || obj === undefined; +export let isEmptyString = (stringToCheck, ignoreSpaces = true) => (ignoreSpaces ? stringToCheck.trim() : stringToCheck) === ''; +export const isNullOrUndefinedOrEmpty = (obj) => obj == null || obj === undefined || isEmptyString(obj); + +export const parseJwt = (token) => { + try { + // Get Token Header + const base64HeaderUrl = token.split('.')[0]; + const base64Header = base64HeaderUrl.replace('-', '+').replace('_', '/'); + const headerData = JSON.parse(atob(base64Header)); + + // Get Token payload and date's + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace('-', '+').replace('_', '/'); + const dataJWT = JSON.parse(atob(base64)); + dataJWT.header = headerData; + + // TODO: add expiration at check ... + + return dataJWT; + } catch (err) { + return false; + } +} + +export const toTokenDetails = (jwtTokenString) => { + const parsedJwt = parseJwt(jwtTokenString); + return { + capability: parsedJwt['x-ably-capability'], + clientId: parsedJwt['x-ably-clientId'], + expires: parsedJwt.exp * 1000, // Convert Seconds to ms + issued: parsedJwt.iat * 1000, + token: jwtTokenString + } +} + +const btoa = (text) => { + return Buffer.from(text, 'binary').toString('base64'); +}; + +const atob = (base64) => { + return Buffer.from(base64, 'base64').toString('binary'); +}; \ No newline at end of file From d8da3d22c4e720bb35df17c0117336303f77f510 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 01:55:40 +0530 Subject: [PATCH 021/111] Added patch for executing channel auth before channel attach --- src/channel/ably/attach.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/channel/ably/attach.ts diff --git a/src/channel/ably/attach.ts b/src/channel/ably/attach.ts new file mode 100644 index 00000000..7f02744f --- /dev/null +++ b/src/channel/ably/attach.ts @@ -0,0 +1,23 @@ +import { isNullOrUndefined } from './utils'; + +export const beforeChannelAttach = (ablyClient, authorize) => { + const dummyRealtimeChannel = ablyClient.channels.get("dummy"); + const internalAttach = dummyRealtimeChannel.__proto__._attach; // get parent class method, store it in temp. variable + if (isNullOrUndefined(internalAttach)) { + console.warn("channel internal attach function not found, please check for right library version") + return; + } + function customInternalAttach(forceReattach, attachReason, errCallback) {// Define new function that needs to be added + const bindedInternalAttach = internalAttach.bind(this); // bind object instance at runtime based on who calls printMessage + // custom logic before attach + authorize(this, (error) => { + if (error) { + errCallback(error); + return; + } else { + bindedInternalAttach(forceReattach, attachReason, errCallback);// call internal function here + } + }) + } + dummyRealtimeChannel.__proto__._attach = customInternalAttach; // add updated extension method to parent class, auto binded +} From 170b0c96625eeef377d71d1221d04e1b7f215672 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 01:59:28 +0530 Subject: [PATCH 022/111] Added mock server for generating JWT for given channelName --- src/channel/ably/mock-auth-server.ts | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/channel/ably/mock-auth-server.ts diff --git a/src/channel/ably/mock-auth-server.ts b/src/channel/ably/mock-auth-server.ts new file mode 100644 index 00000000..fbd8143c --- /dev/null +++ b/src/channel/ably/mock-auth-server.ts @@ -0,0 +1,53 @@ +import {isNullOrUndefinedOrEmpty, parseJwt} from './utils' +const Ably = require("ably/promises"); +const jwt = require("jsonwebtoken"); + +const API_KEY = ''; +const APP_ID = API_KEY.split('.')[0], + KEY_PARTS = API_KEY.split(':'), + KEY_NAME = KEY_PARTS[0], + KEY_SECRET = KEY_PARTS[1]; + +const ablyClient = new Ably.Rest(API_KEY); + +const tokenInvalidOrExpired = (serverTime, token) => { + const tokenInvalid = false; + const parsedJwt = parseJwt(token); + return tokenInvalid || parsedJwt.exp * 1000 <= serverTime; +}; + +const clientId = 'sacdaddy@gmail.com' + +export const getSignedToken = async (channelName = null, token = null) => { + const header = { + "typ":"JWT", + "alg":"HS256", + "kid": KEY_NAME + } + // Set capabilities for public channel as per https://ably.com/docs/core-features/authentication#capability-operations + let channelClaims = new Set(['"public:*":["subscribe", "history", "channel-metadata"]']); + let iat = 0; + let exp = 0; + let serverTime = await ablyClient.time(); + if (!isNullOrUndefinedOrEmpty(token) && !tokenInvalidOrExpired(serverTime, token)) { + const parsedJwt = parseJwt(token); + iat = parsedJwt.iat; + exp = parsedJwt.exp; + channelClaims = new Set(parsedJwt['x-ably-capability'].slice(1, -1).split(',')); + } else { + iat = Math.round(serverTime/1000); + exp = iat + 60; /* time of expiration in seconds */ + } + if (!isNullOrUndefinedOrEmpty(channelName)) { + channelClaims.add(`"${channelName}":["*"]`) + } + const capabilities = Array.from(channelClaims).join(','); + const claims = { + iat, + exp, + "x-ably-clientId": clientId, + "x-ably-capability": `{${capabilities}}` + } + // signJWT(header, claims, KEY_SECRET); broken + return jwt.sign(claims, KEY_SECRET, {header}); +} \ No newline at end of file From e46b474a115b9d4a3c55ee6e6caa30e191cdc041 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 02:00:04 +0530 Subject: [PATCH 023/111] Added sequential token request executer --- src/channel/ably/token-request.ts | 53 +++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/channel/ably/token-request.ts diff --git a/src/channel/ably/token-request.ts b/src/channel/ably/token-request.ts new file mode 100644 index 00000000..e4bb1645 --- /dev/null +++ b/src/channel/ably/token-request.ts @@ -0,0 +1,53 @@ +import { getSignedToken } from './mock-auth-server'; + +export class SequentialAuthTokenRequestExecuter { + cachedToken = null; + queue: TaskQueue; + + constructor(token : string = null) { + this.cachedToken = token; + this.queue = new TaskQueue(); + } + + execute = (tokenRequestFn) => new Promise(async (resolve, reject) => { + await this.queue.run(async () => { + try { + const token = await tokenRequestFn(this.cachedToken); + this.cachedToken = token; + resolve(token); + } catch (err) { + reject(err); + } + }) + }) + + request = channelName => this.execute(token => getSignedToken(channelName, token)); +} + +class TaskQueue { + total: Number; + todo: Array; + running: Array; + count: Number; + + constructor(tasks = [], concurrentCount = 1) { + this.total = tasks.length; + this.todo = tasks; + this.running = []; + this.count = concurrentCount; + } + + canRunNext = () => (this.running.length < this.count) && this.todo.length; + + run = async (task: Function) => { + if (task) { + this.todo.push(task); + } + while (this.canRunNext()) { + const currentTask = this.todo.shift(); + this.running.push(currentTask); + await currentTask(); + this.running.shift(); + } + } +} \ No newline at end of file From 0191ac4f1fb417c0b4ec68b8eb5bd030bacc7c00 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 02:00:37 +0530 Subject: [PATCH 024/111] Added auth related methods for authz of private/presence channels --- src/channel/ably/auth.ts | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/channel/ably/auth.ts diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts new file mode 100644 index 00000000..2a6a2222 --- /dev/null +++ b/src/channel/ably/auth.ts @@ -0,0 +1,70 @@ +import { beforeChannelAttach } from './attach'; +import { toTokenDetails } from './utils'; +import { SequentialAuthTokenRequestExecuter } from './token-request'; +import { AblyChannel } from '../ably-channel'; + +const authRequestExecuter = new SequentialAuthTokenRequestExecuter(); + +export const authOptions = { + queryTime: true, + useTokenAuth: true, + authCallback: async (_, callback) => { // get token from tokenParams + try { + const jwtToken = await authRequestExecuter.request(null); // Replace this by network request to PHP server + const tokenDetails = toTokenDetails(jwtToken); + callback(null, tokenDetails); + } catch (error) { + callback(error, null); + } + } +} + +export const applyAuthorizeBeforeChannelAttach = (ablyClient) => { + beforeChannelAttach(ablyClient, (realtimeChannel, errorCallback) => { + const channelName = realtimeChannel.name; + if (channelName.startsWith("public:")) { + errorCallback(null); + return; + } + + // Use cached token if has channel capability and is not expired + const token = ablyClient.auth.tokenDetails; + if (token) { + const tokenHasChannelCapability = token.capability.includes(channelName); + if (tokenHasChannelCapability && token.expires >= Date.now()) { // TODO : Replace with server time + errorCallback(null); + return; + } + } + + // explicitly request token for given channel name + authRequestExecuter.request(channelName).then(jwtToken => { // get upgraded token with channel access + ablyClient.auth.authorize(null, { ...authOptions, token: toTokenDetails(jwtToken) }, (err, tokenDetails) => { + if (err) { + errorCallback(err); + } else { + errorCallback(null); + } + }); + }) + }); +} + +export const onChannelFailed = (ablyChannel: AblyChannel) => stateChange => { + if (stateChange.reason?.code == 40160) { // channel capability rejected https://help.ably.io/error/40160 + handleChannelAuthError(ablyChannel); + } +} + +const handleChannelAuthError = (ablyChannel: AblyChannel) => { + const channelName = ablyChannel.name; + authRequestExecuter.request(channelName).then(jwtToken => { // get upgraded token with channel access + ablyChannel.ably.auth.authorize(null, { ...authOptions, token: toTokenDetails(jwtToken) }, (err, _tokenDetails) => { + if (err) { + ablyChannel._publishErrors(err); + } else { + ablyChannel.channel.attach(); + } + }); + }); +} \ No newline at end of file From c31be4d15f09e86591339a494aeadaa67a4ceeef Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 02:01:59 +0530 Subject: [PATCH 025/111] Added channel auth error handler on failed channel state --- src/channel/ably-presence-channel.ts | 6 ++++++ src/channel/ably-private-channel.ts | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts index 6563e66d..f3f824b6 100644 --- a/src/channel/ably-presence-channel.ts +++ b/src/channel/ably-presence-channel.ts @@ -1,10 +1,16 @@ import { AblyChannel } from './ably-channel'; import { PresenceChannel } from './presence-channel'; +import { onChannelFailed } from './ably'; /** * This class represents an Ably presence channel. */ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel { + + constructor(ably: any, name: string, options: any) { + super(ably, name, options); + this.channel.on("failed", onChannelFailed(this)); + } /** * Register a callback to be called anytime the member list changes. */ diff --git a/src/channel/ably-private-channel.ts b/src/channel/ably-private-channel.ts index c88f778b..cc9175b3 100644 --- a/src/channel/ably-private-channel.ts +++ b/src/channel/ably-private-channel.ts @@ -1,6 +1,11 @@ import { AblyChannel } from './ably-channel'; - +import { onChannelFailed } from './ably'; export class AblyPrivateChannel extends AblyChannel { + + constructor(ably: any, name: string, options: any) { + super(ably, name, options); + this.channel.on("failed", onChannelFailed(this)); + } /** * Trigger client event on the channel. */ From 3ad6972cb2a832ed247e2c3ad4af25b43a3810b7 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 02:02:42 +0530 Subject: [PATCH 026/111] Extracted private method for publishing errors in AblyChannel class --- src/channel/ably-channel.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index 3371db22..7359b939 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -53,7 +53,7 @@ export class AblyChannel extends Channel { this.eventFormatter = new EventFormatter(this.options.namespace); this.subscribedListeners = []; this.errorListeners = []; - + this.subscribe(); } @@ -66,7 +66,7 @@ export class AblyChannel extends Channel { if (current == 'attached') { this.subscribedListeners.forEach(listener => listener()); } else if (reason) { - this.errorListeners.forEach(listener => listener(reason)); + this._publishErrors(reason); } }); this.channel.attach(); @@ -147,4 +147,8 @@ export class AblyChannel extends Channel { return this; } + + _publishErrors = (err) => { + this.errorListeners.forEach(listener => listener(err)); + } } From afbc3fc6a13faf7e0e88824196a49f9fcf9701fb Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 02:03:14 +0530 Subject: [PATCH 027/111] Fixed auth exports, updated ably-connector to apply authz for channels --- src/channel/ably/index.ts | 1 + src/channel/index.ts | 1 + src/connector/ably-connector.ts | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 src/channel/ably/index.ts diff --git a/src/channel/ably/index.ts b/src/channel/ably/index.ts new file mode 100644 index 00000000..e295e15a --- /dev/null +++ b/src/channel/ably/index.ts @@ -0,0 +1 @@ +export * from './auth'; \ No newline at end of file diff --git a/src/channel/index.ts b/src/channel/index.ts index 847c02a6..1d592685 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -3,6 +3,7 @@ export * from './presence-channel'; export * from './ably-channel'; export * from './ably-private-channel'; export * from './ably-presence-channel'; +export * from './ably'; export * from './pusher-channel'; export * from './pusher-private-channel'; export * from './pusher-encrypted-private-channel'; diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index f28146f1..ad5ad848 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -5,6 +5,8 @@ import { AblyChannel, AblyPrivateChannel, AblyPresenceChannel, + authOptions, + applyAuthorizeBeforeChannelAttach } from './../channel'; /** * This class creates a connector to Ably. @@ -27,7 +29,8 @@ export class AblyConnector extends Connector { if (typeof this.options.client !== 'undefined') { this.ably = this.options.client; } else { - this.ably = new Ably.Realtime(this.options); + this.ably = new Ably.Realtime({...this.options, ...authOptions}); + applyAuthorizeBeforeChannelAttach(this.ably); } } From 9655563d94c4f53f683e0050245623da9d300492 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 10:24:31 +0530 Subject: [PATCH 028/111] updated sequential token executer, update auth into a class --- src/channel/ably/auth.ts | 111 +++++++++++++++++------------- src/channel/ably/token-request.ts | 10 +-- 2 files changed, 68 insertions(+), 53 deletions(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 2a6a2222..3bafe26c 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -3,68 +3,83 @@ import { toTokenDetails } from './utils'; import { SequentialAuthTokenRequestExecuter } from './token-request'; import { AblyChannel } from '../ably-channel'; -const authRequestExecuter = new SequentialAuthTokenRequestExecuter(); +export class AblyAuth { -export const authOptions = { - queryTime: true, - useTokenAuth: true, - authCallback: async (_, callback) => { // get token from tokenParams - try { - const jwtToken = await authRequestExecuter.request(null); // Replace this by network request to PHP server - const tokenDetails = toTokenDetails(jwtToken); - callback(null, tokenDetails); - } catch (error) { - callback(error, null); + // TODO - Can be updated with request throttle, to send multiple request payload under single request + authRequestExecuter: SequentialAuthTokenRequestExecuter; + + authOptions = { + queryTime: true, + useTokenAuth: true, + authCallback: async (_, callback) => { // get token from tokenParams + try { + const jwtToken = await this.authRequestExecuter.request(null); // Replace this by network request to PHP server + const tokenDetails = toTokenDetails(jwtToken); + callback(null, tokenDetails); + } catch (error) { + callback(error, null); + } } } -} -export const applyAuthorizeBeforeChannelAttach = (ablyClient) => { - beforeChannelAttach(ablyClient, (realtimeChannel, errorCallback) => { - const channelName = realtimeChannel.name; - if (channelName.startsWith("public:")) { - errorCallback(null); - return; - } + // TODO - Add default HTTP request fn + requestToken = (channelName: string, existingToken: string) => { + + } - // Use cached token if has channel capability and is not expired - const token = ablyClient.auth.tokenDetails; - if (token) { - const tokenHasChannelCapability = token.capability.includes(channelName); - if (tokenHasChannelCapability && token.expires >= Date.now()) { // TODO : Replace with server time + constructor(options) { + const { token, requestTokenFn } = options; + this.authRequestExecuter = new SequentialAuthTokenRequestExecuter(token, requestTokenFn ?? this.requestToken); + } + + enableAuthorizeBeforeChannelAttach = (ablyClient) => { + beforeChannelAttach(ablyClient, (realtimeChannel, errorCallback) => { + const channelName = realtimeChannel.name; + if (channelName.startsWith("public:")) { errorCallback(null); return; } + + // Use cached token if has channel capability and is not expired + const token = ablyClient.auth.tokenDetails; + if (token) { + const tokenHasChannelCapability = token.capability.includes(channelName); + if (tokenHasChannelCapability && token.expires >= Date.now()) { // TODO : Replace with server time + errorCallback(null); + return; + } + } + + // explicitly request token for given channel name + this.authRequestExecuter.request(channelName).then(jwtToken => { // get upgraded token with channel access + ablyClient.auth.authorize(null, { ...this.authOptions, token: toTokenDetails(jwtToken) }, (err, tokenDetails) => { + if (err) { + errorCallback(err); + } else { + errorCallback(null); + } + }); + }) + }); + } + + onChannelFailed = (ablyChannel: AblyChannel) => stateChange => { + if (stateChange.reason?.code == 40160) { // channel capability rejected https://help.ably.io/error/40160 + this.handleChannelAuthError(ablyChannel); } + } - // explicitly request token for given channel name - authRequestExecuter.request(channelName).then(jwtToken => { // get upgraded token with channel access - ablyClient.auth.authorize(null, { ...authOptions, token: toTokenDetails(jwtToken) }, (err, tokenDetails) => { + handleChannelAuthError = (ablyChannel: AblyChannel) => { + const channelName = ablyChannel.name; + this.authRequestExecuter.request(channelName).then(jwtToken => { // get upgraded token with channel access + ablyChannel.ably.auth.authorize(null, { ...this.authOptions, token: toTokenDetails(jwtToken) }, (err, _tokenDetails) => { if (err) { - errorCallback(err); + ablyChannel._publishErrors(err); } else { - errorCallback(null); + ablyChannel.channel.attach(); } }); - }) - }); -} - -export const onChannelFailed = (ablyChannel: AblyChannel) => stateChange => { - if (stateChange.reason?.code == 40160) { // channel capability rejected https://help.ably.io/error/40160 - handleChannelAuthError(ablyChannel); + }); } } -const handleChannelAuthError = (ablyChannel: AblyChannel) => { - const channelName = ablyChannel.name; - authRequestExecuter.request(channelName).then(jwtToken => { // get upgraded token with channel access - ablyChannel.ably.auth.authorize(null, { ...authOptions, token: toTokenDetails(jwtToken) }, (err, _tokenDetails) => { - if (err) { - ablyChannel._publishErrors(err); - } else { - ablyChannel.channel.attach(); - } - }); - }); -} \ No newline at end of file diff --git a/src/channel/ably/token-request.ts b/src/channel/ably/token-request.ts index e4bb1645..9835c335 100644 --- a/src/channel/ably/token-request.ts +++ b/src/channel/ably/token-request.ts @@ -1,11 +1,11 @@ -import { getSignedToken } from './mock-auth-server'; - export class SequentialAuthTokenRequestExecuter { - cachedToken = null; + cachedToken: string; queue: TaskQueue; + requestTokenFn: Function; - constructor(token : string = null) { + constructor(token: string = null, requestTokenFn: Function) { this.cachedToken = token; + this.requestTokenFn = requestTokenFn; this.queue = new TaskQueue(); } @@ -21,7 +21,7 @@ export class SequentialAuthTokenRequestExecuter { }) }) - request = channelName => this.execute(token => getSignedToken(channelName, token)); + request = channelName => this.execute(token => this.requestTokenFn(channelName, token)); } class TaskQueue { From 7e29ad07dbb9f63cb22b71dde5adf8b86a421ad3 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 10:26:50 +0530 Subject: [PATCH 029/111] Added extra AblyAuth param to ably private/presence channel constuctors --- src/channel/ably-presence-channel.ts | 6 +++--- src/channel/ably-private-channel.ts | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts index f3f824b6..fb50dbd8 100644 --- a/src/channel/ably-presence-channel.ts +++ b/src/channel/ably-presence-channel.ts @@ -1,15 +1,15 @@ import { AblyChannel } from './ably-channel'; +import { AblyAuth } from './ably/auth'; import { PresenceChannel } from './presence-channel'; -import { onChannelFailed } from './ably'; /** * This class represents an Ably presence channel. */ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel { - constructor(ably: any, name: string, options: any) { + constructor(ably: any, name: string, options: any, auth: AblyAuth) { super(ably, name, options); - this.channel.on("failed", onChannelFailed(this)); + this.channel.on("failed", auth.onChannelFailed(this)); } /** * Register a callback to be called anytime the member list changes. diff --git a/src/channel/ably-private-channel.ts b/src/channel/ably-private-channel.ts index cc9175b3..8aa3d9a3 100644 --- a/src/channel/ably-private-channel.ts +++ b/src/channel/ably-private-channel.ts @@ -1,10 +1,11 @@ import { AblyChannel } from './ably-channel'; -import { onChannelFailed } from './ably'; +import { AblyAuth } from './ably/auth'; + export class AblyPrivateChannel extends AblyChannel { - constructor(ably: any, name: string, options: any) { + constructor(ably: any, name: string, options: any, auth: AblyAuth) { super(ably, name, options); - this.channel.on("failed", onChannelFailed(this)); + this.channel.on("failed", auth.onChannelFailed(this)); } /** * Trigger client event on the channel. From af13dddfff2a8f6cac93dcb7caed63992dd4eb43 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 10:27:28 +0530 Subject: [PATCH 030/111] Updated AblyConnector with AblyAuth class --- src/connector/ably-connector.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index ad5ad848..d3e3ce8d 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -5,9 +5,9 @@ import { AblyChannel, AblyPrivateChannel, AblyPresenceChannel, - authOptions, - applyAuthorizeBeforeChannelAttach + AblyAuth, } from './../channel'; +import { SequentialAuthTokenRequestExecuter } from '../channel/ably/token-request'; /** * This class creates a connector to Ably. */ @@ -22,6 +22,17 @@ export class AblyConnector extends Connector { */ channels: Record = {}; + /** + * Auth instance containing all explicit channel authz ops. + */ + ablyAuth: AblyAuth; + + /** + * Create a new class instance. + */ + constructor(options: any) { + super(options); + } /** * Create a fresh Ably connection. */ @@ -29,8 +40,9 @@ export class AblyConnector extends Connector { if (typeof this.options.client !== 'undefined') { this.ably = this.options.client; } else { - this.ably = new Ably.Realtime({...this.options, ...authOptions}); - applyAuthorizeBeforeChannelAttach(this.ably); + this.ablyAuth = new AblyAuth(this.options); + this.ably = new Ably.Realtime({ ...this.ablyAuth.authOptions, ...this.options }); + this.ablyAuth.enableAuthorizeBeforeChannelAttach(this.ably); } } @@ -59,7 +71,7 @@ export class AblyConnector extends Connector { privateChannel(name: string): AblyPrivateChannel { const prefixedName = `private:${name}`; // adding private as a ably namespace prefix if (!this.channels[prefixedName]) { - this.channels[prefixedName] = new AblyPrivateChannel(this.ably, prefixedName, this.options); + this.channels[prefixedName] = new AblyPrivateChannel(this.ably, prefixedName, this.options, this.ablyAuth); } return this.channels[prefixedName] as AblyPrivateChannel; @@ -71,7 +83,7 @@ export class AblyConnector extends Connector { presenceChannel(name: string): AblyPresenceChannel { const prefixedName = `presence:${name}`; // adding presence as a ably namespace prefix if (!this.channels[prefixedName]) { - this.channels[prefixedName] = new AblyPresenceChannel(this.ably, prefixedName, this.options); + this.channels[prefixedName] = new AblyPresenceChannel(this.ably, prefixedName, this.options, this.ablyAuth); } return this.channels[prefixedName] as AblyPresenceChannel; From 4db8709d2e844d9b43502b0ad9d03a43e20cf94e Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 10:28:01 +0530 Subject: [PATCH 031/111] Refactored channel attach, added flag to make sure it's executed only once --- src/channel/ably/attach.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/channel/ably/attach.ts b/src/channel/ably/attach.ts index 7f02744f..6c5182bd 100644 --- a/src/channel/ably/attach.ts +++ b/src/channel/ably/attach.ts @@ -1,7 +1,15 @@ import { isNullOrUndefined } from './utils'; -export const beforeChannelAttach = (ablyClient, authorize) => { +let channelAttachPatched = false; + +/** + * Modifies existing channel attach with custom authz implementation + */ +export const beforeChannelAttach = (ablyClient, authorize: Function) => { const dummyRealtimeChannel = ablyClient.channels.get("dummy"); + if (channelAttachPatched) { //Only once all ably instance + return; + } const internalAttach = dummyRealtimeChannel.__proto__._attach; // get parent class method, store it in temp. variable if (isNullOrUndefined(internalAttach)) { console.warn("channel internal attach function not found, please check for right library version") @@ -20,4 +28,5 @@ export const beforeChannelAttach = (ablyClient, authorize) => { }) } dummyRealtimeChannel.__proto__._attach = customInternalAttach; // add updated extension method to parent class, auto binded + channelAttachPatched = true; } From ab29e153a99df351406caa6ad8e66c0d2ecafd8d Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 10:45:04 +0530 Subject: [PATCH 032/111] Updated utils with base64 implementation --- src/channel/ably/utils.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 26bf28bd..078bf61f 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -7,12 +7,12 @@ export const parseJwt = (token) => { // Get Token Header const base64HeaderUrl = token.split('.')[0]; const base64Header = base64HeaderUrl.replace('-', '+').replace('_', '/'); - const headerData = JSON.parse(atob(base64Header)); + const headerData = JSON.parse(asciiToBase64(base64Header)); // Get Token payload and date's const base64Url = token.split('.')[1]; const base64 = base64Url.replace('-', '+').replace('_', '/'); - const dataJWT = JSON.parse(atob(base64)); + const dataJWT = JSON.parse(asciiToBase64(base64)); dataJWT.header = headerData; // TODO: add expiration at check ... @@ -34,10 +34,15 @@ export const toTokenDetails = (jwtTokenString) => { } } -const btoa = (text) => { +let base64toAscii = (text: string) => { return Buffer.from(text, 'binary').toString('base64'); }; -const atob = (base64) => { +let asciiToBase64 = (base64: string) => { return Buffer.from(base64, 'base64').toString('binary'); -}; \ No newline at end of file +}; + +if (window) { + base64toAscii = btoa; + asciiToBase64 = atob; +} \ No newline at end of file From a84e195cc8c0a692179d99f8246a3575f2a25af8 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 10:52:24 +0530 Subject: [PATCH 033/111] Moved mock-auth-server under tests --- package.json | 1 + {src/channel => tests}/ably/mock-auth-server.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) rename {src/channel => tests}/ably/mock-auth-server.ts (81%) diff --git a/package.json b/package.json index 9032febe..6f2fd42a 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "ably": "^1.2.20", "eslint": "^8.11.0", "jest": "^27.5.1", + "jsonwebtoken": "^8.5.1", "rollup": "^2.70.1", "rollup-plugin-typescript2": "^0.31.2", "standard-version": "^9.3.2", diff --git a/src/channel/ably/mock-auth-server.ts b/tests/ably/mock-auth-server.ts similarity index 81% rename from src/channel/ably/mock-auth-server.ts rename to tests/ably/mock-auth-server.ts index fbd8143c..3a2541e8 100644 --- a/src/channel/ably/mock-auth-server.ts +++ b/tests/ably/mock-auth-server.ts @@ -1,6 +1,6 @@ -import {isNullOrUndefinedOrEmpty, parseJwt} from './utils' -const Ably = require("ably/promises"); -const jwt = require("jsonwebtoken"); +import { isNullOrUndefinedOrEmpty, parseJwt } from '../../src/channel/ably/utils'; +import * as Ably from "ably/promises"; +import * as jwt from "jsonwebtoken"; const API_KEY = ''; const APP_ID = API_KEY.split('.')[0], @@ -16,12 +16,12 @@ const tokenInvalidOrExpired = (serverTime, token) => { return tokenInvalid || parsedJwt.exp * 1000 <= serverTime; }; -const clientId = 'sacdaddy@gmail.com' +const clientId = 'sacOO7@github.com' export const getSignedToken = async (channelName = null, token = null) => { const header = { - "typ":"JWT", - "alg":"HS256", + "typ": "JWT", + "alg": "HS256", "kid": KEY_NAME } // Set capabilities for public channel as per https://ably.com/docs/core-features/authentication#capability-operations @@ -35,7 +35,7 @@ export const getSignedToken = async (channelName = null, token = null) => { exp = parsedJwt.exp; channelClaims = new Set(parsedJwt['x-ably-capability'].slice(1, -1).split(',')); } else { - iat = Math.round(serverTime/1000); + iat = Math.round(serverTime / 1000); exp = iat + 60; /* time of expiration in seconds */ } if (!isNullOrUndefinedOrEmpty(channelName)) { @@ -49,5 +49,5 @@ export const getSignedToken = async (channelName = null, token = null) => { "x-ably-capability": `{${capabilities}}` } // signJWT(header, claims, KEY_SECRET); broken - return jwt.sign(claims, KEY_SECRET, {header}); + return jwt.sign(claims, KEY_SECRET, { header }); } \ No newline at end of file From ad69c0ff2f8b84e6a40ec5e54ea60ca84526889a Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 11:16:45 +0530 Subject: [PATCH 034/111] Refactored utils, added proper types --- src/channel/ably/utils.ts | 41 ++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 078bf61f..4cfe6512 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -2,35 +2,32 @@ export const isNullOrUndefined = (obj) => obj == null || obj === undefined; export let isEmptyString = (stringToCheck, ignoreSpaces = true) => (ignoreSpaces ? stringToCheck.trim() : stringToCheck) === ''; export const isNullOrUndefinedOrEmpty = (obj) => obj == null || obj === undefined || isEmptyString(obj); -export const parseJwt = (token) => { - try { - // Get Token Header - const base64HeaderUrl = token.split('.')[0]; - const base64Header = base64HeaderUrl.replace('-', '+').replace('_', '/'); - const headerData = JSON.parse(asciiToBase64(base64Header)); - - // Get Token payload and date's - const base64Url = token.split('.')[1]; - const base64 = base64Url.replace('-', '+').replace('_', '/'); - const dataJWT = JSON.parse(asciiToBase64(base64)); - dataJWT.header = headerData; - - // TODO: add expiration at check ... - - return dataJWT; - } catch (err) { - return false; - } +/** + * @throws Exception if parsing error + */ +export const parseJwt = (jwtToken: string) => { + // Get Token Header + const base64HeaderUrl = jwtToken.split('.')[0]; + const base64Header = base64HeaderUrl.replace('-', '+').replace('_', '/'); + const headerData = JSON.parse(asciiToBase64(base64Header)); + + // Get Token payload and date's + const base64Url = jwtToken.split('.')[1]; + const base64 = base64Url.replace('-', '+').replace('_', '/'); + const dataJWT = JSON.parse(asciiToBase64(base64)); + dataJWT.header = headerData; + + return dataJWT; } -export const toTokenDetails = (jwtTokenString) => { - const parsedJwt = parseJwt(jwtTokenString); +export const toTokenDetails = (jwtToken: string) => { + const parsedJwt = parseJwt(jwtToken); return { capability: parsedJwt['x-ably-capability'], clientId: parsedJwt['x-ably-clientId'], expires: parsedJwt.exp * 1000, // Convert Seconds to ms issued: parsedJwt.iat * 1000, - token: jwtTokenString + token: jwtToken } } From ecbfc1475d274bcb1c240cdcb81962eceed44f95 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 11:20:39 +0530 Subject: [PATCH 035/111] Added proper exception handling for auth failures --- src/channel/ably/auth.ts | 4 ++-- src/channel/ably/token-request.ts | 4 ++-- src/connector/ably-connector.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 3bafe26c..f73b5aac 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -59,7 +59,7 @@ export class AblyAuth { errorCallback(null); } }); - }) + }).catch(err => errorCallback(err)); // TODO : Check if errors/exceptions are properly handled }); } @@ -79,7 +79,7 @@ export class AblyAuth { ablyChannel.channel.attach(); } }); - }); + }).catch(err => ablyChannel._publishErrors(err)); } } diff --git a/src/channel/ably/token-request.ts b/src/channel/ably/token-request.ts index 9835c335..9a5d86ed 100644 --- a/src/channel/ably/token-request.ts +++ b/src/channel/ably/token-request.ts @@ -9,7 +9,7 @@ export class SequentialAuthTokenRequestExecuter { this.queue = new TaskQueue(); } - execute = (tokenRequestFn) => new Promise(async (resolve, reject) => { + execute = (tokenRequestFn): Promise => new Promise(async (resolve, reject) => { await this.queue.run(async () => { try { const token = await tokenRequestFn(this.cachedToken); @@ -21,7 +21,7 @@ export class SequentialAuthTokenRequestExecuter { }) }) - request = channelName => this.execute(token => this.requestTokenFn(channelName, token)); + request = (channelName):Promise => this.execute(token => this.requestTokenFn(channelName, token)); } class TaskQueue { diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index d3e3ce8d..6112ee1e 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -7,7 +7,7 @@ import { AblyPresenceChannel, AblyAuth, } from './../channel'; -import { SequentialAuthTokenRequestExecuter } from '../channel/ably/token-request'; + /** * This class creates a connector to Ably. */ From 64e3d892202f9317c54567215828c6bcf56c5d52 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 13:17:47 +0530 Subject: [PATCH 036/111] Refactored utils atob and btoa functions --- src/channel/ably/utils.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 4cfe6512..89364bc2 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -9,12 +9,12 @@ export const parseJwt = (jwtToken: string) => { // Get Token Header const base64HeaderUrl = jwtToken.split('.')[0]; const base64Header = base64HeaderUrl.replace('-', '+').replace('_', '/'); - const headerData = JSON.parse(asciiToBase64(base64Header)); + const headerData = JSON.parse(toText(base64Header)); // Get Token payload and date's const base64Url = jwtToken.split('.')[1]; const base64 = base64Url.replace('-', '+').replace('_', '/'); - const dataJWT = JSON.parse(asciiToBase64(base64)); + const dataJWT = JSON.parse(toText(base64)); dataJWT.header = headerData; return dataJWT; @@ -31,15 +31,18 @@ export const toTokenDetails = (jwtToken: string) => { } } -let base64toAscii = (text: string) => { +const isBrowser = typeof window === 'object'; + +let toBase64 = (text: string) => { + if (isBrowser) { + return btoa(text); + } return Buffer.from(text, 'binary').toString('base64'); }; -let asciiToBase64 = (base64: string) => { +let toText = (base64: string) => { + if (isBrowser) { + return atob(base64); + } return Buffer.from(base64, 'base64').toString('binary'); -}; - -if (window) { - base64toAscii = btoa; - asciiToBase64 = atob; -} \ No newline at end of file +}; \ No newline at end of file From 4af15a1fc9786d9e18de22f5ad7abb184fe91f17 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 14:10:22 +0530 Subject: [PATCH 037/111] Added sandbox test-app-setup script --- tests/ably/test-app-setup.json | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/ably/test-app-setup.json diff --git a/tests/ably/test-app-setup.json b/tests/ably/test-app-setup.json new file mode 100644 index 00000000..41d4b6e4 --- /dev/null +++ b/tests/ably/test-app-setup.json @@ -0,0 +1,48 @@ +{ + "_post_apps": "/* JSON body using in POST sandbox-rest.ably.io/apps request to set up the Test app */", + "post_apps": { + "limits": { "presence": { "maxMembers": 250 } }, + "keys": [ + {}, + { + "capability": "{ \"cansubscribe:*\":[\"subscribe\"], \"canpublish:*\":[\"publish\"], \"canpublish:andpresence\":[\"presence\",\"publish\"], \"pushenabled:*\":[\"publish\",\"subscribe\",\"push-subscribe\"], \"pushenabled:admin:*\":[\"publish\",\"subscribe\",\"push-admin\"] }" + }, + { + "capability": "{ \"channel0\":[\"publish\"], \"channel1\":[\"publish\"], \"channel2\":[\"publish\", \"subscribe\"], \"channel3\":[\"subscribe\"], \"channel4\":[\"presence\", \"publish\", \"subscribe\"], \"channel5\":[\"presence\"], \"channel6\":[\"*\"] }" + }, + { + "capability": "{ \"*\":[\"subscribe\"] }" + } + ], + "namespaces": [ + { "id": "persisted", "persisted": true }, + { "id": "pushenabled", "pushEnabled": true } + ], + "channels": [ + { + "name": "persisted:presence_fixtures", + "presence": [ + { "clientId": "client_bool", "data": "true" }, + { "clientId": "client_int", "data": "24" }, + { "clientId": "client_string", "data": "This is a string clientData payload" }, + { "clientId": "client_json", "data": "{ \"test\": \"This is a JSONObject clientData payload\"}" }, + { "clientId": "client_decoded", "data": "{\"example\":{\"json\":\"Object\"}}", "encoding": "json" }, + { + "clientId": "client_encoded", + "data": "HO4cYSP8LybPYBPZPHQOtuD53yrD3YV3NBoTEYBh4U0N1QXHbtkfsDfTspKeLQFt", + "encoding": "json/utf-8/cipher+aes-128-cbc/base64" + } + ] + } + ] + }, + + "_cipher": "/* Cipher configuration for client_encoded presence fixture data used in REST tests */", + "cipher": { + "algorithm": "aes", + "mode": "cbc", + "keylength": 128, + "key": "WUP6u0K7MXI5Zeo0VppPwg==", + "iv": "HO4cYSP8LybPYBPZPHQOtg==" + } + } \ No newline at end of file From 83fe4dc122156efac1064b3fdcb51f25ae76bb11 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 14:10:49 +0530 Subject: [PATCH 038/111] Added sandbox file to create and delete test app --- tests/ably/sandbox.ts | 112 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 tests/ably/sandbox.ts diff --git a/tests/ably/sandbox.ts b/tests/ably/sandbox.ts new file mode 100644 index 00000000..d60fc898 --- /dev/null +++ b/tests/ably/sandbox.ts @@ -0,0 +1,112 @@ +const http = require('http'); +const https = require('https'); +const fs = require('fs'); + +let restHost = 'sandbox-rest.ably.io'; +const tlsPort = 443; +const toBase64 = (text: string) => Buffer.from(text, 'binary').toString('base64'); + +const loadJsonData = (dataPath, callback) => { + fs.readFile(dataPath, (err, data: Buffer) => { + if (err) { + callback(err); + return; + } + try { + data = JSON.parse(data.toString()); + } catch (e) { + callback(e); + return; + } + callback(null, data); + }); +} + +const httpReq = (options, callback) => { + var body = options.body; + delete options.body; + var response = ''; + var request = (options.scheme == 'http' ? http : https).request(options, function (res) { + res.setEncoding('utf8'); + res.on('data', function (chunk) { + response += chunk; + }); + res.on('end', function () { + if (res.statusCode >= 300) { + callback('Invalid HTTP request: ' + response + '; statusCode = ' + res.statusCode); + } else { + callback(null, response); + } + }); + }); + request.on('error', function (err) { + callback(err); + }); + request.end(body); +} + +function prefixDomainWithEnvironment(domain, environment) { + if (environment.toLowerCase() === 'production') { + return domain; + } else { + return environment + '-' + domain; + } +} + +const creatNewApp = (callback) => { + loadJsonData('test-app-setup.json', function (err, testData) { + if (err) { + callback(err); + return; + } + var postData = JSON.stringify(testData.post_apps); + var postOptions = { + host: restHost, + port: tlsPort, + path: '/apps', + method: 'POST', + scheme: 'https', + headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'Content-Length': postData.length }, + body: postData, + }; + + httpReq(postOptions, function (err, res) { + if (err) { + callback(err); + } else { + if (typeof res === 'string') res = JSON.parse(res); + if (res.keys.length != testData.post_apps.keys.length) { + callback('Failed to create correct number of keys for app'); + } else if (res.namespaces.length != testData.post_apps.namespaces.length) { + callback('Failed to create correct number of namespaces for app'); + } else { + var testApp = { + accountId: res.accountId, + appId: res.appId, + keys: res.keys, + cipherConfig: testData.cipher, + }; + callback(null, testApp); + } + } + }); + }); +} + +const deleteApp = (app, callback) => { + var authKey = app.keys[0].keyStr, + authHeader = toBase64(authKey); + + var delOptions = { + host: restHost, + port: tlsPort, + method: 'DELETE', + path: '/apps/' + app.appId, + scheme: 'https', + headers: { Authorization: 'Basic ' + authHeader }, + }; + + httpReq(delOptions, function (err) { + callback(err); + }); +} \ No newline at end of file From 596ef8d7d46fc525d845bf6ced010c1bf8977ff3 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 14:11:09 +0530 Subject: [PATCH 039/111] Refactored ably utils, applied constants --- src/channel/ably/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 89364bc2..9cd82587 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -1,5 +1,5 @@ export const isNullOrUndefined = (obj) => obj == null || obj === undefined; -export let isEmptyString = (stringToCheck, ignoreSpaces = true) => (ignoreSpaces ? stringToCheck.trim() : stringToCheck) === ''; +export const isEmptyString = (stringToCheck, ignoreSpaces = true) => (ignoreSpaces ? stringToCheck.trim() : stringToCheck) === ''; export const isNullOrUndefinedOrEmpty = (obj) => obj == null || obj === undefined || isEmptyString(obj); /** @@ -33,14 +33,14 @@ export const toTokenDetails = (jwtToken: string) => { const isBrowser = typeof window === 'object'; -let toBase64 = (text: string) => { +const toBase64 = (text: string) => { if (isBrowser) { return btoa(text); } return Buffer.from(text, 'binary').toString('base64'); }; -let toText = (base64: string) => { +const toText = (base64: string) => { if (isBrowser) { return atob(base64); } From 0bf35b7c068e5ede1af1d54d2c7bbb6cd35cf616 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 15:44:13 +0530 Subject: [PATCH 040/111] Refactored code, added basic sandbox test --- tests/ably/sandbox.test.ts | 46 ++++++++++++++++++++++ tests/ably/{ => setup}/mock-auth-server.ts | 2 +- tests/ably/{ => setup}/sandbox.ts | 45 +++++++++++++++------ tests/ably/{ => setup}/test-app-setup.json | 0 4 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 tests/ably/sandbox.test.ts rename tests/ably/{ => setup}/mock-auth-server.ts (95%) rename tests/ably/{ => setup}/sandbox.ts (76%) rename tests/ably/{ => setup}/test-app-setup.json (100%) diff --git a/tests/ably/sandbox.test.ts b/tests/ably/sandbox.test.ts new file mode 100644 index 00000000..0ecbe281 --- /dev/null +++ b/tests/ably/sandbox.test.ts @@ -0,0 +1,46 @@ +import { setup, tearDown } from './setup/sandbox'; +import * as Ably from 'ably'; + +jest.setTimeout(300000); +describe('AblySandbox', () => { + let testApp; + + beforeAll(done => { + setup((err, app) => { + if (err) { + done(err); + return; + } + testApp = app; + done(); + }) + }) + + test('init with key string', () => { + const apiKey = testApp.keys[0].keyStr; + expect(typeof apiKey).toBe('string'); + expect(apiKey).toBeTruthy(); + }) + + test('rest time should work', (done) => { + const apiKey = testApp.keys[0].keyStr; + const restClient = new Ably.Rest(apiKey); + restClient.time((err, time) => { + if (err) { + done(err); + return; + } + expect(typeof time).toBe('number'); + }); + }) + + afterAll((done) => { + tearDown(testApp, (err) => { + if (err) { + done(err); + return; + } + done(); + }) + }) +}); \ No newline at end of file diff --git a/tests/ably/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts similarity index 95% rename from tests/ably/mock-auth-server.ts rename to tests/ably/setup/mock-auth-server.ts index 3a2541e8..bbf19cda 100644 --- a/tests/ably/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -1,4 +1,4 @@ -import { isNullOrUndefinedOrEmpty, parseJwt } from '../../src/channel/ably/utils'; +import { isNullOrUndefinedOrEmpty, parseJwt } from '../../../src/channel/ably/utils'; import * as Ably from "ably/promises"; import * as jwt from "jsonwebtoken"; diff --git a/tests/ably/sandbox.ts b/tests/ably/setup/sandbox.ts similarity index 76% rename from tests/ably/sandbox.ts rename to tests/ably/setup/sandbox.ts index d60fc898..2a6cf6cf 100644 --- a/tests/ably/sandbox.ts +++ b/tests/ably/setup/sandbox.ts @@ -1,13 +1,14 @@ -const http = require('http'); -const https = require('https'); -const fs = require('fs'); +import path from "path"; +import http from 'http'; +import https from 'https'; +import fs from 'fs'; let restHost = 'sandbox-rest.ably.io'; const tlsPort = 443; const toBase64 = (text: string) => Buffer.from(text, 'binary').toString('base64'); const loadJsonData = (dataPath, callback) => { - fs.readFile(dataPath, (err, data: Buffer) => { + fs.readFile(path.join(__dirname, dataPath), (err, data: Buffer) => { if (err) { callback(err); return; @@ -95,18 +96,36 @@ const creatNewApp = (callback) => { const deleteApp = (app, callback) => { var authKey = app.keys[0].keyStr, - authHeader = toBase64(authKey); + authHeader = toBase64(authKey); var delOptions = { - host: restHost, - port: tlsPort, - method: 'DELETE', - path: '/apps/' + app.appId, - scheme: 'https', - headers: { Authorization: 'Basic ' + authHeader }, + host: restHost, + port: tlsPort, + method: 'DELETE', + path: '/apps/' + app.appId, + scheme: 'https', + headers: { Authorization: 'Basic ' + authHeader }, }; httpReq(delOptions, function (err) { - callback(err); + callback(err); }); -} \ No newline at end of file +} + +// creatNewApp((err, testApp) => { +// if (err) { +// console.error('error creating the app'); +// } else { +// console.log('created app') +// console.log(testApp); +// deleteApp(testApp, (err) => { +// if (err) { +// console.error(err) +// } else { +// console.log('deleted test app successfully') +// } +// }) +// } +// }); + +export { creatNewApp as setup, deleteApp as tearDown } \ No newline at end of file diff --git a/tests/ably/test-app-setup.json b/tests/ably/setup/test-app-setup.json similarity index 100% rename from tests/ably/test-app-setup.json rename to tests/ably/setup/test-app-setup.json From 4d8662ff7e570d9c227841b8c11f109bbc6a2b62 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 19:09:32 +0530 Subject: [PATCH 041/111] Fixed failing ably sandbox test --- tests/ably/sandbox.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ably/sandbox.test.ts b/tests/ably/sandbox.test.ts index 0ecbe281..acfe9401 100644 --- a/tests/ably/sandbox.test.ts +++ b/tests/ably/sandbox.test.ts @@ -1,7 +1,7 @@ import { setup, tearDown } from './setup/sandbox'; import * as Ably from 'ably'; -jest.setTimeout(300000); +jest.setTimeout(20000); describe('AblySandbox', () => { let testApp; @@ -31,6 +31,7 @@ describe('AblySandbox', () => { return; } expect(typeof time).toBe('number'); + done() }); }) From b5dc73258d0a7331e8765006bf7b4cfde071b302 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 23:43:44 +0530 Subject: [PATCH 042/111] Refactored mock-auth server, added class wrapper --- tests/ably/setup/mock-auth-server.ts | 94 +++++++++++++++------------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index bbf19cda..ab14bf38 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -2,52 +2,56 @@ import { isNullOrUndefinedOrEmpty, parseJwt } from '../../../src/channel/ably/ut import * as Ably from "ably/promises"; import * as jwt from "jsonwebtoken"; -const API_KEY = ''; -const APP_ID = API_KEY.split('.')[0], - KEY_PARTS = API_KEY.split(':'), - KEY_NAME = KEY_PARTS[0], - KEY_SECRET = KEY_PARTS[1]; +export class MockAuthServer { + keyName: string; + keySecret: string; + ablyClient: Ably.Rest; + clientId = 'sacOO7@github.com' -const ablyClient = new Ably.Rest(API_KEY); - -const tokenInvalidOrExpired = (serverTime, token) => { - const tokenInvalid = false; - const parsedJwt = parseJwt(token); - return tokenInvalid || parsedJwt.exp * 1000 <= serverTime; -}; - -const clientId = 'sacOO7@github.com' - -export const getSignedToken = async (channelName = null, token = null) => { - const header = { - "typ": "JWT", - "alg": "HS256", - "kid": KEY_NAME + constructor(apiKey: string) { + const keys = apiKey.split(':'); + this.keyName = keys[0]; + this.keySecret = keys[1]; + this.ablyClient = new Ably.Rest(apiKey); } - // Set capabilities for public channel as per https://ably.com/docs/core-features/authentication#capability-operations - let channelClaims = new Set(['"public:*":["subscribe", "history", "channel-metadata"]']); - let iat = 0; - let exp = 0; - let serverTime = await ablyClient.time(); - if (!isNullOrUndefinedOrEmpty(token) && !tokenInvalidOrExpired(serverTime, token)) { + + tokenInvalidOrExpired = (serverTime, token) => { + const tokenInvalid = false; const parsedJwt = parseJwt(token); - iat = parsedJwt.iat; - exp = parsedJwt.exp; - channelClaims = new Set(parsedJwt['x-ably-capability'].slice(1, -1).split(',')); - } else { - iat = Math.round(serverTime / 1000); - exp = iat + 60; /* time of expiration in seconds */ - } - if (!isNullOrUndefinedOrEmpty(channelName)) { - channelClaims.add(`"${channelName}":["*"]`) - } - const capabilities = Array.from(channelClaims).join(','); - const claims = { - iat, - exp, - "x-ably-clientId": clientId, - "x-ably-capability": `{${capabilities}}` + return tokenInvalid || parsedJwt.exp * 1000 <= serverTime; + }; + + getSignedToken = async (channelName = null, token = null) => { + const header = { + "typ": "JWT", + "alg": "HS256", + "kid": this.keyName + } + // Set capabilities for public channel as per https://ably.com/docs/core-features/authentication#capability-operations + let channelClaims = new Set(['"public:*":["subscribe", "history", "channel-metadata"]']); + let iat = 0; + let exp = 0; + let serverTime = await this.ablyClient.time(); + if (!isNullOrUndefinedOrEmpty(token) && !this.tokenInvalidOrExpired(serverTime, token)) { + const parsedJwt = parseJwt(token); + iat = parsedJwt.iat; + exp = parsedJwt.exp; + channelClaims = new Set(parsedJwt['x-ably-capability'].slice(1, -1).split(',')); + } else { + iat = Math.round(serverTime / 1000); + exp = iat + 60; /* time of expiration in seconds */ + } + if (!isNullOrUndefinedOrEmpty(channelName)) { + channelClaims.add(`"${channelName}":["*"]`) + } + const capabilities = Array.from(channelClaims).join(','); + const claims = { + iat, + exp, + "x-ably-clientId": this.clientId, + "x-ably-capability": `{${capabilities}}` + } + return jwt.sign(claims, this.keySecret, { header }); } - // signJWT(header, claims, KEY_SECRET); broken - return jwt.sign(claims, KEY_SECRET, { header }); -} \ No newline at end of file +} + From 44cb7d329e7d291d1e98d32565e20ed81c99043e Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 23:45:33 +0530 Subject: [PATCH 043/111] Removed unnecessary constructor from ably-connector --- src/connector/ably-connector.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index 6112ee1e..dc351351 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -27,12 +27,6 @@ export class AblyConnector extends Connector { */ ablyAuth: AblyAuth; - /** - * Create a new class instance. - */ - constructor(options: any) { - super(options); - } /** * Create a fresh Ably connection. */ From 6bd8b79d5284a1ae4fa537adbb73d12997ffdc28 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 1 Jun 2022 23:46:23 +0530 Subject: [PATCH 044/111] Added ably channel and connection test --- tests/ably/ably-channel.test.ts | 47 +++++++++++++++++++++++++++ tests/ably/ably-connection.test.ts | 52 ++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 tests/ably/ably-channel.test.ts create mode 100644 tests/ably/ably-connection.test.ts diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts new file mode 100644 index 00000000..71113b6a --- /dev/null +++ b/tests/ably/ably-channel.test.ts @@ -0,0 +1,47 @@ +import { setup, tearDown } from './setup/sandbox'; +import * as Ably from 'ably'; + +jest.setTimeout(20000); +describe('AblyChannel', () => { + let testApp; + + beforeAll(done => { + setup((err, app) => { + if (err) { + done(err); + return; + } + testApp = app; + done(); + }) + }) + + test('init with key string', () => { + const apiKey = testApp.keys[0].keyStr; + expect(typeof apiKey).toBe('string'); + expect(apiKey).toBeTruthy(); + }) + + test('rest time should work', (done) => { + const apiKey = testApp.keys[0].keyStr; + const restClient = new Ably.Rest(apiKey); + restClient.time((err, time) => { + if (err) { + done(err); + return; + } + expect(typeof time).toBe('number'); + done() + }); + }) + + afterAll((done) => { + tearDown(testApp, (err) => { + if (err) { + done(err); + return; + } + done(); + }) + }) +}); \ No newline at end of file diff --git a/tests/ably/ably-connection.test.ts b/tests/ably/ably-connection.test.ts new file mode 100644 index 00000000..98c22607 --- /dev/null +++ b/tests/ably/ably-connection.test.ts @@ -0,0 +1,52 @@ +import { setup, tearDown } from './setup/sandbox'; +import Echo from '../../src/echo'; +import { MockAuthServer } from './setup/mock-auth-server'; + +jest.setTimeout(20000); +describe('AblyConnection', () => { + let testApp; + let mockAuthServer; + + beforeAll(done => { + console.log('before all called'); + setup((err, app) => { + if (err) { + done(err); + return; + } + testApp = app; + console.log(testApp); + mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); + done(); + }) + }) + + test('should be able to connect to server', (done) => { + const echo = new Echo({ + broadcaster: 'ably', + useTls: true, + environment: 'sandbox', + requestTokenFn: mockAuthServer.getSignedToken + }); + echo.connector.ably.connection.on(({ current, reason }) => { + console.log(current, reason); + if (current == 'connected') { + done(); + } + if (reason) { + done(reason) + } + }); + }) + + afterAll((done) => { + console.log('after all called'); + tearDown(testApp, (err) => { + if (err) { + done(err); + return; + } + done(); + }) + }) +}); \ No newline at end of file From 85778279dfd7278a11813e74fd5d6fe31838ddae Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 2 Jun 2022 18:08:41 +0530 Subject: [PATCH 045/111] Updated channel capability check for auth channels before attaching --- src/channel/ably/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index f73b5aac..00d380c8 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -43,7 +43,7 @@ export class AblyAuth { // Use cached token if has channel capability and is not expired const token = ablyClient.auth.tokenDetails; if (token) { - const tokenHasChannelCapability = token.capability.includes(channelName); + const tokenHasChannelCapability = token.capability.includes(`${channelName}"`); if (tokenHasChannelCapability && token.expires >= Date.now()) { // TODO : Replace with server time errorCallback(null); return; From 23925284fd14bca4434993dfd29e84cf34bfcd09 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 3 Jun 2022 23:15:16 +0530 Subject: [PATCH 046/111] Updated mock-auth server code with capabilities as map --- tests/ably/setup/mock-auth-server.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index ab14bf38..130e4995 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -28,7 +28,7 @@ export class MockAuthServer { "kid": this.keyName } // Set capabilities for public channel as per https://ably.com/docs/core-features/authentication#capability-operations - let channelClaims = new Set(['"public:*":["subscribe", "history", "channel-metadata"]']); + let capabilities = {"public:*":["subscribe", "history", "channel-metadata"]}; let iat = 0; let exp = 0; let serverTime = await this.ablyClient.time(); @@ -36,20 +36,19 @@ export class MockAuthServer { const parsedJwt = parseJwt(token); iat = parsedJwt.iat; exp = parsedJwt.exp; - channelClaims = new Set(parsedJwt['x-ably-capability'].slice(1, -1).split(',')); + capabilities = parsedJwt['x-ably-capability']; } else { iat = Math.round(serverTime / 1000); exp = iat + 60; /* time of expiration in seconds */ } if (!isNullOrUndefinedOrEmpty(channelName)) { - channelClaims.add(`"${channelName}":["*"]`) + capabilities[channelName] = ["*"] } - const capabilities = Array.from(channelClaims).join(','); const claims = { iat, exp, "x-ably-clientId": this.clientId, - "x-ably-capability": `{${capabilities}}` + "x-ably-capability": capabilities } return jwt.sign(claims, this.keySecret, { header }); } From f02375ab0b6fec1d8caa2825999743a582edd305 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 7 Jun 2022 00:35:49 +0530 Subject: [PATCH 047/111] Updated parseJwt, reduced unnecessary tokenDetails size --- src/channel/ably/utils.ts | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 9cd82587..977e287e 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -5,30 +5,39 @@ export const isNullOrUndefinedOrEmpty = (obj) => obj == null || obj === undefine /** * @throws Exception if parsing error */ -export const parseJwt = (jwtToken: string) => { +export const parseJwt = (jwtToken: string, forceParseJson = false): { header: any, payload: any} => { // Get Token Header const base64HeaderUrl = jwtToken.split('.')[0]; const base64Header = base64HeaderUrl.replace('-', '+').replace('_', '/'); - const headerData = JSON.parse(toText(base64Header)); - - // Get Token payload and date's + let header = toText(base64Header); + if (forceParseJson) { + header = JSON.parse(header); + } + // Get Token payload const base64Url = jwtToken.split('.')[1]; const base64 = base64Url.replace('-', '+').replace('_', '/'); - const dataJWT = JSON.parse(toText(base64)); - dataJWT.header = headerData; - - return dataJWT; + let payload = toText(base64); + if (forceParseJson) { + payload = JSON.parse(payload); + } + return {header, payload}; } export const toTokenDetails = (jwtToken: string) => { - const parsedJwt = parseJwt(jwtToken); + const {payload} = parseJwt(jwtToken); + const parsedJwt = JSON.parse(payload, (key, value)=> { + if (key === 'x-ably-capability') { // exclude capability since tokenDetails becomes bloated + return undefined; + } + return value; + }); return { - capability: parsedJwt['x-ably-capability'], + // capability: parsedJwt['x-ably-capability'], // RSA4f - tokenDetails size should't exceed 128kb clientId: parsedJwt['x-ably-clientId'], expires: parsedJwt.exp * 1000, // Convert Seconds to ms issued: parsedJwt.iat * 1000, token: jwtToken - } + }; } const isBrowser = typeof window === 'object'; From 061321d4727e9dbce8ba930b21f2a0f2a3e48a97 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 7 Jun 2022 00:36:16 +0530 Subject: [PATCH 048/111] Updated mockauthserver with updated parseJwt --- tests/ably/setup/mock-auth-server.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index 130e4995..833ed6b2 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -12,13 +12,13 @@ export class MockAuthServer { const keys = apiKey.split(':'); this.keyName = keys[0]; this.keySecret = keys[1]; - this.ablyClient = new Ably.Rest(apiKey); + this.ablyClient = new Ably.Rest(apiKey); } tokenInvalidOrExpired = (serverTime, token) => { const tokenInvalid = false; - const parsedJwt = parseJwt(token); - return tokenInvalid || parsedJwt.exp * 1000 <= serverTime; + const { payload } = parseJwt(token, true); + return tokenInvalid || payload.exp * 1000 <= serverTime; }; getSignedToken = async (channelName = null, token = null) => { @@ -28,15 +28,15 @@ export class MockAuthServer { "kid": this.keyName } // Set capabilities for public channel as per https://ably.com/docs/core-features/authentication#capability-operations - let capabilities = {"public:*":["subscribe", "history", "channel-metadata"]}; + let capabilities = { "public:*": ["subscribe", "history", "channel-metadata"] }; let iat = 0; let exp = 0; let serverTime = await this.ablyClient.time(); if (!isNullOrUndefinedOrEmpty(token) && !this.tokenInvalidOrExpired(serverTime, token)) { - const parsedJwt = parseJwt(token); - iat = parsedJwt.iat; - exp = parsedJwt.exp; - capabilities = parsedJwt['x-ably-capability']; + const { payload } = parseJwt(token, true); + iat = payload.iat; + exp = payload.exp; + capabilities = payload['x-ably-capability']; } else { iat = Math.round(serverTime / 1000); exp = iat + 60; /* time of expiration in seconds */ From f55f56436ce9f59511312ccbeba19d170378395e Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 7 Jun 2022 01:17:11 +0530 Subject: [PATCH 049/111] Added utils test for pasing JWT token and converting to tokenDetails --- tests/ably/utils.test.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/ably/utils.test.ts diff --git a/tests/ably/utils.test.ts b/tests/ably/utils.test.ts new file mode 100644 index 00000000..a51ea46b --- /dev/null +++ b/tests/ably/utils.test.ts @@ -0,0 +1,39 @@ +import { parseJwt, toTokenDetails } from "../../src/channel/ably/utils"; + +describe('Utils', () => { + test('should parse JWT properly', () => { + const token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImFiY2QifQ.eyJpYXQiOjE2NTQ1NDM1NjIsImV4cCI6MTY1NDU0NzE2MiwieC1hYmx5LWNsaWVudElkIjoidXNlcjEyMyIsIngtYWJseS1jYXBhYmlsaXR5Ijp7InB1YmxpYzoqIjpbInN1YnNjcmliZSIsImhpc3RvcnkiLCJjaGFubmVsLW1ldGFkYXRhIl19fQ._9S9i-tKGtpcQbAIk4h0bIs3TpKlXFi0jsUcQ-LaHZs"; + const expectedHeader = { + "typ": "JWT", + "alg": "HS256", + "kid": "abcd" + } + const expectedPayload = { + "iat": 1654543562, + "exp": 1654547162, + "x-ably-clientId": "user123", + "x-ably-capability": { + "public:*": [ + "subscribe", + "history", + "channel-metadata" + ] + } + } + + expect(parseJwt(token).header).toBe(JSON.stringify(expectedHeader)); + expect(parseJwt(token).payload).toBe(JSON.stringify(expectedPayload)); + + expect(parseJwt(token, true).header).toStrictEqual(expectedHeader); + expect(parseJwt(token, true).payload).toStrictEqual(expectedPayload); + }) + + test('should convert to tokenDetails', () => { + const token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImFiY2QifQ.eyJpYXQiOjE2NTQ1NDM1NjIsImV4cCI6MTY1NDU0NzE2MiwieC1hYmx5LWNsaWVudElkIjoidXNlcjEyMyIsIngtYWJseS1jYXBhYmlsaXR5Ijp7InB1YmxpYzoqIjpbInN1YnNjcmliZSIsImhpc3RvcnkiLCJjaGFubmVsLW1ldGFkYXRhIl19fQ._9S9i-tKGtpcQbAIk4h0bIs3TpKlXFi0jsUcQ-LaHZs"; + const tokenDetails = toTokenDetails(token); + expect(tokenDetails.clientId).toBe('user123'); + expect(tokenDetails.expires).toBe(1654547162000); + expect(tokenDetails.issued).toBe(1654543562000); + expect(tokenDetails.token).toBe(token); + }) +}); \ No newline at end of file From 975cd8787b14999c60b399ccde540443c316c1ff Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 7 Jun 2022 01:17:25 +0530 Subject: [PATCH 050/111] Skipped ably-connection test --- tests/ably/ably-connection.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ably/ably-connection.test.ts b/tests/ably/ably-connection.test.ts index 98c22607..001412a1 100644 --- a/tests/ably/ably-connection.test.ts +++ b/tests/ably/ably-connection.test.ts @@ -3,7 +3,7 @@ import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; jest.setTimeout(20000); -describe('AblyConnection', () => { +describe.skip('AblyConnection', () => { let testApp; let mockAuthServer; From 5227ba2aee4eb611b7c6f06c892eaad71d444e4a Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 7 Jun 2022 15:42:51 +0530 Subject: [PATCH 051/111] Refactored utils and auth file --- src/channel/ably/auth.ts | 2 +- src/channel/ably/utils.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 00d380c8..44ac9041 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -72,7 +72,7 @@ export class AblyAuth { handleChannelAuthError = (ablyChannel: AblyChannel) => { const channelName = ablyChannel.name; this.authRequestExecuter.request(channelName).then(jwtToken => { // get upgraded token with channel access - ablyChannel.ably.auth.authorize(null, { ...this.authOptions, token: toTokenDetails(jwtToken) }, (err, _tokenDetails) => { + ablyChannel.ably.auth.authorize(null, { ...this.authOptions, token: toTokenDetails(jwtToken) as any}, (err, _tokenDetails) => { if (err) { ablyChannel._publishErrors(err); } else { diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 977e287e..c521c831 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -5,7 +5,7 @@ export const isNullOrUndefinedOrEmpty = (obj) => obj == null || obj === undefine /** * @throws Exception if parsing error */ -export const parseJwt = (jwtToken: string, forceParseJson = false): { header: any, payload: any} => { +export const parseJwt = (jwtToken: string, forceParseJson = false): { header: any, payload: any } => { // Get Token Header const base64HeaderUrl = jwtToken.split('.')[0]; const base64Header = base64HeaderUrl.replace('-', '+').replace('_', '/'); @@ -20,12 +20,12 @@ export const parseJwt = (jwtToken: string, forceParseJson = false): { header: an if (forceParseJson) { payload = JSON.parse(payload); } - return {header, payload}; + return { header, payload }; } export const toTokenDetails = (jwtToken: string) => { - const {payload} = parseJwt(jwtToken); - const parsedJwt = JSON.parse(payload, (key, value)=> { + const { payload } = parseJwt(jwtToken); + const parsedJwt = JSON.parse(payload, (key, value) => { if (key === 'x-ably-capability') { // exclude capability since tokenDetails becomes bloated return undefined; } From b31e1a327c3f88c2325ad9df5d1fc6b425f863f1 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 01:25:23 +0530 Subject: [PATCH 052/111] Fixed mock-auth server, Added proper parsing and serialization to fields --- tests/ably/setup/mock-auth-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index 833ed6b2..b7485ac6 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -36,7 +36,7 @@ export class MockAuthServer { const { payload } = parseJwt(token, true); iat = payload.iat; exp = payload.exp; - capabilities = payload['x-ably-capability']; + capabilities = JSON.parse(payload['x-ably-capability']); } else { iat = Math.round(serverTime / 1000); exp = iat + 60; /* time of expiration in seconds */ @@ -48,7 +48,7 @@ export class MockAuthServer { iat, exp, "x-ably-clientId": this.clientId, - "x-ably-capability": capabilities + "x-ably-capability": JSON.stringify(capabilities) } return jwt.sign(claims, this.keySecret, { header }); } From cbe377584650b92eca1ae2c44bdb250a2e01235e Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 02:14:03 +0530 Subject: [PATCH 053/111] Refactored utils, updated mock-server and related tests --- src/channel/ably/utils.ts | 24 +++--------- tests/ably/setup/mock-auth-server.ts | 4 +- tests/ably/utils.test.ts | 58 ++++++++++++---------------- 3 files changed, 33 insertions(+), 53 deletions(-) diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index c521c831..63608c39 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -5,37 +5,25 @@ export const isNullOrUndefinedOrEmpty = (obj) => obj == null || obj === undefine /** * @throws Exception if parsing error */ -export const parseJwt = (jwtToken: string, forceParseJson = false): { header: any, payload: any } => { +export const parseJwt = (jwtToken: string): { header: any, payload: any } => { // Get Token Header const base64HeaderUrl = jwtToken.split('.')[0]; const base64Header = base64HeaderUrl.replace('-', '+').replace('_', '/'); - let header = toText(base64Header); - if (forceParseJson) { - header = JSON.parse(header); - } + const header = JSON.parse(toText(base64Header)); // Get Token payload const base64Url = jwtToken.split('.')[1]; const base64 = base64Url.replace('-', '+').replace('_', '/'); - let payload = toText(base64); - if (forceParseJson) { - payload = JSON.parse(payload); - } + const payload = JSON.parse(toText(base64)); return { header, payload }; } export const toTokenDetails = (jwtToken: string) => { const { payload } = parseJwt(jwtToken); - const parsedJwt = JSON.parse(payload, (key, value) => { - if (key === 'x-ably-capability') { // exclude capability since tokenDetails becomes bloated - return undefined; - } - return value; - }); return { // capability: parsedJwt['x-ably-capability'], // RSA4f - tokenDetails size should't exceed 128kb - clientId: parsedJwt['x-ably-clientId'], - expires: parsedJwt.exp * 1000, // Convert Seconds to ms - issued: parsedJwt.iat * 1000, + clientId: payload['x-ably-clientId'], + expires: payload.exp * 1000, // Convert Seconds to ms + issued: payload.iat * 1000, token: jwtToken }; } diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index b7485ac6..292d1221 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -17,7 +17,7 @@ export class MockAuthServer { tokenInvalidOrExpired = (serverTime, token) => { const tokenInvalid = false; - const { payload } = parseJwt(token, true); + const { payload } = parseJwt(token); return tokenInvalid || payload.exp * 1000 <= serverTime; }; @@ -33,7 +33,7 @@ export class MockAuthServer { let exp = 0; let serverTime = await this.ablyClient.time(); if (!isNullOrUndefinedOrEmpty(token) && !this.tokenInvalidOrExpired(serverTime, token)) { - const { payload } = parseJwt(token, true); + const { payload } = parseJwt(token); iat = payload.iat; exp = payload.exp; capabilities = JSON.parse(payload['x-ably-capability']); diff --git a/tests/ably/utils.test.ts b/tests/ably/utils.test.ts index a51ea46b..4a878f2c 100644 --- a/tests/ably/utils.test.ts +++ b/tests/ably/utils.test.ts @@ -1,39 +1,31 @@ import { parseJwt, toTokenDetails } from "../../src/channel/ably/utils"; +// TODO - Update token with string capability describe('Utils', () => { - test('should parse JWT properly', () => { - const token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImFiY2QifQ.eyJpYXQiOjE2NTQ1NDM1NjIsImV4cCI6MTY1NDU0NzE2MiwieC1hYmx5LWNsaWVudElkIjoidXNlcjEyMyIsIngtYWJseS1jYXBhYmlsaXR5Ijp7InB1YmxpYzoqIjpbInN1YnNjcmliZSIsImhpc3RvcnkiLCJjaGFubmVsLW1ldGFkYXRhIl19fQ._9S9i-tKGtpcQbAIk4h0bIs3TpKlXFi0jsUcQ-LaHZs"; - const expectedHeader = { - "typ": "JWT", - "alg": "HS256", - "kid": "abcd" - } - const expectedPayload = { - "iat": 1654543562, - "exp": 1654547162, - "x-ably-clientId": "user123", - "x-ably-capability": { - "public:*": [ - "subscribe", - "history", - "channel-metadata" - ] - } - } + test('should parse JWT properly', () => { + const token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImFiY2QifQ.eyJpYXQiOjE2NTQ2MzQyMTIsImV4cCI6MTY1NDYzNzgxMiwieC1hYmx5LWNsaWVudElkIjoidXNlcjEyMyIsIngtYWJseS1jYXBhYmlsaXR5Ijoie1wicHVibGljOipcIjpbXCJzdWJzY3JpYmVcIixcImhpc3RvcnlcIixcImNoYW5uZWwtbWV0YWRhdGFcIl19In0.GenM5EyUeJvgAGBD_EG-89FueNKWtyRZyi61s9G2Bs4"; + const expectedHeader = { + "alg": "HS256", + "kid": "abcd", + "typ": "JWT", + } + const expectedPayload = { + "iat": 1654634212, + "exp": 1654637812, + "x-ably-clientId": "user123", + "x-ably-capability": '{"public:*":["subscribe","history","channel-metadata"]}' + } - expect(parseJwt(token).header).toBe(JSON.stringify(expectedHeader)); - expect(parseJwt(token).payload).toBe(JSON.stringify(expectedPayload)); - - expect(parseJwt(token, true).header).toStrictEqual(expectedHeader); - expect(parseJwt(token, true).payload).toStrictEqual(expectedPayload); - }) + expect(parseJwt(token).header).toStrictEqual(expectedHeader); + expect(parseJwt(token).payload).toStrictEqual(expectedPayload); + }) - test('should convert to tokenDetails', () => { - const token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImFiY2QifQ.eyJpYXQiOjE2NTQ1NDM1NjIsImV4cCI6MTY1NDU0NzE2MiwieC1hYmx5LWNsaWVudElkIjoidXNlcjEyMyIsIngtYWJseS1jYXBhYmlsaXR5Ijp7InB1YmxpYzoqIjpbInN1YnNjcmliZSIsImhpc3RvcnkiLCJjaGFubmVsLW1ldGFkYXRhIl19fQ._9S9i-tKGtpcQbAIk4h0bIs3TpKlXFi0jsUcQ-LaHZs"; - const tokenDetails = toTokenDetails(token); - expect(tokenDetails.clientId).toBe('user123'); - expect(tokenDetails.expires).toBe(1654547162000); - expect(tokenDetails.issued).toBe(1654543562000); - expect(tokenDetails.token).toBe(token); - }) + test('should convert to tokenDetails', () => { + const token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImFiY2QifQ.eyJpYXQiOjE2NTQ2MzQyMTIsImV4cCI6MTY1NDYzNzgxMiwieC1hYmx5LWNsaWVudElkIjoidXNlcjEyMyIsIngtYWJseS1jYXBhYmlsaXR5Ijoie1wicHVibGljOipcIjpbXCJzdWJzY3JpYmVcIixcImhpc3RvcnlcIixcImNoYW5uZWwtbWV0YWRhdGFcIl19In0.GenM5EyUeJvgAGBD_EG-89FueNKWtyRZyi61s9G2Bs4"; + const tokenDetails = toTokenDetails(token); + expect(tokenDetails.clientId).toBe('user123'); + expect(tokenDetails.expires).toBe(1654637812000); + expect(tokenDetails.issued).toBe(1654634212000); + expect(tokenDetails.token).toBe(token); + }) }); \ No newline at end of file From 37e8c87a4dc2e5a095b3c2ce9499262b6c71e04c Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 14:27:21 +0530 Subject: [PATCH 054/111] Added test to check for the basic working of the sandbox app --- tests/ably/ably-sandbox.test.ts | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/ably/ably-sandbox.test.ts diff --git a/tests/ably/ably-sandbox.test.ts b/tests/ably/ably-sandbox.test.ts new file mode 100644 index 00000000..e66fe8dc --- /dev/null +++ b/tests/ably/ably-sandbox.test.ts @@ -0,0 +1,48 @@ +import { setup, tearDown } from './setup/sandbox'; +import * as Ably from 'ably'; + +jest.setTimeout(20000); +describe('AblySandbox', () => { + let testApp; + + beforeAll(done => { + setup((err, app) => { + if (err) { + done(err); + return; + } + testApp = app; + done(); + }) + }) + + test('init with key string', () => { + const apiKey = testApp.keys[0].keyStr; + expect(typeof apiKey).toBe('string'); + expect(apiKey).toBeTruthy(); + }) + + test('rest time should work', (done) => { + const apiKey = testApp.keys[0].keyStr; + const restClient = new Ably.Rest(apiKey); + expect.assertions(1); + restClient.time((err, time) => { + if (err) { + done(err); + return; + } + expect(typeof time).toBe('number'); + done(); + }); + }) + + afterAll((done) => { + tearDown(testApp, (err) => { + if (err) { + done(err); + return; + } + done(); + }) + }) +}); \ No newline at end of file From 8c4c28ca4c30fca41c9a5da67b3a09a292305f36 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 14:30:08 +0530 Subject: [PATCH 055/111] Fixed AblyConnection test, added checks for connection states --- tests/ably/ably-connection.test.ts | 74 ++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/tests/ably/ably-connection.test.ts b/tests/ably/ably-connection.test.ts index 001412a1..1f33a497 100644 --- a/tests/ably/ably-connection.test.ts +++ b/tests/ably/ably-connection.test.ts @@ -3,50 +3,88 @@ import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; jest.setTimeout(20000); -describe.skip('AblyConnection', () => { +describe('AblyConnection', () => { let testApp; let mockAuthServer; + let echo; beforeAll(done => { - console.log('before all called'); setup((err, app) => { if (err) { done(err); return; } testApp = app; - console.log(testApp); mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); done(); }) }) - test('should be able to connect to server', (done) => { - const echo = new Echo({ + afterAll((done) => { + tearDown(testApp, (err) => { + if (err) { + done(err); + return; + } + done(); + }) + }) + + beforeEach(() => { + echo = new Echo({ broadcaster: 'ably', useTls: true, environment: 'sandbox', requestTokenFn: mockAuthServer.getSignedToken }); - echo.connector.ably.connection.on(({ current, reason }) => { - console.log(current, reason); - if (current == 'connected') { - done(); + }); + + afterEach(() => { + echo.disconnect(); + }); + + test('should be able to connect to server', (done) => { + expect.assertions(2); + echo.connector.ably.connection.on(({ current, previous, reason }) => { + if (current == 'connecting') { + expect(previous).toBe('initialized'); } - if (reason) { - done(reason) + else if (current == 'connected') { + expect(previous).toBe('connecting'); + setTimeout(() => { // Added timeout to make sure connection stays active + echo.connector.ably.connection.off(); + done(); + }, 3000) + } + else if (reason) { + done(reason); + return; } }); }) - afterAll((done) => { - console.log('after all called'); - tearDown(testApp, (err) => { - if (err) { - done(err); + test('should be able to disconnect from server', (done) => { + expect.assertions(4); + echo.connector.ably.connection.on(({ current, previous, reason }) => { + if (current == 'connecting') { + expect(previous).toBe('initialized'); + } + else if (current == 'connected') { + expect(previous).toBe('connecting'); + echo.disconnect(); + } + else if (current == 'closing') { + expect(previous).toBe('connected'); + } + else if (current == 'closed') { + expect(previous).toBe('closing'); + echo.connector.ably.connection.off(); + done(); + } + else if (reason) { + done(reason); return; } - done(); - }) + }); }) }); \ No newline at end of file From 14f631cad936318cc54c18ea64c0600df9d0d921 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 15:40:31 +0530 Subject: [PATCH 056/111] Fixed listening to the event using specified namespace --- src/channel/ably-channel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index 7359b939..7aee0254 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -84,7 +84,7 @@ export class AblyChannel extends Channel { * Listen for an event on the channel instance. */ listen(event: string, callback: Function): AblyChannel { - this.channel.subscribe(event, callback as any); + this.channel.subscribe(this.eventFormatter.format(event), callback as any); return this; } From ec7ca316658d860d4fc1ba8a0939138d2ab9ad9a Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 19:32:06 +0530 Subject: [PATCH 057/111] Fixed ably-channel listen and listen-to-all methods --- src/channel/ably-channel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index 7aee0254..ae1bce41 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -84,7 +84,7 @@ export class AblyChannel extends Channel { * Listen for an event on the channel instance. */ listen(event: string, callback: Function): AblyChannel { - this.channel.subscribe(this.eventFormatter.format(event), callback as any); + this.channel.subscribe(this.eventFormatter.format(event), ({data}) => callback(data)); return this; } @@ -96,7 +96,7 @@ export class AblyChannel extends Channel { this.channel.subscribe(({ name, data }) => { let namespace = this.options.namespace.replace(/\./g, '\\'); - let formattedEvent = name.startsWith(namespace) ? name.substring(namespace.length + 1) : '.' + event; + let formattedEvent = name.startsWith(namespace) ? name.substring(namespace.length + 1) : '.' + name; callback(formattedEvent, data); }); From 5d45949b0e57cf7ced38f4956e16405a1d93a898 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 19:33:13 +0530 Subject: [PATCH 058/111] Updated mock-auth-server, added env. sandbox and broadcast functionality --- tests/ably/setup/mock-auth-server.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index 292d1221..057ec0e3 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -8,11 +8,11 @@ export class MockAuthServer { ablyClient: Ably.Rest; clientId = 'sacOO7@github.com' - constructor(apiKey: string) { + constructor(apiKey: string, environment = "sandbox") { const keys = apiKey.split(':'); this.keyName = keys[0]; this.keySecret = keys[1]; - this.ablyClient = new Ably.Rest(apiKey); + this.ablyClient = new Ably.Rest({key: apiKey, environment}); } tokenInvalidOrExpired = (serverTime, token) => { @@ -21,6 +21,10 @@ export class MockAuthServer { return tokenInvalid || payload.exp * 1000 <= serverTime; }; + broadcast = async (channelName: string, eventName : string, message : string) => { + await this.ablyClient.channels.get(channelName).publish(eventName, message); + } + getSignedToken = async (channelName = null, token = null) => { const header = { "typ": "JWT", From 029599733627d6fd2de7f5394ebbaaa94d4a9878 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 19:35:12 +0530 Subject: [PATCH 059/111] Added safe assert util for testing async done based assertions --- tests/ably/ably-sandbox.test.ts | 4 ++-- tests/ably/setup/utils.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 tests/ably/setup/utils.ts diff --git a/tests/ably/ably-sandbox.test.ts b/tests/ably/ably-sandbox.test.ts index e66fe8dc..5200de7c 100644 --- a/tests/ably/ably-sandbox.test.ts +++ b/tests/ably/ably-sandbox.test.ts @@ -1,5 +1,6 @@ import { setup, tearDown } from './setup/sandbox'; import * as Ably from 'ably'; +import safeAssert from './setup/utils'; jest.setTimeout(20000); describe('AblySandbox', () => { @@ -31,8 +32,7 @@ describe('AblySandbox', () => { done(err); return; } - expect(typeof time).toBe('number'); - done(); + safeAssert(() => expect(typeof time).toBe('number'), done); }); }) diff --git a/tests/ably/setup/utils.ts b/tests/ably/setup/utils.ts new file mode 100644 index 00000000..0184d536 --- /dev/null +++ b/tests/ably/setup/utils.ts @@ -0,0 +1,10 @@ +const safeAssert = ((assertions : Function, done : Function) => { + try { + assertions(); + done(); + } catch(err) { + done(err); + } +}); + +export default safeAssert; \ No newline at end of file From 7cafe3cff54b87a1c127a60dfd123996b6c55f12 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 19:35:50 +0530 Subject: [PATCH 060/111] Added ably-channel tests for listening to the event --- tests/ably/ably-channel.test.ts | 86 +++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index 71113b6a..edccd4ba 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -1,9 +1,14 @@ import { setup, tearDown } from './setup/sandbox'; -import * as Ably from 'ably'; +import Echo from '../../src/echo'; +import { MockAuthServer } from './setup/mock-auth-server'; +import { AblyChannel } from '../../src/channel'; +import safeAssert from './setup/utils'; jest.setTimeout(20000); describe('AblyChannel', () => { - let testApp; + let testApp: any; + let mockAuthServer: MockAuthServer; + let echo: Echo; beforeAll(done => { setup((err, app) => { @@ -12,29 +17,11 @@ describe('AblyChannel', () => { return; } testApp = app; + mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); done(); }) }) - test('init with key string', () => { - const apiKey = testApp.keys[0].keyStr; - expect(typeof apiKey).toBe('string'); - expect(apiKey).toBeTruthy(); - }) - - test('rest time should work', (done) => { - const apiKey = testApp.keys[0].keyStr; - const restClient = new Ably.Rest(apiKey); - restClient.time((err, time) => { - if (err) { - done(err); - return; - } - expect(typeof time).toBe('number'); - done() - }); - }) - afterAll((done) => { tearDown(testApp, (err) => { if (err) { @@ -44,4 +31,61 @@ describe('AblyChannel', () => { done(); }) }) + + beforeEach(() => { + echo = new Echo({ + broadcaster: 'ably', + useTls: true, + environment: 'sandbox', + requestTokenFn: mockAuthServer.getSignedToken + }); + }); + + afterEach(() => { + echo.disconnect(); + }); + + test('channel subscription', (done) => { + echo.channel('test').subscribed(() => { + done(); + }); + }); + + test('Listen for a default broadcaster event', (done) => { + const publicChannel = echo.channel('test') as AblyChannel; + publicChannel + .subscribed(() => { + mockAuthServer.broadcast('public:test', 'App\\Events\\testEvent', 'Hello there'); + }) + .listen('testEvent', data => { + safeAssert(() => expect(data).toBe('Hello there'), done); + }); + }); + + // https://laravel.com/docs/9.x/broadcasting#broadcast-name + test('Listen for a broadcast as event', (done) => { + const publicChannel = echo.channel('test') as AblyChannel; + publicChannel + .subscribed(() => { + mockAuthServer.broadcast('public:test', 'testEvent', 'Hello there'); + }) + .listen('.testEvent', data => { + safeAssert(() => expect(data).toBe('Hello there'), done); + }); + }); + + test('Listen for all events', (done) => { + const publicChannel = echo.channel('test1') as AblyChannel; + publicChannel + .subscribed(() => { + mockAuthServer.broadcast('public:test1', 'testEvent', 'Hello there'); + }) + .listenToAll((eventName, data) => { + safeAssert(() => { + expect(eventName).toBe('.testEvent'); + expect(data).toBe('Hello there'); + }, done); + }); + }) + }); \ No newline at end of file From 2bfa7b820ae679a42b1069ae2650e9e8492ec41d Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 22:54:55 +0530 Subject: [PATCH 061/111] Refactored safeAssert, added check for if it's a final assertion --- tests/ably/ably-channel.test.ts | 6 ++--- tests/ably/ably-connection.test.ts | 42 ++++++++++++++++-------------- tests/ably/ably-sandbox.test.ts | 2 +- tests/ably/setup/utils.ts | 8 +++--- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index edccd4ba..6827e272 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -58,7 +58,7 @@ describe('AblyChannel', () => { mockAuthServer.broadcast('public:test', 'App\\Events\\testEvent', 'Hello there'); }) .listen('testEvent', data => { - safeAssert(() => expect(data).toBe('Hello there'), done); + safeAssert(() => expect(data).toBe('Hello there'), done, true); }); }); @@ -70,7 +70,7 @@ describe('AblyChannel', () => { mockAuthServer.broadcast('public:test', 'testEvent', 'Hello there'); }) .listen('.testEvent', data => { - safeAssert(() => expect(data).toBe('Hello there'), done); + safeAssert(() => expect(data).toBe('Hello there'), done, true); }); }); @@ -84,7 +84,7 @@ describe('AblyChannel', () => { safeAssert(() => { expect(eventName).toBe('.testEvent'); expect(data).toBe('Hello there'); - }, done); + }, done, true); }); }) diff --git a/tests/ably/ably-connection.test.ts b/tests/ably/ably-connection.test.ts index 1f33a497..48964973 100644 --- a/tests/ably/ably-connection.test.ts +++ b/tests/ably/ably-connection.test.ts @@ -1,23 +1,24 @@ import { setup, tearDown } from './setup/sandbox'; import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; +import safeAssert from './setup/utils'; jest.setTimeout(20000); describe('AblyConnection', () => { - let testApp; - let mockAuthServer; - let echo; + let testApp: any; + let mockAuthServer: MockAuthServer; + let echo: Echo; beforeAll(done => { - setup((err, app) => { - if (err) { - done(err); - return; - } - testApp = app; - mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); - done(); - }) + setup((err, app) => { + if (err) { + done(err); + return; + } + testApp = app; + mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); + done(); + }) }) afterAll((done) => { @@ -44,13 +45,16 @@ describe('AblyConnection', () => { }); test('should be able to connect to server', (done) => { - expect.assertions(2); + expect.assertions(3); echo.connector.ably.connection.on(({ current, previous, reason }) => { if (current == 'connecting') { - expect(previous).toBe('initialized'); + safeAssert(() => expect(previous).toBe('initialized'), done); } else if (current == 'connected') { - expect(previous).toBe('connecting'); + safeAssert(()=> { + expect(previous).toBe('connecting'); + expect(typeof echo.socketId()).toBe('string'); + }, done); setTimeout(() => { // Added timeout to make sure connection stays active echo.connector.ably.connection.off(); done(); @@ -67,17 +71,17 @@ describe('AblyConnection', () => { expect.assertions(4); echo.connector.ably.connection.on(({ current, previous, reason }) => { if (current == 'connecting') { - expect(previous).toBe('initialized'); + safeAssert(() => expect(previous).toBe('initialized'), done); } else if (current == 'connected') { - expect(previous).toBe('connecting'); + safeAssert(() => expect(previous).toBe('connecting'), done); echo.disconnect(); } else if (current == 'closing') { - expect(previous).toBe('connected'); + safeAssert(() => expect(previous).toBe('connected'), done); } else if (current == 'closed') { - expect(previous).toBe('closing'); + safeAssert(() => expect(previous).toBe('closing'), done); echo.connector.ably.connection.off(); done(); } diff --git a/tests/ably/ably-sandbox.test.ts b/tests/ably/ably-sandbox.test.ts index 5200de7c..2f12c3a7 100644 --- a/tests/ably/ably-sandbox.test.ts +++ b/tests/ably/ably-sandbox.test.ts @@ -32,7 +32,7 @@ describe('AblySandbox', () => { done(err); return; } - safeAssert(() => expect(typeof time).toBe('number'), done); + safeAssert(() => expect(typeof time).toBe('number'), done, true); }); }) diff --git a/tests/ably/setup/utils.ts b/tests/ably/setup/utils.ts index 0184d536..ac8dbe61 100644 --- a/tests/ably/setup/utils.ts +++ b/tests/ably/setup/utils.ts @@ -1,8 +1,10 @@ -const safeAssert = ((assertions : Function, done : Function) => { +const safeAssert = ((assertions: Function, done: Function, finalAssertion = false) => { try { assertions(); - done(); - } catch(err) { + if (finalAssertion) { + done(); + } + } catch (err) { done(err); } }); From d51ff5ebb0732b78351d5daa23e2dc693396cc9c Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 23:39:43 +0530 Subject: [PATCH 062/111] Added explicit test for listening to client-whisper and notification --- tests/ably/ably-channel.test.ts | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index 6827e272..af03df29 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -51,7 +51,7 @@ describe('AblyChannel', () => { }); }); - test('Listen for a default broadcaster event', (done) => { + test('Listen to a default broadcaster event', (done) => { const publicChannel = echo.channel('test') as AblyChannel; publicChannel .subscribed(() => { @@ -63,7 +63,7 @@ describe('AblyChannel', () => { }); // https://laravel.com/docs/9.x/broadcasting#broadcast-name - test('Listen for a broadcast as event', (done) => { + test('Listen to a broadcast as event', (done) => { const publicChannel = echo.channel('test') as AblyChannel; publicChannel .subscribed(() => { @@ -74,7 +74,30 @@ describe('AblyChannel', () => { }); }); - test('Listen for all events', (done) => { + test('Listen to a whisper', done => { + const publicChannel = echo.channel('test') as AblyChannel; + publicChannel + .subscribed(() => { + mockAuthServer.broadcast('public:test', 'client-msg', 'Hello there'); + }) + .listenForWhisper('msg', data => { + safeAssert(() => expect(data).toBe('Hello there'), done, true); + }); + }) + + + test('Listen to a notification', done => { + const publicChannel = echo.channel('test') as AblyChannel; + publicChannel + .subscribed(() => { + mockAuthServer.broadcast('public:test', 'Illuminate\\Notifications\\Events\\BroadcastNotificationCreated', 'Hello there'); + }) + .notification(data => { + safeAssert(() => expect(data).toBe('Hello there'), done, true); + }); + }) + + test('Listen to all events', (done) => { const publicChannel = echo.channel('test1') as AblyChannel; publicChannel .subscribed(() => { @@ -87,5 +110,5 @@ describe('AblyChannel', () => { }, done, true); }); }) - + }); \ No newline at end of file From 384e0d419d0d36bdec5440d8b0a0e3bbd3e9d89b Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 23:40:04 +0530 Subject: [PATCH 063/111] Removed commented testcode from sandbox --- tests/ably/setup/sandbox.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/ably/setup/sandbox.ts b/tests/ably/setup/sandbox.ts index 2a6cf6cf..39aa1120 100644 --- a/tests/ably/setup/sandbox.ts +++ b/tests/ably/setup/sandbox.ts @@ -112,20 +112,4 @@ const deleteApp = (app, callback) => { }); } -// creatNewApp((err, testApp) => { -// if (err) { -// console.error('error creating the app'); -// } else { -// console.log('created app') -// console.log(testApp); -// deleteApp(testApp, (err) => { -// if (err) { -// console.error(err) -// } else { -// console.log('deleted test app successfully') -// } -// }) -// } -// }); - export { creatNewApp as setup, deleteApp as tearDown } \ No newline at end of file From 6c079a7a3e364f134cf784e7c12ca050ee1b565f Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jun 2022 23:45:15 +0530 Subject: [PATCH 064/111] Updated errorListener to log whole stateChange with error instead of just error --- src/channel/ably-channel.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index ae1bce41..c6fe70a3 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -62,11 +62,12 @@ export class AblyChannel extends Channel { */ subscribe(): any { this.channel = this.ably.channels.get(this.name); - this.channel.on(({ current, reason }) => { + this.channel.on(stateChange => { + const {current, reason} = stateChange; if (current == 'attached') { this.subscribedListeners.forEach(listener => listener()); } else if (reason) { - this._publishErrors(reason); + this._publishErrors(stateChange); } }); this.channel.attach(); From 28099b03cb556fa235b2e34a19d8403f031b7eec Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 9 Jun 2022 01:41:17 +0530 Subject: [PATCH 065/111] Refactored ably-channel and auth to handle errors properly --- src/channel/ably-channel.ts | 10 ++++++---- src/channel/ably/auth.ts | 13 +++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index c6fe70a3..2e209749 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -63,14 +63,14 @@ export class AblyChannel extends Channel { subscribe(): any { this.channel = this.ably.channels.get(this.name); this.channel.on(stateChange => { - const {current, reason} = stateChange; + const { current, reason } = stateChange; if (current == 'attached') { this.subscribedListeners.forEach(listener => listener()); } else if (reason) { this._publishErrors(stateChange); } }); - this.channel.attach(); + this.channel.attach(this._publishErrors); } /** @@ -85,7 +85,7 @@ export class AblyChannel extends Channel { * Listen for an event on the channel instance. */ listen(event: string, callback: Function): AblyChannel { - this.channel.subscribe(this.eventFormatter.format(event), ({data}) => callback(data)); + this.channel.subscribe(this.eventFormatter.format(event), ({ data }) => callback(data)); return this; } @@ -150,6 +150,8 @@ export class AblyChannel extends Channel { } _publishErrors = (err) => { - this.errorListeners.forEach(listener => listener(err)); + if (err) { + this.errorListeners.forEach(listener => listener(err)); + } } } diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 44ac9041..992478d9 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -1,5 +1,5 @@ import { beforeChannelAttach } from './attach'; -import { toTokenDetails } from './utils'; +import { parseJwt, toTokenDetails } from './utils'; import { SequentialAuthTokenRequestExecuter } from './token-request'; import { AblyChannel } from '../ably-channel'; @@ -41,10 +41,11 @@ export class AblyAuth { } // Use cached token if has channel capability and is not expired - const token = ablyClient.auth.tokenDetails; - if (token) { - const tokenHasChannelCapability = token.capability.includes(`${channelName}"`); - if (tokenHasChannelCapability && token.expires >= Date.now()) { // TODO : Replace with server time + const tokenDetails = ablyClient.auth.tokenDetails; + if (tokenDetails) { + const capability = parseJwt(tokenDetails.token).payload['x-ably-capability']; + const tokenHasChannelCapability = capability.includes(`${channelName}"`); + if (tokenHasChannelCapability && tokenDetails.expires > Date.now()) { // TODO : Replace with server time errorCallback(null); return; } @@ -76,7 +77,7 @@ export class AblyAuth { if (err) { ablyChannel._publishErrors(err); } else { - ablyChannel.channel.attach(); + ablyChannel.channel.attach(ablyChannel._publishErrors); } }); }).catch(err => ablyChannel._publishErrors(err)); From 40c787ae9a386d9ed20e22c7689baaed58f6930b Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 9 Jun 2022 01:42:59 +0530 Subject: [PATCH 066/111] Refactored mock-auth-server for short-lived and banned channels --- tests/ably/setup/mock-auth-server.ts | 32 +++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index 057ec0e3..1173c0dc 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -2,12 +2,17 @@ import { isNullOrUndefinedOrEmpty, parseJwt } from '../../../src/channel/ably/ut import * as Ably from "ably/promises"; import * as jwt from "jsonwebtoken"; +type channels = Array; + export class MockAuthServer { keyName: string; keySecret: string; ablyClient: Ably.Rest; clientId = 'sacOO7@github.com' + shortLived: channels; + banned: channels; + constructor(apiKey: string, environment = "sandbox") { const keys = apiKey.split(':'); this.keyName = keys[0]; @@ -15,16 +20,16 @@ export class MockAuthServer { this.ablyClient = new Ably.Rest({key: apiKey, environment}); } + broadcast = async (channelName: string, eventName : string, message : string) => { + await this.ablyClient.channels.get(channelName).publish(eventName, message); + } + tokenInvalidOrExpired = (serverTime, token) => { const tokenInvalid = false; const { payload } = parseJwt(token); return tokenInvalid || payload.exp * 1000 <= serverTime; }; - broadcast = async (channelName: string, eventName : string, message : string) => { - await this.ablyClient.channels.get(channelName).publish(eventName, message); - } - getSignedToken = async (channelName = null, token = null) => { const header = { "typ": "JWT", @@ -48,13 +53,30 @@ export class MockAuthServer { if (!isNullOrUndefinedOrEmpty(channelName)) { capabilities[channelName] = ["*"] } - const claims = { + let claims = { iat, exp, "x-ably-clientId": this.clientId, "x-ably-capability": JSON.stringify(capabilities) } + claims = this.validateShortLivedOrBannedChannels(channelName, claims); return jwt.sign(claims, this.keySecret, { header }); } + + setAuthExceptions = (shortLived : channels = [], banned : channels = []) => { + this.shortLived = shortLived; + this.banned = banned; + } + + validateShortLivedOrBannedChannels = (channelName : string, claims : any) => { + if (this.shortLived?.includes(channelName)) { + const exp = claims.iat + 3; // if channel is shortlived, token expiry set to 3 seconds + return {...claims, exp }; + } + if (this.banned?.includes(channelName)) { + throw new Error(`User can't be authenticated for ${channelName}`); + } + return claims; + } } From 7513e3cc69ce563d99222a8186a93de59d53808a Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 9 Jun 2022 01:47:47 +0530 Subject: [PATCH 067/111] Added private channel test for listening to subscription error --- tests/ably/ably-private-channel.test.ts | 68 +++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/ably/ably-private-channel.test.ts diff --git a/tests/ably/ably-private-channel.test.ts b/tests/ably/ably-private-channel.test.ts new file mode 100644 index 00000000..d466aac2 --- /dev/null +++ b/tests/ably/ably-private-channel.test.ts @@ -0,0 +1,68 @@ +import { setup, tearDown } from './setup/sandbox'; +import Echo from '../../src/echo'; +import { MockAuthServer } from './setup/mock-auth-server'; +import safeAssert from './setup/utils'; + +jest.setTimeout(20000); +describe('AblyChannel', () => { + let testApp: any; + let mockAuthServer: MockAuthServer; + let echo: Echo; + + beforeAll(done => { + setup((err, app) => { + if (err) { + done(err); + return; + } + testApp = app; + mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); + done(); + }) + }) + + afterAll((done) => { + tearDown(testApp, (err) => { + if (err) { + done(err); + return; + } + done(); + }) + }) + + beforeEach(() => { + echo = new Echo({ + broadcaster: 'ably', + useTls: true, + environment: 'sandbox', + requestTokenFn: mockAuthServer.getSignedToken + }); + }); + + afterEach(() => { + echo.disconnect(); + }); + + test.skip('channel subscription', (done) => { + echo.channel('test').subscribed(() => { + done(); + }); + }); + + test('channel subscription error', done => { + mockAuthServer.setAuthExceptions(['private:shortLivedChannel']) + echo.private('shortLivedChannel') + .error(stateChangeError => { + safeAssert(()=> expect(stateChangeError).toBeTruthy(), done, true) + }); + }); + + test.skip('channel subscription error', done => { + mockAuthServer.setAuthExceptions([], ['private:bannedChannel']) + echo.private('bannedChannel') + .error(stateChangeError => { + safeAssert(()=> expect(stateChangeError).toBeTruthy(), done, true) + }); + }); +}); \ No newline at end of file From d934aa5df59d87ba9eb2dfda3023490b806df756 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 9 Jun 2022 01:48:20 +0530 Subject: [PATCH 068/111] Added tests for stop listening to the event/client-event --- tests/ably/ably-channel.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index af03df29..650cc413 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -85,7 +85,7 @@ describe('AblyChannel', () => { }); }) - + test('Listen to a notification', done => { const publicChannel = echo.channel('test') as AblyChannel; publicChannel @@ -109,6 +109,13 @@ describe('AblyChannel', () => { expect(data).toBe('Hello there'); }, done, true); }); + }); + + test.skip('stop listening to a event', ()=> { + }) + + test.skip('stop listening to a client event', ()=> { + }) }); \ No newline at end of file From 2ba22dd7eef22e08a9f497a2d7ba0c2285a0a123 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 9 Jun 2022 14:23:25 +0530 Subject: [PATCH 069/111] Refactored publish error, added subscribe and error remove callbacks --- src/channel/ably-channel.ts | 36 +++++++++++++++++++++++++++++++++--- src/channel/ably/auth.ts | 6 +++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index 2e209749..41d505c7 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -67,10 +67,10 @@ export class AblyChannel extends Channel { if (current == 'attached') { this.subscribedListeners.forEach(listener => listener()); } else if (reason) { - this._publishErrors(stateChange); + this._publishError(stateChange); } }); - this.channel.attach(this._publishErrors); + this.channel.attach(this._publishError); } /** @@ -149,7 +149,37 @@ export class AblyChannel extends Channel { return this; } - _publishErrors = (err) => { + /** + * Unregisters given error callback from the listeners. + * @param callback + * @returns AblyChannel + */ + _removeSubscribed(callback?: Function): AblyChannel { + if (callback) { + this.subscribedListeners = this.subscribedListeners.filter(s => s != callback); + } else { + this.subscribedListeners = []; + } + + return this; + } + + /** + * Unregisters given error callback from the listeners. + * @param callback + * @returns AblyChannel + */ + _removeError(callback?: Function): AblyChannel { + if (callback) { + this.errorListeners = this.errorListeners.filter(e => e != callback); + } else { + this.errorListeners = []; + } + + return this; + } + + _publishError = (err: any) => { if (err) { this.errorListeners.forEach(listener => listener(err)); } diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 992478d9..ac093548 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -75,12 +75,12 @@ export class AblyAuth { this.authRequestExecuter.request(channelName).then(jwtToken => { // get upgraded token with channel access ablyChannel.ably.auth.authorize(null, { ...this.authOptions, token: toTokenDetails(jwtToken) as any}, (err, _tokenDetails) => { if (err) { - ablyChannel._publishErrors(err); + ablyChannel._publishError(err); } else { - ablyChannel.channel.attach(ablyChannel._publishErrors); + ablyChannel.channel.attach(ablyChannel._publishError); } }); - }).catch(err => ablyChannel._publishErrors(err)); + }).catch(err => ablyChannel._publishError(err)); } } From 97160a920d51ab55ad43a5d8d9945ce2682d064e Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 9 Jun 2022 14:25:38 +0530 Subject: [PATCH 070/111] Added tests related to error callback for channels --- tests/ably/ably-channel.test.ts | 5 +++- tests/ably/ably-connection.test.ts | 31 ++++++++++++++--------- tests/ably/ably-private-channel.test.ts | 33 ++++++++++++++++--------- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index 650cc413..4a176c79 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -41,8 +41,11 @@ describe('AblyChannel', () => { }); }); - afterEach(() => { + afterEach(done => { echo.disconnect(); + echo.connector.ably.connection.once('closed', ()=> { + done(); + }); }); test('channel subscription', (done) => { diff --git a/tests/ably/ably-connection.test.ts b/tests/ably/ably-connection.test.ts index 48964973..0c86f8a1 100644 --- a/tests/ably/ably-connection.test.ts +++ b/tests/ably/ably-connection.test.ts @@ -10,15 +10,15 @@ describe('AblyConnection', () => { let echo: Echo; beforeAll(done => { - setup((err, app) => { - if (err) { - done(err); - return; - } - testApp = app; - mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); - done(); - }) + setup((err, app) => { + if (err) { + done(err); + return; + } + testApp = app; + mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); + done(); + }) }) afterAll((done) => { @@ -40,8 +40,15 @@ describe('AblyConnection', () => { }); }); - afterEach(() => { - echo.disconnect(); + afterEach(done => { + if (echo.connector.ably.connection.state === 'closed') { + done(); + } else { + echo.disconnect(); + echo.connector.ably.connection.once('closed', ()=> { + done(); + }); + } }); test('should be able to connect to server', (done) => { @@ -51,7 +58,7 @@ describe('AblyConnection', () => { safeAssert(() => expect(previous).toBe('initialized'), done); } else if (current == 'connected') { - safeAssert(()=> { + safeAssert(() => { expect(previous).toBe('connecting'); expect(typeof echo.socketId()).toBe('string'); }, done); diff --git a/tests/ably/ably-private-channel.test.ts b/tests/ably/ably-private-channel.test.ts index d466aac2..93c02953 100644 --- a/tests/ably/ably-private-channel.test.ts +++ b/tests/ably/ably-private-channel.test.ts @@ -2,6 +2,7 @@ import { setup, tearDown } from './setup/sandbox'; import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; import safeAssert from './setup/utils'; +import { AblyChannel } from '../../src/channel'; jest.setTimeout(20000); describe('AblyChannel', () => { @@ -40,29 +41,39 @@ describe('AblyChannel', () => { }); }); - afterEach(() => { + afterEach(done => { echo.disconnect(); + echo.connector.ably.connection.once('closed', ()=> { + done(); + }); }); - test.skip('channel subscription', (done) => { - echo.channel('test').subscribed(() => { + test('channel subscription', (done) => { + const channel = echo.private('test') as AblyChannel; + channel.subscribed(() => { + channel._removeSubscribed(); done(); }); }); - test('channel subscription error', done => { - mockAuthServer.setAuthExceptions(['private:shortLivedChannel']) - echo.private('shortLivedChannel') + // TODO - fix recursived attach when connection is closed, reproduce using API_KEY instead of sandbox + test('channel subscription error, token expired', done => { + mockAuthServer.setAuthExceptions(['private:shortLivedChannel']); + const channel = echo.private('shortLivedChannel') as AblyChannel; + channel .error(stateChangeError => { - safeAssert(()=> expect(stateChangeError).toBeTruthy(), done, true) + channel._removeError(); + safeAssert(() => expect(stateChangeError).toBeTruthy(), done, true) }); }); - test.skip('channel subscription error', done => { - mockAuthServer.setAuthExceptions([], ['private:bannedChannel']) - echo.private('bannedChannel') + test('channel subscription error, access denied', done => { + mockAuthServer.setAuthExceptions([], ['private:bannedChannel']); + const channel = echo.private('bannedChannel') as AblyChannel; + channel .error(stateChangeError => { - safeAssert(()=> expect(stateChangeError).toBeTruthy(), done, true) + channel._removeError(); + safeAssert(() => expect(stateChangeError).toBeTruthy(), done, true) }); }); }); \ No newline at end of file From e426701036418ec0ce81107860e64cfb3e4d8428 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 9 Jun 2022 15:52:45 +0530 Subject: [PATCH 071/111] Added tests for stop listening to event, updated test-utils --- src/channel/ably-channel.ts | 4 +- tests/ably/ably-channel.test.ts | 85 +++++++++++++++++++++++++++++---- tests/ably/setup/utils.ts | 8 ++++ 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index 41d505c7..2543eff3 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -84,8 +84,8 @@ export class AblyChannel extends Channel { /** * Listen for an event on the channel instance. */ - listen(event: string, callback: Function): AblyChannel { - this.channel.subscribe(this.eventFormatter.format(event), ({ data }) => callback(data)); + listen(event: string, callback: Function | any): AblyChannel { + this.channel.subscribe(this.eventFormatter.format(event), callback); return this; } diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index 4a176c79..1d4f5c3e 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -2,7 +2,7 @@ import { setup, tearDown } from './setup/sandbox'; import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; import { AblyChannel } from '../../src/channel'; -import safeAssert from './setup/utils'; +import safeAssert, { execute, sleep } from './setup/utils'; jest.setTimeout(20000); describe('AblyChannel', () => { @@ -43,7 +43,7 @@ describe('AblyChannel', () => { afterEach(done => { echo.disconnect(); - echo.connector.ably.connection.once('closed', ()=> { + echo.connector.ably.connection.once('closed', () => { done(); }); }); @@ -60,7 +60,7 @@ describe('AblyChannel', () => { .subscribed(() => { mockAuthServer.broadcast('public:test', 'App\\Events\\testEvent', 'Hello there'); }) - .listen('testEvent', data => { + .listen('testEvent', ({ data }) => { safeAssert(() => expect(data).toBe('Hello there'), done, true); }); }); @@ -72,7 +72,7 @@ describe('AblyChannel', () => { .subscribed(() => { mockAuthServer.broadcast('public:test', 'testEvent', 'Hello there'); }) - .listen('.testEvent', data => { + .listen('.testEvent', ({ data }) => { safeAssert(() => expect(data).toBe('Hello there'), done, true); }); }); @@ -83,7 +83,7 @@ describe('AblyChannel', () => { .subscribed(() => { mockAuthServer.broadcast('public:test', 'client-msg', 'Hello there'); }) - .listenForWhisper('msg', data => { + .listenForWhisper('msg', ({ data }) => { safeAssert(() => expect(data).toBe('Hello there'), done, true); }); }) @@ -95,7 +95,7 @@ describe('AblyChannel', () => { .subscribed(() => { mockAuthServer.broadcast('public:test', 'Illuminate\\Notifications\\Events\\BroadcastNotificationCreated', 'Hello there'); }) - .notification(data => { + .notification(({ data }) => { safeAssert(() => expect(data).toBe('Hello there'), done, true); }); }) @@ -114,11 +114,76 @@ describe('AblyChannel', () => { }); }); - test.skip('stop listening to a event', ()=> { - + test('stop listening to a event', async () => { + const publicChannel = echo.channel('test') as AblyChannel; + const eventHandler1 = jest.fn(); + const eventHandler2 = jest.fn(); + const eventHandler3 = jest.fn(); + + + await new Promise(resolve => { + publicChannel + .subscribed(resolve) + .listen('.testEvent', eventHandler1) + .listen('.testEvent', eventHandler2) + .listen('.testEvent', eventHandler3) + }); + + execute(() => mockAuthServer.broadcast('public:test', 'testEvent', 'Hello there'), 4); + await sleep(3000); + expect(eventHandler1).toBeCalledTimes(4); + expect(eventHandler2).toBeCalledTimes(4); + expect(eventHandler3).toBeCalledTimes(4); + jest.clearAllMocks(); + publicChannel.stopListening('.testEvent', eventHandler1); + + execute(() => mockAuthServer.broadcast('public:test', 'testEvent', 'Hello there'), 3); + await sleep(3000); + expect(eventHandler1).toBeCalledTimes(0); + expect(eventHandler2).toBeCalledTimes(3); + expect(eventHandler3).toBeCalledTimes(3); + jest.clearAllMocks(); + publicChannel.stopListening('.testEvent'); + + execute(() => mockAuthServer.broadcast('public:test', 'testEvent', 'Hello there'), 3); + await sleep(3000); + expect(eventHandler1).toBeCalledTimes(0); + expect(eventHandler2).toBeCalledTimes(0); + expect(eventHandler3).toBeCalledTimes(0); + }) - test.skip('stop listening to a client event', ()=> { - + // internally calls stop listening to a event + test('stop listening to a whisper/client-event', async () => { + const publicChannel = echo.channel('test') as AblyChannel; + const eventHandler1 = jest.fn(); + const eventHandler2 = jest.fn(); + const eventHandler3 = jest.fn(); + + await new Promise(resolve => { + publicChannel + .subscribed(resolve) + .listenForWhisper('msg', eventHandler1) + .listenForWhisper('msg', eventHandler2) + .listenForWhisper('msg2', eventHandler3) + + }); + + execute(() => mockAuthServer.broadcast('public:test', 'client-msg', 'Hello there'), 4); + execute(() => mockAuthServer.broadcast('public:test', 'client-msg2', 'Hello there'), 1); + await sleep(3000); + expect(eventHandler1).toBeCalledTimes(4); + expect(eventHandler2).toBeCalledTimes(4); + expect(eventHandler3).toBeCalledTimes(1); + jest.clearAllMocks(); + publicChannel.stopListeningForWhisper('msg', eventHandler1); + + execute(() => mockAuthServer.broadcast('public:test', 'client-msg', 'Hello there'), 3); + execute(() => mockAuthServer.broadcast('public:test', 'client-msg2', 'Hello there'), 2); + await sleep(3000); + expect(eventHandler1).toBeCalledTimes(0); + expect(eventHandler2).toBeCalledTimes(3); + expect(eventHandler3).toBeCalledTimes(2); + }) }); \ No newline at end of file diff --git a/tests/ably/setup/utils.ts b/tests/ably/setup/utils.ts index ac8dbe61..aa8f8be7 100644 --- a/tests/ably/setup/utils.ts +++ b/tests/ably/setup/utils.ts @@ -9,4 +9,12 @@ const safeAssert = ((assertions: Function, done: Function, finalAssertion = fals } }); +export const sleep = (time : number) => new Promise(res => setTimeout(res, time)); + +export const execute = (fn: Function, times : number) => { + while(times--) { + fn(); + } +} + export default safeAssert; \ No newline at end of file From a91e19c76a90bb83097d9dbef4052ed01c3507d7 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 9 Jun 2022 17:49:46 +0530 Subject: [PATCH 072/111] Added whisper send and receive test to the private-channel --- tests/ably/ably-private-channel.test.ts | 33 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tests/ably/ably-private-channel.test.ts b/tests/ably/ably-private-channel.test.ts index 93c02953..d9f5ce4a 100644 --- a/tests/ably/ably-private-channel.test.ts +++ b/tests/ably/ably-private-channel.test.ts @@ -2,10 +2,10 @@ import { setup, tearDown } from './setup/sandbox'; import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; import safeAssert from './setup/utils'; -import { AblyChannel } from '../../src/channel'; +import { AblyChannel, AblyPrivateChannel } from '../../src/channel'; jest.setTimeout(20000); -describe('AblyChannel', () => { +describe('AblyPrivateChannel', () => { let testApp: any; let mockAuthServer: MockAuthServer; let echo: Echo; @@ -49,30 +49,41 @@ describe('AblyChannel', () => { }); test('channel subscription', (done) => { - const channel = echo.private('test') as AblyChannel; - channel.subscribed(() => { - channel._removeSubscribed(); + const privateChannel = echo.private('test') as AblyChannel; + privateChannel.subscribed(() => { + privateChannel._removeSubscribed(); done(); }); }); + test('Whisper and listen to it', done => { + const privateChannel = echo.private('test') as AblyPrivateChannel; + privateChannel + .subscribed(() => { + privateChannel.whisper('msg', 'Hello there jonny!'); + }) + .listenForWhisper('msg', ({ data }) => { + safeAssert(() => expect(data).toBe('Hello there jonny!'), done, true); + }); + }) + // TODO - fix recursived attach when connection is closed, reproduce using API_KEY instead of sandbox test('channel subscription error, token expired', done => { mockAuthServer.setAuthExceptions(['private:shortLivedChannel']); - const channel = echo.private('shortLivedChannel') as AblyChannel; - channel + const privateChannel = echo.private('shortLivedChannel') as AblyChannel; + privateChannel .error(stateChangeError => { - channel._removeError(); + privateChannel._removeError(); safeAssert(() => expect(stateChangeError).toBeTruthy(), done, true) }); }); test('channel subscription error, access denied', done => { mockAuthServer.setAuthExceptions([], ['private:bannedChannel']); - const channel = echo.private('bannedChannel') as AblyChannel; - channel + const privateChannel = echo.private('bannedChannel') as AblyChannel; + privateChannel .error(stateChangeError => { - channel._removeError(); + privateChannel._removeError(); safeAssert(() => expect(stateChangeError).toBeTruthy(), done, true) }); }); From 65f892e7434061e27ea2d86e9726cf0850a2b999 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 9 Jun 2022 18:05:59 +0530 Subject: [PATCH 073/111] Added basic presence-channel-test, updated unsubscribe method to remove listeners --- src/channel/ably-channel.ts | 1 + src/channel/ably-presence-channel.ts | 1 + tests/ably/ably-presence-channel.test.ts | 57 ++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 tests/ably/ably-presence-channel.test.ts diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index 2543eff3..bf2a337e 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -78,6 +78,7 @@ export class AblyChannel extends Channel { */ unsubscribe(): void { this.channel.unsubscribe(); + this.channel.off(); this.channel.detach(); } diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts index fb50dbd8..a7d8f8dd 100644 --- a/src/channel/ably-presence-channel.ts +++ b/src/channel/ably-presence-channel.ts @@ -57,6 +57,7 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel */ enter(data: any, callback: Function): AblyPresenceChannel { this.channel.presence.enter(data, callback as any); + return this; } diff --git a/tests/ably/ably-presence-channel.test.ts b/tests/ably/ably-presence-channel.test.ts new file mode 100644 index 00000000..9f1c89eb --- /dev/null +++ b/tests/ably/ably-presence-channel.test.ts @@ -0,0 +1,57 @@ +import { setup, tearDown } from './setup/sandbox'; +import Echo from '../../src/echo'; +import { MockAuthServer } from './setup/mock-auth-server'; +import { AblyPresenceChannel } from '../../src/channel'; + +jest.setTimeout(20000); +describe('AblyPresenceChannel', () => { + let testApp: any; + let mockAuthServer: MockAuthServer; + let echo: Echo; + + beforeAll(done => { + setup((err, app) => { + if (err) { + done(err); + return; + } + testApp = app; + mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); + done(); + }) + }) + + afterAll((done) => { + tearDown(testApp, (err) => { + if (err) { + done(err); + return; + } + done(); + }) + }) + + beforeEach(() => { + echo = new Echo({ + broadcaster: 'ably', + useTls: true, + environment: 'sandbox', + requestTokenFn: mockAuthServer.getSignedToken + }); + }); + + afterEach(done => { + echo.disconnect(); + echo.connector.ably.connection.once('closed', ()=> { + done(); + }); + }); + + test('channel subscription', (done) => { + const presenceChannel = echo.join('test') as AblyPresenceChannel; + presenceChannel.subscribed(() => { + presenceChannel._removeSubscribed(); + done(); + }); + }); +}); \ No newline at end of file From 3543b24e7a7bad8731125dda98aed95b72eaeac4 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 9 Jun 2022 23:39:58 +0530 Subject: [PATCH 074/111] Refactored publishError name to _alertErrorListeners --- src/channel/ably-channel.ts | 8 ++++---- src/channel/ably/auth.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index bf2a337e..1937a3bf 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -67,14 +67,14 @@ export class AblyChannel extends Channel { if (current == 'attached') { this.subscribedListeners.forEach(listener => listener()); } else if (reason) { - this._publishError(stateChange); + this._alertErrorListeners(stateChange); } }); - this.channel.attach(this._publishError); + this.channel.attach(this._alertErrorListeners); } /** - * Unsubscribe from an Ably channel. + * Unsubscribe from an Ably channel, unregister all callbacks and finally detach the channel */ unsubscribe(): void { this.channel.unsubscribe(); @@ -180,7 +180,7 @@ export class AblyChannel extends Channel { return this; } - _publishError = (err: any) => { + _alertErrorListeners = (err: any) => { if (err) { this.errorListeners.forEach(listener => listener(err)); } diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index ac093548..92197509 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -75,12 +75,12 @@ export class AblyAuth { this.authRequestExecuter.request(channelName).then(jwtToken => { // get upgraded token with channel access ablyChannel.ably.auth.authorize(null, { ...this.authOptions, token: toTokenDetails(jwtToken) as any}, (err, _tokenDetails) => { if (err) { - ablyChannel._publishError(err); + ablyChannel._alertErrorListeners(err); } else { - ablyChannel.channel.attach(ablyChannel._publishError); + ablyChannel.channel.attach(ablyChannel._alertErrorListeners); } }); - }).catch(err => ablyChannel._publishError(err)); + }).catch(err => ablyChannel._alertErrorListeners(err)); } } From 0af417065d0324fa6de2ece018282281ab6b4c4f Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 9 Jun 2022 23:41:33 +0530 Subject: [PATCH 075/111] Overriden unsubscribe inside presence channel, removed presence listeners --- src/channel/ably-presence-channel.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts index a7d8f8dd..48efca9c 100644 --- a/src/channel/ably-presence-channel.ts +++ b/src/channel/ably-presence-channel.ts @@ -11,6 +11,12 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel super(ably, name, options); this.channel.on("failed", auth.onChannelFailed(this)); } + + unsubscribe(): void { + this.channel.presence.unsubscribe(); + super.unsubscribe(); + } + /** * Register a callback to be called anytime the member list changes. */ From ca5e161a9dfdaaf3d21dd9c77a3b2d07aad501b3 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 10 Jun 2022 01:40:22 +0530 Subject: [PATCH 076/111] refactored unregister subscribe and error callbacks --- src/channel/ably-channel.ts | 6 ++++-- tests/ably/ably-presence-channel.test.ts | 14 +++++++++++++- tests/ably/ably-private-channel.test.ts | 6 +++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index 1937a3bf..dffdf9c9 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -78,6 +78,8 @@ export class AblyChannel extends Channel { */ unsubscribe(): void { this.channel.unsubscribe(); + this.unregisterError(); + this.unregisterSubscribed(); this.channel.off(); this.channel.detach(); } @@ -155,7 +157,7 @@ export class AblyChannel extends Channel { * @param callback * @returns AblyChannel */ - _removeSubscribed(callback?: Function): AblyChannel { + unregisterSubscribed(callback?: Function): AblyChannel { if (callback) { this.subscribedListeners = this.subscribedListeners.filter(s => s != callback); } else { @@ -170,7 +172,7 @@ export class AblyChannel extends Channel { * @param callback * @returns AblyChannel */ - _removeError(callback?: Function): AblyChannel { + unregisterError(callback?: Function): AblyChannel { if (callback) { this.errorListeners = this.errorListeners.filter(e => e != callback); } else { diff --git a/tests/ably/ably-presence-channel.test.ts b/tests/ably/ably-presence-channel.test.ts index 9f1c89eb..69a210b7 100644 --- a/tests/ably/ably-presence-channel.test.ts +++ b/tests/ably/ably-presence-channel.test.ts @@ -50,8 +50,20 @@ describe('AblyPresenceChannel', () => { test('channel subscription', (done) => { const presenceChannel = echo.join('test') as AblyPresenceChannel; presenceChannel.subscribed(() => { - presenceChannel._removeSubscribed(); + presenceChannel.unregisterSubscribed(); done(); }); }); + + test.skip('channel member list change', done => { + + }); + + test.skip('member joined', done=> { + + }) + + test.skip('member left', done => { + + }) }); \ No newline at end of file diff --git a/tests/ably/ably-private-channel.test.ts b/tests/ably/ably-private-channel.test.ts index d9f5ce4a..c508b52b 100644 --- a/tests/ably/ably-private-channel.test.ts +++ b/tests/ably/ably-private-channel.test.ts @@ -51,7 +51,7 @@ describe('AblyPrivateChannel', () => { test('channel subscription', (done) => { const privateChannel = echo.private('test') as AblyChannel; privateChannel.subscribed(() => { - privateChannel._removeSubscribed(); + privateChannel.unregisterSubscribed(); done(); }); }); @@ -73,7 +73,7 @@ describe('AblyPrivateChannel', () => { const privateChannel = echo.private('shortLivedChannel') as AblyChannel; privateChannel .error(stateChangeError => { - privateChannel._removeError(); + privateChannel.unregisterError(); safeAssert(() => expect(stateChangeError).toBeTruthy(), done, true) }); }); @@ -83,7 +83,7 @@ describe('AblyPrivateChannel', () => { const privateChannel = echo.private('bannedChannel') as AblyChannel; privateChannel .error(stateChangeError => { - privateChannel._removeError(); + privateChannel.unregisterError(); safeAssert(() => expect(stateChangeError).toBeTruthy(), done, true) }); }); From 7b54c5e6772e72880e76c6e2b443c95052a075fe Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 10 Jun 2022 01:42:38 +0530 Subject: [PATCH 077/111] Added attached listener to immedicately get presence when joined the channel --- src/channel/ably-presence-channel.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts index 48efca9c..c6e5b693 100644 --- a/src/channel/ably-presence-channel.ts +++ b/src/channel/ably-presence-channel.ts @@ -7,12 +7,29 @@ import { PresenceChannel } from './presence-channel'; */ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel { + presenceData : any; + + hereListeners: Function[]; + constructor(ably: any, name: string, options: any, auth: AblyAuth) { super(ably, name, options); + this.hereListeners = []; this.channel.on("failed", auth.onChannelFailed(this)); + this.channel.on("attached", () => { + this.enter(this.presenceData, (err : any) => { + if (err) { + this._alertErrorListeners(err); + } else { + this.channel.presence.get((err, members) => { + this.hereListeners.forEach(listener => listener(members, err)); + }); + } + }); + }); } unsubscribe(): void { + this.channel.presence.leave(); this.channel.presence.unsubscribe(); super.unsubscribe(); } @@ -21,15 +38,7 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel * Register a callback to be called anytime the member list changes. */ here(callback: Function): AblyPresenceChannel { - this.channel.presence.get((err, members) => { - if (err) { - callback(null, err); - return; - } - - callback(members, null); - }) - + this.hereListeners.push(callback); return this; } From 383c7206cb2950a75dc3529fa10232c169090f92 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 10 Jun 2022 12:23:18 +0530 Subject: [PATCH 078/111] Updated mock-auth-server to return extra-info for presence channel --- tests/ably/setup/mock-auth-server.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index 1173c0dc..c922a3f5 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -9,6 +9,7 @@ export class MockAuthServer { keySecret: string; ablyClient: Ably.Rest; clientId = 'sacOO7@github.com' + userInfo = {id : 'sacOO7@github.com', name: 'sacOO7'} shortLived: channels; banned: channels; @@ -30,7 +31,7 @@ export class MockAuthServer { return tokenInvalid || payload.exp * 1000 <= serverTime; }; - getSignedToken = async (channelName = null, token = null) => { + getSignedToken = async (channelName : any = null, token : any = null) => { const header = { "typ": "JWT", "alg": "HS256", @@ -60,9 +61,15 @@ export class MockAuthServer { "x-ably-capability": JSON.stringify(capabilities) } claims = this.validateShortLivedOrBannedChannels(channelName, claims); - return jwt.sign(claims, this.keySecret, { header }); + const response = { token : jwt.sign(claims, this.keySecret, { header })}; + if (this.isPresenceChannel(channelName)) { + return {...response, info: this.userInfo} + } + return response; } + isPresenceChannel = channelName => channelName.startsWith("presence:"); + setAuthExceptions = (shortLived : channels = [], banned : channels = []) => { this.shortLived = shortLived; this.banned = banned; From 52c2c3e04e26043cdd7fa813f4b917558486c7ec Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 10 Jun 2022 12:30:20 +0530 Subject: [PATCH 079/111] Updated token-request/auth to handle new response, refactored channels for auto-subscribe --- src/channel/ably-channel.ts | 8 +++--- src/channel/ably-presence-channel.ts | 28 +++++++++++++------ src/channel/ably-private-channel.ts | 3 ++- src/channel/ably/auth.ts | 40 ++++++++++++++++++---------- src/channel/ably/token-request.ts | 8 +++--- src/connector/ably-connector.ts | 2 +- tests/ably/setup/mock-auth-server.ts | 2 +- 7 files changed, 59 insertions(+), 32 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index dffdf9c9..fdb097d2 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -44,7 +44,7 @@ export class AblyChannel extends Channel { /** * Create a new class instance. */ - constructor(ably: any, name: string, options: any) { + constructor(ably: any, name: string, options: any, autoSubscribe = true) { super(); this.name = name; @@ -53,15 +53,17 @@ export class AblyChannel extends Channel { this.eventFormatter = new EventFormatter(this.options.namespace); this.subscribedListeners = []; this.errorListeners = []; + this.channel = ably.channels.get(name); - this.subscribe(); + if (autoSubscribe) { + this.subscribe(); + } } /** * Subscribe to an Ably channel. */ subscribe(): any { - this.channel = this.ably.channels.get(this.name); this.channel.on(stateChange => { const { current, reason } = stateChange; if (current == 'attached') { diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts index c6e5b693..e03e13d5 100644 --- a/src/channel/ably-presence-channel.ts +++ b/src/channel/ably-presence-channel.ts @@ -7,16 +7,16 @@ import { PresenceChannel } from './presence-channel'; */ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel { - presenceData : any; + presenceData: any; hereListeners: Function[]; constructor(ably: any, name: string, options: any, auth: AblyAuth) { - super(ably, name, options); + super(ably, name, options, false); this.hereListeners = []; this.channel.on("failed", auth.onChannelFailed(this)); this.channel.on("attached", () => { - this.enter(this.presenceData, (err : any) => { + this.enter(this.presenceData, (err: any) => { if (err) { this._alertErrorListeners(err); } else { @@ -26,19 +26,21 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel } }); }); + this.subscribe(); } unsubscribe(): void { - this.channel.presence.leave(); + this.leave(this.presenceData); + this.unregisterHere(); this.channel.presence.unsubscribe(); super.unsubscribe(); } - + /** * Register a callback to be called anytime the member list changes. */ here(callback: Function): AblyPresenceChannel { - this.hereListeners.push(callback); + this.hereListeners.push(callback); return this; } @@ -72,7 +74,7 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel */ enter(data: any, callback: Function): AblyPresenceChannel { this.channel.presence.enter(data, callback as any); - + return this; } @@ -82,7 +84,7 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel * @param callback - success/error callback (err) => {} * @returns AblyPresenceChannel */ - leave(data: any, callback: Function): AblyPresenceChannel { + leave(data: any, callback?: Function): AblyPresenceChannel { this.channel.presence.leave(data, callback as any); return this; @@ -100,6 +102,16 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel return this; } + unregisterHere(callback?: Function) { + if (callback) { + this.errorListeners = this.hereListeners.filter(e => e != callback); + } else { + this.errorListeners = []; + } + + return this; + } + /** * Trigger client event on the channel. */ diff --git a/src/channel/ably-private-channel.ts b/src/channel/ably-private-channel.ts index 8aa3d9a3..6f374649 100644 --- a/src/channel/ably-private-channel.ts +++ b/src/channel/ably-private-channel.ts @@ -4,8 +4,9 @@ import { AblyAuth } from './ably/auth'; export class AblyPrivateChannel extends AblyChannel { constructor(ably: any, name: string, options: any, auth: AblyAuth) { - super(ably, name, options); + super(ably, name, options, false); this.channel.on("failed", auth.onChannelFailed(this)); + this.subscribe(); } /** * Trigger client event on the channel. diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 92197509..61a61da3 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -2,6 +2,8 @@ import { beforeChannelAttach } from './attach'; import { parseJwt, toTokenDetails } from './utils'; import { SequentialAuthTokenRequestExecuter } from './token-request'; import { AblyChannel } from '../ably-channel'; +import { AblyConnector } from '../../connector/ably-connector'; +import { AblyPresenceChannel } from '../ably-presence-channel'; export class AblyAuth { @@ -13,8 +15,8 @@ export class AblyAuth { useTokenAuth: true, authCallback: async (_, callback) => { // get token from tokenParams try { - const jwtToken = await this.authRequestExecuter.request(null); // Replace this by network request to PHP server - const tokenDetails = toTokenDetails(jwtToken); + const { token } = await this.authRequestExecuter.request(null); // Replace this by network request to PHP server + const tokenDetails = toTokenDetails(token); callback(null, tokenDetails); } catch (error) { callback(error, null); @@ -32,7 +34,8 @@ export class AblyAuth { this.authRequestExecuter = new SequentialAuthTokenRequestExecuter(token, requestTokenFn ?? this.requestToken); } - enableAuthorizeBeforeChannelAttach = (ablyClient) => { + enableAuthorizeBeforeChannelAttach = (ablyConnector: AblyConnector) => { + const ablyClient: any = ablyConnector.ably; beforeChannelAttach(ablyClient, (realtimeChannel, errorCallback) => { const channelName = realtimeChannel.name; if (channelName.startsWith("public:")) { @@ -52,8 +55,10 @@ export class AblyAuth { } // explicitly request token for given channel name - this.authRequestExecuter.request(channelName).then(jwtToken => { // get upgraded token with channel access - ablyClient.auth.authorize(null, { ...this.authOptions, token: toTokenDetails(jwtToken) }, (err, tokenDetails) => { + this.authRequestExecuter.request(channelName).then(({ token: jwtToken, info }) => { // get upgraded token with channel access + const echoChannel = ablyConnector.channels[channelName]; + this.setPresenceInfo(echoChannel, info); + ablyClient.auth.authorize(null, { ...this.authOptions, token: toTokenDetails(jwtToken) }, (err, _tokenDetails) => { if (err) { errorCallback(err); } else { @@ -64,23 +69,30 @@ export class AblyAuth { }); } - onChannelFailed = (ablyChannel: AblyChannel) => stateChange => { + onChannelFailed = (echoAblyChannel: AblyChannel) => stateChange => { if (stateChange.reason?.code == 40160) { // channel capability rejected https://help.ably.io/error/40160 - this.handleChannelAuthError(ablyChannel); + this.handleChannelAuthError(echoAblyChannel); } } - handleChannelAuthError = (ablyChannel: AblyChannel) => { - const channelName = ablyChannel.name; - this.authRequestExecuter.request(channelName).then(jwtToken => { // get upgraded token with channel access - ablyChannel.ably.auth.authorize(null, { ...this.authOptions, token: toTokenDetails(jwtToken) as any}, (err, _tokenDetails) => { + handleChannelAuthError = (echoAblyChannel: AblyChannel) => { + const channelName = echoAblyChannel.name; + this.authRequestExecuter.request(channelName).then(({ token: jwtToken, info }) => { // get upgraded token with channel access + this.setPresenceInfo(echoAblyChannel, info); + echoAblyChannel.ably.auth.authorize(null, { ...this.authOptions, token: toTokenDetails(jwtToken) as any }, (err, _tokenDetails) => { if (err) { - ablyChannel._alertErrorListeners(err); + echoAblyChannel._alertErrorListeners(err); } else { - ablyChannel.channel.attach(ablyChannel._alertErrorListeners); + echoAblyChannel.channel.attach(echoAblyChannel._alertErrorListeners); } }); - }).catch(err => ablyChannel._alertErrorListeners(err)); + }).catch(err => echoAblyChannel._alertErrorListeners(err)); + } + + setPresenceInfo = (echoAblyChannel: AblyChannel, info: any) => { + if (echoAblyChannel instanceof AblyPresenceChannel) { + echoAblyChannel.presenceData = info; + } } } diff --git a/src/channel/ably/token-request.ts b/src/channel/ably/token-request.ts index 9a5d86ed..523fe1ad 100644 --- a/src/channel/ably/token-request.ts +++ b/src/channel/ably/token-request.ts @@ -9,19 +9,19 @@ export class SequentialAuthTokenRequestExecuter { this.queue = new TaskQueue(); } - execute = (tokenRequestFn): Promise => new Promise(async (resolve, reject) => { + execute = (tokenRequestFn : Function): Promise<{token : string, info: any}> => new Promise(async (resolve, reject) => { await this.queue.run(async () => { try { - const token = await tokenRequestFn(this.cachedToken); + const {token, info} = await tokenRequestFn(this.cachedToken); this.cachedToken = token; - resolve(token); + resolve({token, info}); } catch (err) { reject(err); } }) }) - request = (channelName):Promise => this.execute(token => this.requestTokenFn(channelName, token)); + request = (channelName: string):Promise<{token : string, info: any}> => this.execute(token => this.requestTokenFn(channelName, token)); } class TaskQueue { diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index dc351351..52a0b0ae 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -36,7 +36,7 @@ export class AblyConnector extends Connector { } else { this.ablyAuth = new AblyAuth(this.options); this.ably = new Ably.Realtime({ ...this.ablyAuth.authOptions, ...this.options }); - this.ablyAuth.enableAuthorizeBeforeChannelAttach(this.ably); + this.ablyAuth.enableAuthorizeBeforeChannelAttach(this); } } diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index c922a3f5..6c457307 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -62,7 +62,7 @@ export class MockAuthServer { } claims = this.validateShortLivedOrBannedChannels(channelName, claims); const response = { token : jwt.sign(claims, this.keySecret, { header })}; - if (this.isPresenceChannel(channelName)) { + if (channelName && this.isPresenceChannel(channelName)) { return {...response, info: this.userInfo} } return response; From ec5626254506b6b09ed6e3d539aef5ca1d807418 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Sun, 12 Jun 2022 02:19:15 +0530 Subject: [PATCH 080/111] Refactored hereListeners, enter channel on attach --- src/channel/ably-presence-channel.ts | 34 +++++----------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts index e03e13d5..5deb8214 100644 --- a/src/channel/ably-presence-channel.ts +++ b/src/channel/ably-presence-channel.ts @@ -9,29 +9,15 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel presenceData: any; - hereListeners: Function[]; - constructor(ably: any, name: string, options: any, auth: AblyAuth) { super(ably, name, options, false); - this.hereListeners = []; this.channel.on("failed", auth.onChannelFailed(this)); - this.channel.on("attached", () => { - this.enter(this.presenceData, (err: any) => { - if (err) { - this._alertErrorListeners(err); - } else { - this.channel.presence.get((err, members) => { - this.hereListeners.forEach(listener => listener(members, err)); - }); - } - }); - }); + this.channel.on("attached", () => this.enter(this.presenceData, this._alertErrorListeners)); this.subscribe(); } unsubscribe(): void { - this.leave(this.presenceData); - this.unregisterHere(); + this.leave(this.presenceData, this._alertErrorListeners); this.channel.presence.unsubscribe(); super.unsubscribe(); } @@ -40,7 +26,9 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel * Register a callback to be called anytime the member list changes. */ here(callback: Function): AblyPresenceChannel { - this.hereListeners.push(callback); + this.channel.presence.subscribe(['enter', 'update', 'leave'], _ => + this.channel.presence.get((err, members) => callback(members, err)// returns local sync copy of updated members + )); return this; } @@ -48,7 +36,7 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel * Listen for someone joining the channel. */ joining(callback: Function): AblyPresenceChannel { - this.channel.presence.subscribe('enter', (member) => { + this.channel.presence.subscribe(['enter', 'update'], (member) => { callback(member); }); @@ -102,16 +90,6 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel return this; } - unregisterHere(callback?: Function) { - if (callback) { - this.errorListeners = this.hereListeners.filter(e => e != callback); - } else { - this.errorListeners = []; - } - - return this; - } - /** * Trigger client event on the channel. */ From 286428e5434397f25b1c95d23e8652d78ef571e7 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Sun, 12 Jun 2022 02:37:28 +0530 Subject: [PATCH 081/111] Added test for channel members on update and channel join --- tests/ably/ably-presence-channel.test.ts | 33 +++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/tests/ably/ably-presence-channel.test.ts b/tests/ably/ably-presence-channel.test.ts index 69a210b7..6348c604 100644 --- a/tests/ably/ably-presence-channel.test.ts +++ b/tests/ably/ably-presence-channel.test.ts @@ -2,6 +2,7 @@ import { setup, tearDown } from './setup/sandbox'; import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; import { AblyPresenceChannel } from '../../src/channel'; +import safeAssert from './setup/utils'; jest.setTimeout(20000); describe('AblyPresenceChannel', () => { @@ -42,7 +43,7 @@ describe('AblyPresenceChannel', () => { afterEach(done => { echo.disconnect(); - echo.connector.ably.connection.once('closed', ()=> { + echo.connector.ably.connection.once('closed', () => { done(); }); }); @@ -55,15 +56,35 @@ describe('AblyPresenceChannel', () => { }); }); - test.skip('channel member list change', done => { - + test('channel member list change', done => { + const presenceChannel = echo.join('test') as AblyPresenceChannel; + presenceChannel.here((members, err) => { + safeAssert(() => { + expect(members).toHaveLength(1); + expect(members[0].clientId).toBe('sacOO7@github.com'); + expect(members[0].data).toStrictEqual({ id: 'sacOO7@github.com', name: 'sacOO7' }); + }, done, true); + }); }); - test.skip('member joined', done=> { - + test('member joined', done => { + const presenceChannel = echo.join('test') as AblyPresenceChannel; + presenceChannel.joining(member => { + safeAssert(() => { + expect(member.clientId).toBe('sacOO7@github.com'); + expect(member.data).toStrictEqual({ id: 'sacOO7@github.com', name: 'sacOO7' }); + }, done, true); + }) }) test.skip('member left', done => { - + const presenceChannel = echo.join('test') as AblyPresenceChannel; + presenceChannel.leaving((member) => { + safeAssert(() => { + expect(member.clientId).toBe('sacOO7@github.com'); + expect(member.data).toStrictEqual({ name: 'sacOO7 leaving the channel' }); + }, done, true); + }) + presenceChannel.leave({name: 'sacOO7 leaving the channel'}); }) }); \ No newline at end of file From d078790a0e4ba866bac8ea7f14be479c91aacce9 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 21 Jun 2022 22:16:50 +0530 Subject: [PATCH 082/111] Added missing presence test, updated auth requestToken method to get actual token via HTTP request --- src/channel/ably/auth.ts | 40 ++++++- src/channel/ably/utils.ts | 133 ++++++++++++++++++++++- tests/ably/ably-presence-channel.test.ts | 8 +- tests/ably/setup/sandbox.ts | 26 +---- 4 files changed, 174 insertions(+), 33 deletions(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 61a61da3..16135c4a 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -1,5 +1,5 @@ import { beforeChannelAttach } from './attach'; -import { parseJwt, toTokenDetails } from './utils'; +import { httpReqFunction, parseJwt, toTokenDetails } from './utils'; import { SequentialAuthTokenRequestExecuter } from './token-request'; import { AblyChannel } from '../ably-channel'; import { AblyConnector } from '../../connector/ably-connector'; @@ -9,6 +9,11 @@ export class AblyAuth { // TODO - Can be updated with request throttle, to send multiple request payload under single request authRequestExecuter: SequentialAuthTokenRequestExecuter; + authEndpoint = '/broadcasting/auth'; + authHost = typeof window != 'undefined' && window?.location?.host; + authPort = typeof window != 'undefined' && window?.location?.port; + + httpReq = httpReqFunction(); authOptions = { queryTime: true, @@ -24,13 +29,40 @@ export class AblyAuth { } } - // TODO - Add default HTTP request fn - requestToken = (channelName: string, existingToken: string) => { + requestToken = async (channelName: string, existingToken: string) => { + var postData = JSON.stringify({ channel_name: channelName, token: existingToken }); + var postOptions = { + host: this.authHost, + port: this.authPort, + path: this.authEndpoint, + method: 'POST', + scheme: 'https', + headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'Content-Length': postData.length }, + body: postData, + }; + return new Promise((resolve, reject) => { + this.httpReq(postOptions, function (err: any, res: any) { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + }); } constructor(options) { - const { token, requestTokenFn } = options; + const { token, requestTokenFn, authEndpoint, authHost, authPort } = options; + if (authEndpoint) { + this.authEndpoint = authEndpoint; + }; + if (authHost) { + this.authHost = authHost; + } + if (authPort) { + this.authPort = authPort; + } this.authRequestExecuter = new SequentialAuthTokenRequestExecuter(token, requestTokenFn ?? this.requestToken); } diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 63608c39..5ac25228 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -1,3 +1,5 @@ +import * as ably from 'ably'; + export const isNullOrUndefined = (obj) => obj == null || obj === undefined; export const isEmptyString = (stringToCheck, ignoreSpaces = true) => (ignoreSpaces ? stringToCheck.trim() : stringToCheck) === ''; export const isNullOrUndefinedOrEmpty = (obj) => obj == null || obj === undefined || isEmptyString(obj); @@ -29,6 +31,7 @@ export const toTokenDetails = (jwtToken: string) => { } const isBrowser = typeof window === 'object'; +const isNativescript = typeof global === 'object' && global.isNativescript; const toBase64 = (text: string) => { if (isBrowser) { @@ -42,4 +45,132 @@ const toText = (base64: string) => { return atob(base64); } return Buffer.from(base64, 'base64').toString('binary'); -}; \ No newline at end of file +}; + +function createXHR() { + var result = new XMLHttpRequest(); + if ('withCredentials' in result) return result;// @ts-ignore + if (typeof XDomainRequest !== 'undefined') { // @ts-ignore + var xdr = new XDomainRequest(); /* Use IE-specific "CORS" code with XDR */ + xdr.isXDR = true; + return xdr; + } + return null; +} + +function schemeMatchesCurrent(scheme) { + return scheme === window.location.protocol.slice(0, -1); +} + +export function httpReqFunction() { + if (isNativescript) { + return function (options, callback) { + var http = require('http'); + var uri = options.scheme + '://' + options.host + ':' + options.port + options.path; + + http + .request({ + url: uri, + method: options.method || 'GET', + timeout: 10000, + headers: options.headers, + content: options.body, + }) + .then(function (results) { + callback(null, results.content.toString()); + }) + ['catch'](function (err) { + callback(err); + }); + }; + } else if (isBrowser) { + return function (options, callback) { + var xhr = createXHR(); + var uri; + + uri = options.scheme + '://' + options.host + ':' + options.port + options.path; + + if (xhr.isXDR && !schemeMatchesCurrent(options.scheme)) { + /* Can't use XDR for cross-scheme. For some requests could just force + * the same scheme and be done with it, but not for authenticated + * requests to ably, can't use basic auth for non-tls endpoints. + * Luckily ably can handle jsonp, so just use the ably Http method, + * which will use the jsonp transport. Can't just do this all the time + * as the local express webserver serves files statically, so can't do + * jsonp. */ + if (options.method === 'DELETE') { + /* Ignore DELETEs -- can't be done with jsonp at the moment, and + * simulation apps self-delete after a while */ + callback(); + } else {// @ts-ignore + new ably.Rest.Platform.Http().doUri( + options.method, + null, + uri, + options.headers, + options.body, + options.paramsIfNoHeaders || {}, + callback + ); + } + return; + } + + xhr.open(options.method, uri); + if (options.headers && !xhr.isXDR) { + for (var h in options.headers) if (h !== 'Content-Length') xhr.setRequestHeader(h, options.headers[h]); + } + xhr.onerror = function (err) { + callback(err); + }; + if ('onreadystatechange' in xhr) { + /* XHR */ + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + if (xhr.status >= 300) { + callback('HTTP request failed ' + xhr.status); + return; + } + callback(null, xhr.responseText); + } + }; + } else { + /* XDR */ + xhr.onload = function () { + if (xhr.status >= 300) { + callback('HTTP request failed ' + xhr.status); + return; + } + callback(null, xhr.responseText); + }; + } + xhr.send(options.body); + }; + } else { + var http = require('http'), + https = require('https'); + + return function (options, callback) { + var body = options.body; + delete options.body; + var response = ''; + var request = (options.scheme == 'http' ? http : https).request(options, function (res) { + res.setEncoding('utf8'); + res.on('data', function (chunk) { + response += chunk; + }); + res.on('end', function () { + if (res.statusCode >= 300) { + callback('Invalid HTTP request: ' + response + '; statusCode = ' + res.statusCode); + } else { + callback(null, response); + } + }); + }); + request.on('error', function (err) { + callback(err); + }); + request.end(body); + }; + } +} diff --git a/tests/ably/ably-presence-channel.test.ts b/tests/ably/ably-presence-channel.test.ts index 6348c604..486bde19 100644 --- a/tests/ably/ably-presence-channel.test.ts +++ b/tests/ably/ably-presence-channel.test.ts @@ -77,14 +77,14 @@ describe('AblyPresenceChannel', () => { }) }) - test.skip('member left', done => { + test('member left', done => { const presenceChannel = echo.join('test') as AblyPresenceChannel; - presenceChannel.leaving((member) => { + presenceChannel.leaving(member => { safeAssert(() => { expect(member.clientId).toBe('sacOO7@github.com'); expect(member.data).toStrictEqual({ name: 'sacOO7 leaving the channel' }); }, done, true); - }) - presenceChannel.leave({name: 'sacOO7 leaving the channel'}); + }); + presenceChannel.joining(()=> presenceChannel.leave({name: 'sacOO7 leaving the channel'})); }) }); \ No newline at end of file diff --git a/tests/ably/setup/sandbox.ts b/tests/ably/setup/sandbox.ts index 39aa1120..63ff07dc 100644 --- a/tests/ably/setup/sandbox.ts +++ b/tests/ably/setup/sandbox.ts @@ -1,7 +1,6 @@ import path from "path"; -import http from 'http'; -import https from 'https'; import fs from 'fs'; +import { httpReqFunction } from "../../../src/channel/ably/utils"; let restHost = 'sandbox-rest.ably.io'; const tlsPort = 443; @@ -23,28 +22,7 @@ const loadJsonData = (dataPath, callback) => { }); } -const httpReq = (options, callback) => { - var body = options.body; - delete options.body; - var response = ''; - var request = (options.scheme == 'http' ? http : https).request(options, function (res) { - res.setEncoding('utf8'); - res.on('data', function (chunk) { - response += chunk; - }); - res.on('end', function () { - if (res.statusCode >= 300) { - callback('Invalid HTTP request: ' + response + '; statusCode = ' + res.statusCode); - } else { - callback(null, response); - } - }); - }); - request.on('error', function (err) { - callback(err); - }); - request.end(body); -} +const httpReq = httpReqFunction(); function prefixDomainWithEnvironment(domain, environment) { if (environment.toLowerCase() === 'production') { From 577a92b3483179bd4e3e863edd514c83b20bbe78 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 22 Jun 2022 07:39:49 +0530 Subject: [PATCH 083/111] Updated package.json scripts, removed local path --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6f2fd42a..ac31526f 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "types": "dist/echo.d.ts", "scripts": { "build": "npm run compile && npm run declarations", - "compile": "./node_modules/.bin/rollup -c", - "declarations": "./node_modules/.bin/tsc --emitDeclarationOnly", + "compile": "rollup -c", + "declarations": "tsc --emitDeclarationOnly", "lint": "eslint --ext .js,.ts ./src ./tests", "prepublish": "npm run build", "release": "npm run test && standard-version && git push --follow-tags && npm publish", From 318da77b123d4f518a6b36ea085e9a1e1ef0ec35 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 22 Jun 2022 23:58:13 +0530 Subject: [PATCH 084/111] updated authHost to get plain hostname, rather than port --- src/channel/ably/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 16135c4a..bfa3ac0d 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -10,7 +10,7 @@ export class AblyAuth { // TODO - Can be updated with request throttle, to send multiple request payload under single request authRequestExecuter: SequentialAuthTokenRequestExecuter; authEndpoint = '/broadcasting/auth'; - authHost = typeof window != 'undefined' && window?.location?.host; + authHost = typeof window != 'undefined' && window?.location?.hostname; authPort = typeof window != 'undefined' && window?.location?.port; httpReq = httpReqFunction(); From d007b13a9203a73bab35228e52c4262b1887b56f Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 23 Jun 2022 01:16:27 +0530 Subject: [PATCH 085/111] Added auth-protocol (http/https) support for rest request, added json parsing for string response --- src/channel/ably/auth.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index bfa3ac0d..424cecaf 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -12,6 +12,7 @@ export class AblyAuth { authEndpoint = '/broadcasting/auth'; authHost = typeof window != 'undefined' && window?.location?.hostname; authPort = typeof window != 'undefined' && window?.location?.port; + authProtocol = typeof window != 'undefined' && window?.location?.protocol.replace(':', ''); httpReq = httpReqFunction(); @@ -36,7 +37,7 @@ export class AblyAuth { port: this.authPort, path: this.authEndpoint, method: 'POST', - scheme: 'https', + scheme: this.authProtocol, headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'Content-Length': postData.length }, body: postData, }; @@ -46,7 +47,11 @@ export class AblyAuth { if (err) { reject(err); } else { - resolve(res); + if (typeof res === 'string') { + resolve(JSON.parse(res)); + } else { + resolve(res); + } } }) }); From 15fb2c0518f5862606ea51992b6c899f9e352b11 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 24 Jun 2022 16:34:40 +0530 Subject: [PATCH 086/111] Added option to set authProtocol externally as a part of options --- src/channel/ably/auth.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 424cecaf..dc33b688 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -58,16 +58,19 @@ export class AblyAuth { } constructor(options) { - const { token, requestTokenFn, authEndpoint, authHost, authPort } = options; + const { token, requestTokenFn, authEndpoint, authHost, authPort, authProtocol } = options; if (authEndpoint) { this.authEndpoint = authEndpoint; }; if (authHost) { this.authHost = authHost; - } + }; if (authPort) { this.authPort = authPort; - } + }; + if (authProtocol) { + this.authProtocol = authProtocol; + }; this.authRequestExecuter = new SequentialAuthTokenRequestExecuter(token, requestTokenFn ?? this.requestToken); } From 66ffa4673f8ce9613fd891e894b4339ddbb2af58 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 24 Jun 2022 17:22:20 +0530 Subject: [PATCH 087/111] Refactored comments for channel attach --- src/channel/ably/attach.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/channel/ably/attach.ts b/src/channel/ably/attach.ts index 6c5182bd..76578e23 100644 --- a/src/channel/ably/attach.ts +++ b/src/channel/ably/attach.ts @@ -10,13 +10,13 @@ export const beforeChannelAttach = (ablyClient, authorize: Function) => { if (channelAttachPatched) { //Only once all ably instance return; } - const internalAttach = dummyRealtimeChannel.__proto__._attach; // get parent class method, store it in temp. variable + const internalAttach = dummyRealtimeChannel.__proto__._attach; // get parent class method inferred from object, store it in temp. variable if (isNullOrUndefined(internalAttach)) { console.warn("channel internal attach function not found, please check for right library version") return; } function customInternalAttach(forceReattach, attachReason, errCallback) {// Define new function that needs to be added - const bindedInternalAttach = internalAttach.bind(this); // bind object instance at runtime based on who calls printMessage + const bindedInternalAttach = internalAttach.bind(this); // bind object instance at runtime // custom logic before attach authorize(this, (error) => { if (error) { From fda93bade254647e9c188a7625bcb0d95ecf7958 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 7 Jul 2022 18:18:11 +0530 Subject: [PATCH 088/111] Added extra attach check before publishing subscribe callback to avoid duplicates --- src/channel/ably-channel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index fdb097d2..3fc69322 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -65,8 +65,8 @@ export class AblyChannel extends Channel { */ subscribe(): any { this.channel.on(stateChange => { - const { current, reason } = stateChange; - if (current == 'attached') { + const { previous, current, reason } = stateChange; + if (previous !== 'attached' && current == 'attached') { this.subscribedListeners.forEach(listener => listener()); } else if (reason) { this._alertErrorListeners(stateChange); From bc32238454142bd0060958e871a91210b627b1c0 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 7 Jul 2022 19:23:25 +0530 Subject: [PATCH 089/111] Added exit check to avoid multiple authorization after calling attach randomly --- src/channel/ably/attach.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/channel/ably/attach.ts b/src/channel/ably/attach.ts index 76578e23..56dae3df 100644 --- a/src/channel/ably/attach.ts +++ b/src/channel/ably/attach.ts @@ -16,9 +16,14 @@ export const beforeChannelAttach = (ablyClient, authorize: Function) => { return; } function customInternalAttach(forceReattach, attachReason, errCallback) {// Define new function that needs to be added + if (this.state === 'attached' || this.authorizing) { + return; + } + this.authorizing = true; const bindedInternalAttach = internalAttach.bind(this); // bind object instance at runtime // custom logic before attach authorize(this, (error) => { + this.authorizing = false; if (error) { errCallback(error); return; From 1660c7967bbdc9692d24e7ac3e94223e072442e5 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Mon, 11 Jul 2022 19:39:21 +0530 Subject: [PATCH 090/111] Added separate typings file to avoid ably imports in js dist --- typings/ably.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 typings/ably.ts diff --git a/typings/ably.ts b/typings/ably.ts new file mode 100644 index 00000000..6f10a617 --- /dev/null +++ b/typings/ably.ts @@ -0,0 +1,3 @@ +import * as ably from 'ably'; +export type AblyRealtime = ably.Types.RealtimeCallbacks; +export type AblyRealtimeChannel = ably.Types.RealtimeChannelCallbacks; \ No newline at end of file From 65db5c52847aac4b0fbc4d1dd80c6eb78c3851e0 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Mon, 11 Jul 2022 19:41:47 +0530 Subject: [PATCH 091/111] Added separate typings imports in required files --- src/channel/ably-channel.ts | 6 +++--- src/channel/ably/utils.ts | 4 +--- src/connector/ably-connector.ts | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index 3fc69322..17833719 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -1,6 +1,6 @@ +import { AblyRealtime, AblyRealtimeChannel } from '../../typings/ably'; import { EventFormatter } from '../util'; import { Channel } from './channel'; -import * as AblyImport from 'ably'; /** * This class represents an Ably channel. @@ -9,7 +9,7 @@ export class AblyChannel extends Channel { /** * The Ably client instance. */ - ably: AblyImport.Types.RealtimeCallbacks; + ably: AblyRealtime; /** * The name of the channel. @@ -29,7 +29,7 @@ export class AblyChannel extends Channel { /** * The subscription of the channel. */ - channel: AblyImport.Types.RealtimeChannelCallbacks; + channel: AblyRealtimeChannel; /** * An array containing all registered subscribed listeners. diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 5ac25228..02edca09 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -1,5 +1,3 @@ -import * as ably from 'ably'; - export const isNullOrUndefined = (obj) => obj == null || obj === undefined; export const isEmptyString = (stringToCheck, ignoreSpaces = true) => (ignoreSpaces ? stringToCheck.trim() : stringToCheck) === ''; export const isNullOrUndefinedOrEmpty = (obj) => obj == null || obj === undefined || isEmptyString(obj); @@ -103,7 +101,7 @@ export function httpReqFunction() { * simulation apps self-delete after a while */ callback(); } else {// @ts-ignore - new ably.Rest.Platform.Http().doUri( + new Ably.Rest.Platform.Http().doUri( options.method, null, uri, diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index 52a0b0ae..8d4fb0fd 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -1,5 +1,4 @@ import { Connector } from './connector'; -import * as Ably from 'ably'; import { AblyChannel, @@ -7,6 +6,7 @@ import { AblyPresenceChannel, AblyAuth, } from './../channel'; +import { AblyRealtime } from '../../typings/ably'; /** * This class creates a connector to Ably. @@ -15,7 +15,7 @@ export class AblyConnector extends Connector { /** * The Ably instance. */ - ably: Ably.Types.RealtimeCallbacks; + ably: AblyRealtime; /** * All of the subscribed channel names. From e37b666366a518a424aa1ed658ee92fbc83b810f Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Mon, 11 Jul 2022 19:44:03 +0530 Subject: [PATCH 092/111] Increased sleep timeout of a ably channel integration test --- tests/ably/ably-channel.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index 1d4f5c3e..e1d3b1f7 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -171,7 +171,7 @@ describe('AblyChannel', () => { execute(() => mockAuthServer.broadcast('public:test', 'client-msg', 'Hello there'), 4); execute(() => mockAuthServer.broadcast('public:test', 'client-msg2', 'Hello there'), 1); - await sleep(3000); + await sleep(4000); expect(eventHandler1).toBeCalledTimes(4); expect(eventHandler2).toBeCalledTimes(4); expect(eventHandler3).toBeCalledTimes(1); From 28a6234712e9a1cdd93af57735ad7826e10198ef Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 15 Jul 2022 16:17:47 +0530 Subject: [PATCH 093/111] updated typings for global lib type declarations --- tests/ably/ably-connection.test.ts | 2 ++ tests/ably/ably-presence-channel.test.ts | 2 ++ tests/ably/ably-private-channel.test.ts | 2 ++ typings/index.d.ts | 8 ++++++++ 4 files changed, 14 insertions(+) diff --git a/tests/ably/ably-connection.test.ts b/tests/ably/ably-connection.test.ts index 0c86f8a1..68d13957 100644 --- a/tests/ably/ably-connection.test.ts +++ b/tests/ably/ably-connection.test.ts @@ -2,6 +2,7 @@ import { setup, tearDown } from './setup/sandbox'; import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; import safeAssert from './setup/utils'; +import * as Ably from 'ably'; jest.setTimeout(20000); describe('AblyConnection', () => { @@ -32,6 +33,7 @@ describe('AblyConnection', () => { }) beforeEach(() => { + global.Ably = Ably; echo = new Echo({ broadcaster: 'ably', useTls: true, diff --git a/tests/ably/ably-presence-channel.test.ts b/tests/ably/ably-presence-channel.test.ts index 486bde19..666eb8ae 100644 --- a/tests/ably/ably-presence-channel.test.ts +++ b/tests/ably/ably-presence-channel.test.ts @@ -3,6 +3,7 @@ import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; import { AblyPresenceChannel } from '../../src/channel'; import safeAssert from './setup/utils'; +import * as Ably from 'ably'; jest.setTimeout(20000); describe('AblyPresenceChannel', () => { @@ -33,6 +34,7 @@ describe('AblyPresenceChannel', () => { }) beforeEach(() => { + global.Ably = Ably; echo = new Echo({ broadcaster: 'ably', useTls: true, diff --git a/tests/ably/ably-private-channel.test.ts b/tests/ably/ably-private-channel.test.ts index c508b52b..13e44ce1 100644 --- a/tests/ably/ably-private-channel.test.ts +++ b/tests/ably/ably-private-channel.test.ts @@ -3,6 +3,7 @@ import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; import safeAssert from './setup/utils'; import { AblyChannel, AblyPrivateChannel } from '../../src/channel'; +import * as Ably from 'ably'; jest.setTimeout(20000); describe('AblyPrivateChannel', () => { @@ -33,6 +34,7 @@ describe('AblyPrivateChannel', () => { }) beforeEach(() => { + global.Ably = Ably; echo = new Echo({ broadcaster: 'ably', useTls: true, diff --git a/typings/index.d.ts b/typings/index.d.ts index d2e6ac82..09a3c800 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -4,3 +4,11 @@ declare let io: any; declare let Vue: any; declare let axios: any; declare let jQuery: any; + +export {}; +// libs required to be set globally before initializing echo instance (currently used in tests) +declare global { + var Ably: any; + var Pusher: any; + var io: any; +} From 4f43beffb4f5d43134fc05af291b7de222785743 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 15 Jul 2022 17:26:46 +0530 Subject: [PATCH 094/111] Added test for leaving the public, private and presence channel --- tests/ably/ably-channel.test.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index e1d3b1f7..60f51668 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -1,8 +1,9 @@ import { setup, tearDown } from './setup/sandbox'; import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; -import { AblyChannel } from '../../src/channel'; +import { AblyChannel, AblyPresenceChannel } from '../../src/channel'; import safeAssert, { execute, sleep } from './setup/utils'; +import * as Ably from 'ably'; jest.setTimeout(20000); describe('AblyChannel', () => { @@ -33,6 +34,7 @@ describe('AblyChannel', () => { }) beforeEach(() => { + global.Ably = Ably; echo = new Echo({ broadcaster: 'ably', useTls: true, @@ -186,4 +188,23 @@ describe('AblyChannel', () => { expect(eventHandler3).toBeCalledTimes(2); }) + + test('Leave channel', async() => { + const publicChannel = echo.channel('test') as AblyChannel; + const privateChannel = echo.private('test') as AblyChannel; + const presenceChannel = echo.join('test') as AblyPresenceChannel; + const publicChannelSubscription = new Promise(resolve => publicChannel.subscribed(resolve)); + const privateChannelSubscription = new Promise(resolve => privateChannel.subscribed(resolve)); + const presenceChannelSubscription = new Promise(resolve => presenceChannel.subscribed(resolve)); + + await Promise.all([publicChannelSubscription, privateChannelSubscription, presenceChannelSubscription]); + + echo.leave('test'); + + const publicDetachPromise = new Promise(resolve => publicChannel.channel.on('detached', resolve)); + const privateDetachPromise = new Promise(resolve => privateChannel.channel.on('detached', resolve)); + const presenceDetachPromise = new Promise(resolve => presenceChannel.channel.on('detached', resolve)); + + await Promise.all([publicDetachPromise, privateDetachPromise, presenceDetachPromise]); + }) }); \ No newline at end of file From 1b65765ce9dff092339d70752101604a2669b56b Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 19 Jul 2022 01:16:10 +0530 Subject: [PATCH 095/111] Added token expiry check using ably server time --- src/channel/ably/auth.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index dc33b688..8d1a2576 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -76,6 +76,7 @@ export class AblyAuth { enableAuthorizeBeforeChannelAttach = (ablyConnector: AblyConnector) => { const ablyClient: any = ablyConnector.ably; + ablyClient.auth.getTimestamp(this.authOptions.queryTime, ()=> {}); // generates serverTimeOffset in the background beforeChannelAttach(ablyClient, (realtimeChannel, errorCallback) => { const channelName = realtimeChannel.name; if (channelName.startsWith("public:")) { @@ -88,7 +89,7 @@ export class AblyAuth { if (tokenDetails) { const capability = parseJwt(tokenDetails.token).payload['x-ably-capability']; const tokenHasChannelCapability = capability.includes(`${channelName}"`); - if (tokenHasChannelCapability && tokenDetails.expires > Date.now()) { // TODO : Replace with server time + if (tokenHasChannelCapability && tokenDetails.expires > ablyClient.auth.getTimestampUsingOffset()) { // checks with server time using offset, otherwise local time errorCallback(null); return; } From 9e691f0b4c30000a806d22aacc8272cbf6bec6b1 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 21 Jul 2022 16:12:35 +0530 Subject: [PATCH 096/111] Added missing declarations for Vue, Axios and jQuery inside global --- typings/index.d.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 09a3c800..820c53fa 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,14 +1,11 @@ -declare let Pusher: any; -declare let Ably: any; -declare let io: any; -declare let Vue: any; -declare let axios: any; -declare let jQuery: any; - -export {}; // libs required to be set globally before initializing echo instance (currently used in tests) declare global { var Ably: any; var Pusher: any; var io: any; + var Vue: any; + var axios: any; + var jQuery: any; } + +export {}; From f7ef2d4f6af319fffba5685b2a019e58aac95018 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 22 Jul 2022 01:49:56 +0530 Subject: [PATCH 097/111] Removed unnecessary test-app-setup.json file, updated sandbox --- tests/ably/setup/sandbox.ts | 83 ++++++++-------------------- tests/ably/setup/test-app-setup.json | 48 ---------------- 2 files changed, 24 insertions(+), 107 deletions(-) delete mode 100644 tests/ably/setup/test-app-setup.json diff --git a/tests/ably/setup/sandbox.ts b/tests/ably/setup/sandbox.ts index 63ff07dc..5e8eb42d 100644 --- a/tests/ably/setup/sandbox.ts +++ b/tests/ably/setup/sandbox.ts @@ -1,74 +1,39 @@ -import path from "path"; -import fs from 'fs'; import { httpReqFunction } from "../../../src/channel/ably/utils"; let restHost = 'sandbox-rest.ably.io'; const tlsPort = 443; const toBase64 = (text: string) => Buffer.from(text, 'binary').toString('base64'); -const loadJsonData = (dataPath, callback) => { - fs.readFile(path.join(__dirname, dataPath), (err, data: Buffer) => { - if (err) { - callback(err); - return; - } - try { - data = JSON.parse(data.toString()); - } catch (e) { - callback(e); - return; - } - callback(null, data); - }); -} - const httpReq = httpReqFunction(); -function prefixDomainWithEnvironment(domain, environment) { - if (environment.toLowerCase() === 'production') { - return domain; - } else { - return environment + '-' + domain; - } -} - const creatNewApp = (callback) => { - loadJsonData('test-app-setup.json', function (err, testData) { + var postData = JSON.stringify({ + "keys": [{}], + "namespaces": [] + }); + var postOptions = { + host: restHost, + port: tlsPort, + path: '/apps', + method: 'POST', + scheme: 'https', + headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'Content-Length': postData.length }, + body: postData, + }; + + httpReq(postOptions, function (err, res) { if (err) { callback(err); - return; + } else { + if (typeof res === 'string') res = JSON.parse(res); + var testApp = { + accountId: res.accountId, + appId: res.appId, + keys: res.keys, + cipherConfig: undefined + }; + callback(null, testApp); } - var postData = JSON.stringify(testData.post_apps); - var postOptions = { - host: restHost, - port: tlsPort, - path: '/apps', - method: 'POST', - scheme: 'https', - headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'Content-Length': postData.length }, - body: postData, - }; - - httpReq(postOptions, function (err, res) { - if (err) { - callback(err); - } else { - if (typeof res === 'string') res = JSON.parse(res); - if (res.keys.length != testData.post_apps.keys.length) { - callback('Failed to create correct number of keys for app'); - } else if (res.namespaces.length != testData.post_apps.namespaces.length) { - callback('Failed to create correct number of namespaces for app'); - } else { - var testApp = { - accountId: res.accountId, - appId: res.appId, - keys: res.keys, - cipherConfig: testData.cipher, - }; - callback(null, testApp); - } - } - }); }); } diff --git a/tests/ably/setup/test-app-setup.json b/tests/ably/setup/test-app-setup.json deleted file mode 100644 index 41d4b6e4..00000000 --- a/tests/ably/setup/test-app-setup.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "_post_apps": "/* JSON body using in POST sandbox-rest.ably.io/apps request to set up the Test app */", - "post_apps": { - "limits": { "presence": { "maxMembers": 250 } }, - "keys": [ - {}, - { - "capability": "{ \"cansubscribe:*\":[\"subscribe\"], \"canpublish:*\":[\"publish\"], \"canpublish:andpresence\":[\"presence\",\"publish\"], \"pushenabled:*\":[\"publish\",\"subscribe\",\"push-subscribe\"], \"pushenabled:admin:*\":[\"publish\",\"subscribe\",\"push-admin\"] }" - }, - { - "capability": "{ \"channel0\":[\"publish\"], \"channel1\":[\"publish\"], \"channel2\":[\"publish\", \"subscribe\"], \"channel3\":[\"subscribe\"], \"channel4\":[\"presence\", \"publish\", \"subscribe\"], \"channel5\":[\"presence\"], \"channel6\":[\"*\"] }" - }, - { - "capability": "{ \"*\":[\"subscribe\"] }" - } - ], - "namespaces": [ - { "id": "persisted", "persisted": true }, - { "id": "pushenabled", "pushEnabled": true } - ], - "channels": [ - { - "name": "persisted:presence_fixtures", - "presence": [ - { "clientId": "client_bool", "data": "true" }, - { "clientId": "client_int", "data": "24" }, - { "clientId": "client_string", "data": "This is a string clientData payload" }, - { "clientId": "client_json", "data": "{ \"test\": \"This is a JSONObject clientData payload\"}" }, - { "clientId": "client_decoded", "data": "{\"example\":{\"json\":\"Object\"}}", "encoding": "json" }, - { - "clientId": "client_encoded", - "data": "HO4cYSP8LybPYBPZPHQOtuD53yrD3YV3NBoTEYBh4U0N1QXHbtkfsDfTspKeLQFt", - "encoding": "json/utf-8/cipher+aes-128-cbc/base64" - } - ] - } - ] - }, - - "_cipher": "/* Cipher configuration for client_encoded presence fixture data used in REST tests */", - "cipher": { - "algorithm": "aes", - "mode": "cbc", - "keylength": 128, - "key": "WUP6u0K7MXI5Zeo0VppPwg==", - "iv": "HO4cYSP8LybPYBPZPHQOtg==" - } - } \ No newline at end of file From e077e706dd78c3c64a30d7973279a5294eafbeb6 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 22 Jul 2022 03:09:45 +0530 Subject: [PATCH 098/111] Simplified sandbox implementation, testSetup made easy, refactored tests --- tests/ably/ably-channel.test.ts | 23 ++------- tests/ably/ably-connection.test.ts | 23 ++------- tests/ably/ably-presence-channel.test.ts | 23 ++------- tests/ably/ably-private-channel.test.ts | 23 ++------- tests/ably/ably-sandbox.test.ts | 24 +++------- tests/ably/sandbox.test.ts | 47 ------------------- tests/ably/setup/sandbox.ts | 59 +++++------------------- 7 files changed, 38 insertions(+), 184 deletions(-) delete mode 100644 tests/ably/sandbox.test.ts diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index 60f51668..42431ed2 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -11,26 +11,13 @@ describe('AblyChannel', () => { let mockAuthServer: MockAuthServer; let echo: Echo; - beforeAll(done => { - setup((err, app) => { - if (err) { - done(err); - return; - } - testApp = app; - mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); - done(); - }) + beforeAll(async () => { + testApp = await setup(); + mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); }) - afterAll((done) => { - tearDown(testApp, (err) => { - if (err) { - done(err); - return; - } - done(); - }) + afterAll(async() => { + return await tearDown(testApp); }) beforeEach(() => { diff --git a/tests/ably/ably-connection.test.ts b/tests/ably/ably-connection.test.ts index 68d13957..07d10d37 100644 --- a/tests/ably/ably-connection.test.ts +++ b/tests/ably/ably-connection.test.ts @@ -10,26 +10,13 @@ describe('AblyConnection', () => { let mockAuthServer: MockAuthServer; let echo: Echo; - beforeAll(done => { - setup((err, app) => { - if (err) { - done(err); - return; - } - testApp = app; - mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); - done(); - }) + beforeAll(async () => { + testApp = await setup(); + mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); }) - afterAll((done) => { - tearDown(testApp, (err) => { - if (err) { - done(err); - return; - } - done(); - }) + afterAll(async() => { + return await tearDown(testApp); }) beforeEach(() => { diff --git a/tests/ably/ably-presence-channel.test.ts b/tests/ably/ably-presence-channel.test.ts index 666eb8ae..c614e558 100644 --- a/tests/ably/ably-presence-channel.test.ts +++ b/tests/ably/ably-presence-channel.test.ts @@ -11,26 +11,13 @@ describe('AblyPresenceChannel', () => { let mockAuthServer: MockAuthServer; let echo: Echo; - beforeAll(done => { - setup((err, app) => { - if (err) { - done(err); - return; - } - testApp = app; - mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); - done(); - }) + beforeAll(async () => { + testApp = await setup(); + mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); }) - afterAll((done) => { - tearDown(testApp, (err) => { - if (err) { - done(err); - return; - } - done(); - }) + afterAll(async() => { + return await tearDown(testApp); }) beforeEach(() => { diff --git a/tests/ably/ably-private-channel.test.ts b/tests/ably/ably-private-channel.test.ts index 13e44ce1..bbe04c11 100644 --- a/tests/ably/ably-private-channel.test.ts +++ b/tests/ably/ably-private-channel.test.ts @@ -11,26 +11,13 @@ describe('AblyPrivateChannel', () => { let mockAuthServer: MockAuthServer; let echo: Echo; - beforeAll(done => { - setup((err, app) => { - if (err) { - done(err); - return; - } - testApp = app; - mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); - done(); - }) + beforeAll(async () => { + testApp = await setup(); + mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); }) - afterAll((done) => { - tearDown(testApp, (err) => { - if (err) { - done(err); - return; - } - done(); - }) + afterAll(async() => { + return await tearDown(testApp); }) beforeEach(() => { diff --git a/tests/ably/ably-sandbox.test.ts b/tests/ably/ably-sandbox.test.ts index 2f12c3a7..015f2503 100644 --- a/tests/ably/ably-sandbox.test.ts +++ b/tests/ably/ably-sandbox.test.ts @@ -6,15 +6,12 @@ jest.setTimeout(20000); describe('AblySandbox', () => { let testApp; - beforeAll(done => { - setup((err, app) => { - if (err) { - done(err); - return; - } - testApp = app; - done(); - }) + beforeAll(async () => { + testApp = await setup(); + }) + + afterAll(async() => { + return await tearDown(testApp); }) test('init with key string', () => { @@ -36,13 +33,4 @@ describe('AblySandbox', () => { }); }) - afterAll((done) => { - tearDown(testApp, (err) => { - if (err) { - done(err); - return; - } - done(); - }) - }) }); \ No newline at end of file diff --git a/tests/ably/sandbox.test.ts b/tests/ably/sandbox.test.ts deleted file mode 100644 index acfe9401..00000000 --- a/tests/ably/sandbox.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { setup, tearDown } from './setup/sandbox'; -import * as Ably from 'ably'; - -jest.setTimeout(20000); -describe('AblySandbox', () => { - let testApp; - - beforeAll(done => { - setup((err, app) => { - if (err) { - done(err); - return; - } - testApp = app; - done(); - }) - }) - - test('init with key string', () => { - const apiKey = testApp.keys[0].keyStr; - expect(typeof apiKey).toBe('string'); - expect(apiKey).toBeTruthy(); - }) - - test('rest time should work', (done) => { - const apiKey = testApp.keys[0].keyStr; - const restClient = new Ably.Rest(apiKey); - restClient.time((err, time) => { - if (err) { - done(err); - return; - } - expect(typeof time).toBe('number'); - done() - }); - }) - - afterAll((done) => { - tearDown(testApp, (err) => { - if (err) { - done(err); - return; - } - done(); - }) - }) -}); \ No newline at end of file diff --git a/tests/ably/setup/sandbox.ts b/tests/ably/setup/sandbox.ts index 5e8eb42d..4dfc5e69 100644 --- a/tests/ably/setup/sandbox.ts +++ b/tests/ably/setup/sandbox.ts @@ -1,58 +1,23 @@ -import { httpReqFunction } from "../../../src/channel/ably/utils"; +import got from 'got'; -let restHost = 'sandbox-rest.ably.io'; -const tlsPort = 443; -const toBase64 = (text: string) => Buffer.from(text, 'binary').toString('base64'); +let sandboxUrl = 'https://sandbox-rest.ably.io/apps'; -const httpReq = httpReqFunction(); - -const creatNewApp = (callback) => { - var postData = JSON.stringify({ +const creatNewApp = async () => { + var body = { "keys": [{}], "namespaces": [] - }); - var postOptions = { - host: restHost, - port: tlsPort, - path: '/apps', - method: 'POST', - scheme: 'https', - headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'Content-Length': postData.length }, - body: postData, }; + const res: { + appId: string, + keys: { keyStr: string}[], + } = await got.post(sandboxUrl, { json: body }).json(); - httpReq(postOptions, function (err, res) { - if (err) { - callback(err); - } else { - if (typeof res === 'string') res = JSON.parse(res); - var testApp = { - accountId: res.accountId, - appId: res.appId, - keys: res.keys, - cipherConfig: undefined - }; - callback(null, testApp); - } - }); + return res; } -const deleteApp = (app, callback) => { - var authKey = app.keys[0].keyStr, - authHeader = toBase64(authKey); - - var delOptions = { - host: restHost, - port: tlsPort, - method: 'DELETE', - path: '/apps/' + app.appId, - scheme: 'https', - headers: { Authorization: 'Basic ' + authHeader }, - }; - - httpReq(delOptions, function (err) { - callback(err); - }); +const deleteApp = async (app) => { + var authKey = app.keys[0].keyStr; + return got.delete(`${sandboxUrl}/${app.appId}?key=${authKey}`); } export { creatNewApp as setup, deleteApp as tearDown } \ No newline at end of file From 25c62e847cda8ff16a1792c08cce016e6038522a Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 22 Jul 2022 03:10:31 +0530 Subject: [PATCH 099/111] Removed unnecessary http request code from utils, updated auth file --- src/channel/ably/auth.ts | 6 +- src/channel/ably/utils.ts | 142 ++++---------------------------------- 2 files changed, 17 insertions(+), 131 deletions(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 8d1a2576..8716f889 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -1,5 +1,5 @@ import { beforeChannelAttach } from './attach'; -import { httpReqFunction, parseJwt, toTokenDetails } from './utils'; +import { httpRequest, toTokenDetails, parseJwt} from './utils'; import { SequentialAuthTokenRequestExecuter } from './token-request'; import { AblyChannel } from '../ably-channel'; import { AblyConnector } from '../../connector/ably-connector'; @@ -14,8 +14,6 @@ export class AblyAuth { authPort = typeof window != 'undefined' && window?.location?.port; authProtocol = typeof window != 'undefined' && window?.location?.protocol.replace(':', ''); - httpReq = httpReqFunction(); - authOptions = { queryTime: true, useTokenAuth: true, @@ -43,7 +41,7 @@ export class AblyAuth { }; return new Promise((resolve, reject) => { - this.httpReq(postOptions, function (err: any, res: any) { + httpRequest(postOptions, function (err: any, res: any) { if (err) { reject(err); } else { diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 02edca09..3321ba59 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -17,10 +17,10 @@ export const parseJwt = (jwtToken: string): { header: any, payload: any } => { return { header, payload }; } +// RSA4f - tokenDetails size should't exceed 128kb, so omitted `capability` property export const toTokenDetails = (jwtToken: string) => { const { payload } = parseJwt(jwtToken); return { - // capability: parsedJwt['x-ably-capability'], // RSA4f - tokenDetails size should't exceed 128kb clientId: payload['x-ably-clientId'], expires: payload.exp * 1000, // Convert Seconds to ms issued: payload.iat * 1000, @@ -29,7 +29,6 @@ export const toTokenDetails = (jwtToken: string) => { } const isBrowser = typeof window === 'object'; -const isNativescript = typeof global === 'object' && global.isNativescript; const toBase64 = (text: string) => { if (isBrowser) { @@ -45,130 +44,19 @@ const toText = (base64: string) => { return Buffer.from(base64, 'base64').toString('binary'); }; -function createXHR() { - var result = new XMLHttpRequest(); - if ('withCredentials' in result) return result;// @ts-ignore - if (typeof XDomainRequest !== 'undefined') { // @ts-ignore - var xdr = new XDomainRequest(); /* Use IE-specific "CORS" code with XDR */ - xdr.isXDR = true; - return xdr; - } - return null; -} - -function schemeMatchesCurrent(scheme) { - return scheme === window.location.protocol.slice(0, -1); -} - -export function httpReqFunction() { - if (isNativescript) { - return function (options, callback) { - var http = require('http'); - var uri = options.scheme + '://' + options.host + ':' + options.port + options.path; - - http - .request({ - url: uri, - method: options.method || 'GET', - timeout: 10000, - headers: options.headers, - content: options.body, - }) - .then(function (results) { - callback(null, results.content.toString()); - }) - ['catch'](function (err) { - callback(err); - }); - }; - } else if (isBrowser) { - return function (options, callback) { - var xhr = createXHR(); - var uri; - - uri = options.scheme + '://' + options.host + ':' + options.port + options.path; - - if (xhr.isXDR && !schemeMatchesCurrent(options.scheme)) { - /* Can't use XDR for cross-scheme. For some requests could just force - * the same scheme and be done with it, but not for authenticated - * requests to ably, can't use basic auth for non-tls endpoints. - * Luckily ably can handle jsonp, so just use the ably Http method, - * which will use the jsonp transport. Can't just do this all the time - * as the local express webserver serves files statically, so can't do - * jsonp. */ - if (options.method === 'DELETE') { - /* Ignore DELETEs -- can't be done with jsonp at the moment, and - * simulation apps self-delete after a while */ - callback(); - } else {// @ts-ignore - new Ably.Rest.Platform.Http().doUri( - options.method, - null, - uri, - options.headers, - options.body, - options.paramsIfNoHeaders || {}, - callback - ); - } - return; - } - - xhr.open(options.method, uri); - if (options.headers && !xhr.isXDR) { - for (var h in options.headers) if (h !== 'Content-Length') xhr.setRequestHeader(h, options.headers[h]); - } - xhr.onerror = function (err) { - callback(err); - }; - if ('onreadystatechange' in xhr) { - /* XHR */ - xhr.onreadystatechange = function () { - if (xhr.readyState == 4) { - if (xhr.status >= 300) { - callback('HTTP request failed ' + xhr.status); - return; - } - callback(null, xhr.responseText); - } - }; - } else { - /* XDR */ - xhr.onload = function () { - if (xhr.status >= 300) { - callback('HTTP request failed ' + xhr.status); - return; - } - callback(null, xhr.responseText); - }; - } - xhr.send(options.body); - }; - } else { - var http = require('http'), - https = require('https'); - - return function (options, callback) { - var body = options.body; - delete options.body; - var response = ''; - var request = (options.scheme == 'http' ? http : https).request(options, function (res) { - res.setEncoding('utf8'); - res.on('data', function (chunk) { - response += chunk; - }); - res.on('end', function () { - if (res.statusCode >= 300) { - callback('Invalid HTTP request: ' + response + '; statusCode = ' + res.statusCode); - } else { - callback(null, response); - } - }); - }); - request.on('error', function (err) { - callback(err); - }); - request.end(body); - }; +let httpClient: any; +export function httpRequest(options, callback) { + const uri = options.scheme + '://' + options.host + ':' + options.port + options.path; + if (!httpClient) { + httpClient = new Ably.Rest.Platform.Http(); } + httpClient.doUri( + options.method, + null, + uri, + options.headers, + options.body, + options.paramsIfNoHeaders || {}, + callback + ); } From a5d5b4fbcd43cbffea3a32177441e6fac8140611 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 22 Jul 2022 03:34:32 +0530 Subject: [PATCH 100/111] removed sleep statements from channel tests --- package.json | 4 ++- tests/ably/ably-channel.test.ts | 63 ++++++++++++++++++++------------- tests/ably/setup/sandbox.ts | 9 +++-- tests/ably/setup/utils.ts | 2 -- 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index ac31526f..fa402e14 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@typescript-eslint/parser": "^5.14.0", "ably": "^1.2.20", "eslint": "^8.11.0", + "got": "^11.8.3", "jest": "^27.5.1", "jsonwebtoken": "^8.5.1", "rollup": "^2.70.1", @@ -50,7 +51,8 @@ "standard-version": "^9.3.2", "ts-jest": "^27.1.3", "tslib": "^2.3.1", - "typescript": "^4.6.2" + "typescript": "^4.6.2", + "wait-for-expect": "^3.0.2" }, "engines": { "node": ">=10" diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index 42431ed2..4df3d9fc 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -2,8 +2,9 @@ import { setup, tearDown } from './setup/sandbox'; import Echo from '../../src/echo'; import { MockAuthServer } from './setup/mock-auth-server'; import { AblyChannel, AblyPresenceChannel } from '../../src/channel'; -import safeAssert, { execute, sleep } from './setup/utils'; +import safeAssert, { execute } from './setup/utils'; import * as Ably from 'ably'; +import waitForExpect from "wait-for-expect"; jest.setTimeout(20000); describe('AblyChannel', () => { @@ -16,7 +17,7 @@ describe('AblyChannel', () => { mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); }) - afterAll(async() => { + afterAll(async () => { return await tearDown(testApp); }) @@ -119,26 +120,34 @@ describe('AblyChannel', () => { }); execute(() => mockAuthServer.broadcast('public:test', 'testEvent', 'Hello there'), 4); - await sleep(3000); - expect(eventHandler1).toBeCalledTimes(4); - expect(eventHandler2).toBeCalledTimes(4); - expect(eventHandler3).toBeCalledTimes(4); + + await waitForExpect(() => { + expect(eventHandler1).toBeCalledTimes(4); + expect(eventHandler2).toBeCalledTimes(4); + expect(eventHandler3).toBeCalledTimes(4); + }) + jest.clearAllMocks(); publicChannel.stopListening('.testEvent', eventHandler1); execute(() => mockAuthServer.broadcast('public:test', 'testEvent', 'Hello there'), 3); - await sleep(3000); - expect(eventHandler1).toBeCalledTimes(0); - expect(eventHandler2).toBeCalledTimes(3); - expect(eventHandler3).toBeCalledTimes(3); + + await waitForExpect(() => { + expect(eventHandler1).toBeCalledTimes(0); + expect(eventHandler2).toBeCalledTimes(3); + expect(eventHandler3).toBeCalledTimes(3); + }) + jest.clearAllMocks(); publicChannel.stopListening('.testEvent'); execute(() => mockAuthServer.broadcast('public:test', 'testEvent', 'Hello there'), 3); - await sleep(3000); - expect(eventHandler1).toBeCalledTimes(0); - expect(eventHandler2).toBeCalledTimes(0); - expect(eventHandler3).toBeCalledTimes(0); + + await waitForExpect(() => { + expect(eventHandler1).toBeCalledTimes(0); + expect(eventHandler2).toBeCalledTimes(0); + expect(eventHandler3).toBeCalledTimes(0); + }); }) @@ -160,30 +169,34 @@ describe('AblyChannel', () => { execute(() => mockAuthServer.broadcast('public:test', 'client-msg', 'Hello there'), 4); execute(() => mockAuthServer.broadcast('public:test', 'client-msg2', 'Hello there'), 1); - await sleep(4000); - expect(eventHandler1).toBeCalledTimes(4); - expect(eventHandler2).toBeCalledTimes(4); - expect(eventHandler3).toBeCalledTimes(1); + + await waitForExpect(() => { + expect(eventHandler1).toBeCalledTimes(4); + expect(eventHandler2).toBeCalledTimes(4); + expect(eventHandler3).toBeCalledTimes(1); + }); + jest.clearAllMocks(); publicChannel.stopListeningForWhisper('msg', eventHandler1); execute(() => mockAuthServer.broadcast('public:test', 'client-msg', 'Hello there'), 3); execute(() => mockAuthServer.broadcast('public:test', 'client-msg2', 'Hello there'), 2); - await sleep(3000); - expect(eventHandler1).toBeCalledTimes(0); - expect(eventHandler2).toBeCalledTimes(3); - expect(eventHandler3).toBeCalledTimes(2); - + + await waitForExpect(() => { + expect(eventHandler1).toBeCalledTimes(0); + expect(eventHandler2).toBeCalledTimes(3); + expect(eventHandler3).toBeCalledTimes(2); + }) }) - test('Leave channel', async() => { + test('Leave channel', async () => { const publicChannel = echo.channel('test') as AblyChannel; const privateChannel = echo.private('test') as AblyChannel; const presenceChannel = echo.join('test') as AblyPresenceChannel; const publicChannelSubscription = new Promise(resolve => publicChannel.subscribed(resolve)); const privateChannelSubscription = new Promise(resolve => privateChannel.subscribed(resolve)); const presenceChannelSubscription = new Promise(resolve => presenceChannel.subscribed(resolve)); - + await Promise.all([publicChannelSubscription, privateChannelSubscription, presenceChannelSubscription]); echo.leave('test'); diff --git a/tests/ably/setup/sandbox.ts b/tests/ably/setup/sandbox.ts index 4dfc5e69..d008eee5 100644 --- a/tests/ably/setup/sandbox.ts +++ b/tests/ably/setup/sandbox.ts @@ -9,10 +9,9 @@ const creatNewApp = async () => { }; const res: { appId: string, - keys: { keyStr: string}[], - } = await got.post(sandboxUrl, { json: body }).json(); - - return res; + keys: { keyStr: string }[], + } = await got.post(sandboxUrl, { json: body }).json(); + return res; } const deleteApp = async (app) => { @@ -20,4 +19,4 @@ const deleteApp = async (app) => { return got.delete(`${sandboxUrl}/${app.appId}?key=${authKey}`); } -export { creatNewApp as setup, deleteApp as tearDown } \ No newline at end of file +export { creatNewApp as setup, deleteApp as tearDown } diff --git a/tests/ably/setup/utils.ts b/tests/ably/setup/utils.ts index aa8f8be7..e88f94c7 100644 --- a/tests/ably/setup/utils.ts +++ b/tests/ably/setup/utils.ts @@ -9,8 +9,6 @@ const safeAssert = ((assertions: Function, done: Function, finalAssertion = fals } }); -export const sleep = (time : number) => new Promise(res => setTimeout(res, time)); - export const execute = (fn: Function, times : number) => { while(times--) { fn(); From 2a83224930a5d41b6aa024f027969e102f3a43f5 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 22 Jul 2022 04:14:43 +0530 Subject: [PATCH 101/111] Added browser check to avoid setting content-type header for XHR request --- src/channel/ably/utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 3321ba59..a957e6d0 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -50,6 +50,9 @@ export function httpRequest(options, callback) { if (!httpClient) { httpClient = new Ably.Rest.Platform.Http(); } + if (isBrowser) { + delete options.headers['Content-Length']; // XHR warning - Refused to set unsafe header "Content-Length" + } httpClient.doUri( options.method, null, From 59a38abb5915317a8757f8ed6645c380bbb5ac8b Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Sat, 23 Jul 2022 01:06:13 +0530 Subject: [PATCH 102/111] Declared new type Task synonomous to Function --- src/channel/ably/token-request.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/channel/ably/token-request.ts b/src/channel/ably/token-request.ts index 523fe1ad..4415f9f6 100644 --- a/src/channel/ably/token-request.ts +++ b/src/channel/ably/token-request.ts @@ -24,13 +24,14 @@ export class SequentialAuthTokenRequestExecuter { request = (channelName: string):Promise<{token : string, info: any}> => this.execute(token => this.requestTokenFn(channelName, token)); } +type Task = Function; class TaskQueue { total: Number; - todo: Array; - running: Array; + todo: Array; + running: Array; count: Number; - constructor(tasks = [], concurrentCount = 1) { + constructor(tasks : Array= [], concurrentCount = 1) { this.total = tasks.length; this.todo = tasks; this.running = []; @@ -39,7 +40,7 @@ class TaskQueue { canRunNext = () => (this.running.length < this.count) && this.todo.length; - run = async (task: Function) => { + run = async (task: Task) => { if (task) { this.todo.push(task); } From 05a7a2d03d4bf8a65b54f9fb5d5f85d58bfa5432 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Sat, 23 Jul 2022 19:06:49 +0530 Subject: [PATCH 103/111] Refactored presence-channel with member update --- src/channel/ably-presence-channel.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts index 5deb8214..913b3540 100644 --- a/src/channel/ably-presence-channel.ts +++ b/src/channel/ably-presence-channel.ts @@ -26,7 +26,7 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel * Register a callback to be called anytime the member list changes. */ here(callback: Function): AblyPresenceChannel { - this.channel.presence.subscribe(['enter', 'update', 'leave'], _ => + this.channel.presence.subscribe(['enter', 'update', 'leave'], () => this.channel.presence.get((err, members) => callback(members, err)// returns local sync copy of updated members )); return this; @@ -36,7 +36,7 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel * Listen for someone joining the channel. */ joining(callback: Function): AblyPresenceChannel { - this.channel.presence.subscribe(['enter', 'update'], (member) => { + this.channel.presence.subscribe(['enter', 'update'], member => { callback(member); }); @@ -47,7 +47,7 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel * Listen for someone leaving the channel. */ leaving(callback: Function): AblyPresenceChannel { - this.channel.presence.subscribe('leave', (member) => { + this.channel.presence.subscribe('leave', member => { callback(member); }); From 9891d3185d31d8887f004d043fd6de0b92a14f17 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Sun, 24 Jul 2022 02:08:40 +0530 Subject: [PATCH 104/111] Refactored code, removed unnecessary TODO's and added newline at the end of each file --- src/channel/ably-private-channel.ts | 2 +- src/channel/ably/auth.ts | 8 +++----- src/channel/ably/index.ts | 2 +- src/channel/ably/token-request.ts | 2 +- tests/ably/ably-channel.test.ts | 2 +- tests/ably/ably-connection.test.ts | 2 +- tests/ably/ably-presence-channel.test.ts | 2 +- tests/ably/ably-private-channel.test.ts | 3 +-- tests/ably/ably-sandbox.test.ts | 2 +- tests/ably/setup/mock-auth-server.ts | 1 - tests/ably/setup/utils.ts | 2 +- tests/ably/utils.test.ts | 3 +-- typings/ably.ts | 2 +- typings/index.d.ts | 1 - 14 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/channel/ably-private-channel.ts b/src/channel/ably-private-channel.ts index 6f374649..78b15854 100644 --- a/src/channel/ably-private-channel.ts +++ b/src/channel/ably-private-channel.ts @@ -16,4 +16,4 @@ export class AblyPrivateChannel extends AblyChannel { return this; } -} \ No newline at end of file +} diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 8716f889..6d416709 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -7,7 +7,6 @@ import { AblyPresenceChannel } from '../ably-presence-channel'; export class AblyAuth { - // TODO - Can be updated with request throttle, to send multiple request payload under single request authRequestExecuter: SequentialAuthTokenRequestExecuter; authEndpoint = '/broadcasting/auth'; authHost = typeof window != 'undefined' && window?.location?.hostname; @@ -17,9 +16,9 @@ export class AblyAuth { authOptions = { queryTime: true, useTokenAuth: true, - authCallback: async (_, callback) => { // get token from tokenParams + authCallback: async (_, callback) => { try { - const { token } = await this.authRequestExecuter.request(null); // Replace this by network request to PHP server + const { token } = await this.authRequestExecuter.request(null); const tokenDetails = toTokenDetails(token); callback(null, tokenDetails); } catch (error) { @@ -104,7 +103,7 @@ export class AblyAuth { errorCallback(null); } }); - }).catch(err => errorCallback(err)); // TODO : Check if errors/exceptions are properly handled + }).catch(err => errorCallback(err)); }); } @@ -134,4 +133,3 @@ export class AblyAuth { } } } - diff --git a/src/channel/ably/index.ts b/src/channel/ably/index.ts index e295e15a..269586ee 100644 --- a/src/channel/ably/index.ts +++ b/src/channel/ably/index.ts @@ -1 +1 @@ -export * from './auth'; \ No newline at end of file +export * from './auth'; diff --git a/src/channel/ably/token-request.ts b/src/channel/ably/token-request.ts index 4415f9f6..6cf9bd18 100644 --- a/src/channel/ably/token-request.ts +++ b/src/channel/ably/token-request.ts @@ -51,4 +51,4 @@ class TaskQueue { this.running.shift(); } } -} \ No newline at end of file +} diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index 4df3d9fc..f0df625b 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -207,4 +207,4 @@ describe('AblyChannel', () => { await Promise.all([publicDetachPromise, privateDetachPromise, presenceDetachPromise]); }) -}); \ No newline at end of file +}); diff --git a/tests/ably/ably-connection.test.ts b/tests/ably/ably-connection.test.ts index 07d10d37..5c5f9c18 100644 --- a/tests/ably/ably-connection.test.ts +++ b/tests/ably/ably-connection.test.ts @@ -87,4 +87,4 @@ describe('AblyConnection', () => { } }); }) -}); \ No newline at end of file +}); diff --git a/tests/ably/ably-presence-channel.test.ts b/tests/ably/ably-presence-channel.test.ts index c614e558..85724742 100644 --- a/tests/ably/ably-presence-channel.test.ts +++ b/tests/ably/ably-presence-channel.test.ts @@ -76,4 +76,4 @@ describe('AblyPresenceChannel', () => { }); presenceChannel.joining(()=> presenceChannel.leave({name: 'sacOO7 leaving the channel'})); }) -}); \ No newline at end of file +}); diff --git a/tests/ably/ably-private-channel.test.ts b/tests/ably/ably-private-channel.test.ts index bbe04c11..9d705e5d 100644 --- a/tests/ably/ably-private-channel.test.ts +++ b/tests/ably/ably-private-channel.test.ts @@ -56,7 +56,6 @@ describe('AblyPrivateChannel', () => { }); }) - // TODO - fix recursived attach when connection is closed, reproduce using API_KEY instead of sandbox test('channel subscription error, token expired', done => { mockAuthServer.setAuthExceptions(['private:shortLivedChannel']); const privateChannel = echo.private('shortLivedChannel') as AblyChannel; @@ -76,4 +75,4 @@ describe('AblyPrivateChannel', () => { safeAssert(() => expect(stateChangeError).toBeTruthy(), done, true) }); }); -}); \ No newline at end of file +}); diff --git a/tests/ably/ably-sandbox.test.ts b/tests/ably/ably-sandbox.test.ts index 015f2503..0ce6f0b9 100644 --- a/tests/ably/ably-sandbox.test.ts +++ b/tests/ably/ably-sandbox.test.ts @@ -33,4 +33,4 @@ describe('AblySandbox', () => { }); }) -}); \ No newline at end of file +}); diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index 6c457307..52280460 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -86,4 +86,3 @@ export class MockAuthServer { return claims; } } - diff --git a/tests/ably/setup/utils.ts b/tests/ably/setup/utils.ts index e88f94c7..39e59960 100644 --- a/tests/ably/setup/utils.ts +++ b/tests/ably/setup/utils.ts @@ -15,4 +15,4 @@ export const execute = (fn: Function, times : number) => { } } -export default safeAssert; \ No newline at end of file +export default safeAssert; diff --git a/tests/ably/utils.test.ts b/tests/ably/utils.test.ts index 4a878f2c..c902dd05 100644 --- a/tests/ably/utils.test.ts +++ b/tests/ably/utils.test.ts @@ -1,6 +1,5 @@ import { parseJwt, toTokenDetails } from "../../src/channel/ably/utils"; -// TODO - Update token with string capability describe('Utils', () => { test('should parse JWT properly', () => { const token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImFiY2QifQ.eyJpYXQiOjE2NTQ2MzQyMTIsImV4cCI6MTY1NDYzNzgxMiwieC1hYmx5LWNsaWVudElkIjoidXNlcjEyMyIsIngtYWJseS1jYXBhYmlsaXR5Ijoie1wicHVibGljOipcIjpbXCJzdWJzY3JpYmVcIixcImhpc3RvcnlcIixcImNoYW5uZWwtbWV0YWRhdGFcIl19In0.GenM5EyUeJvgAGBD_EG-89FueNKWtyRZyi61s9G2Bs4"; @@ -28,4 +27,4 @@ describe('Utils', () => { expect(tokenDetails.issued).toBe(1654634212000); expect(tokenDetails.token).toBe(token); }) -}); \ No newline at end of file +}); diff --git a/typings/ably.ts b/typings/ably.ts index 6f10a617..d2063b89 100644 --- a/typings/ably.ts +++ b/typings/ably.ts @@ -1,3 +1,3 @@ import * as ably from 'ably'; export type AblyRealtime = ably.Types.RealtimeCallbacks; -export type AblyRealtimeChannel = ably.Types.RealtimeChannelCallbacks; \ No newline at end of file +export type AblyRealtimeChannel = ably.Types.RealtimeChannelCallbacks; diff --git a/typings/index.d.ts b/typings/index.d.ts index 820c53fa..8d622df2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -7,5 +7,4 @@ declare global { var axios: any; var jQuery: any; } - export {}; From 443433b2f9d93078d6b2415e4a073da13e08cb1a Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Sun, 24 Jul 2022 18:28:42 +0530 Subject: [PATCH 105/111] Refactored code, updated class with function methods --- src/channel/ably/auth.ts | 21 +++++++++++-------- src/channel/ably/token-request.ts | 34 ++++++++++++++++++------------- src/channel/ably/utils.ts | 4 +++- typings/ably.ts | 3 +++ 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 6d416709..251a27aa 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -1,9 +1,10 @@ import { beforeChannelAttach } from './attach'; -import { httpRequest, toTokenDetails, parseJwt} from './utils'; +import { httpRequest, toTokenDetails, parseJwt } from './utils'; import { SequentialAuthTokenRequestExecuter } from './token-request'; import { AblyChannel } from '../ably-channel'; import { AblyConnector } from '../../connector/ably-connector'; import { AblyPresenceChannel } from '../ably-presence-channel'; +import { AuthOptions, ChannelStateChange } from '../../../typings/ably'; export class AblyAuth { @@ -13,12 +14,12 @@ export class AblyAuth { authPort = typeof window != 'undefined' && window?.location?.port; authProtocol = typeof window != 'undefined' && window?.location?.protocol.replace(':', ''); - authOptions = { + authOptions : AuthOptions = { queryTime: true, useTokenAuth: true, authCallback: async (_, callback) => { try { - const { token } = await this.authRequestExecuter.request(null); + const { token } = await this.authRequestExecuter.request(null); const tokenDetails = toTokenDetails(token); callback(null, tokenDetails); } catch (error) { @@ -27,7 +28,7 @@ export class AblyAuth { } } - requestToken = async (channelName: string, existingToken: string) => { + async requestToken(channelName: string, existingToken: string) { var postData = JSON.stringify({ channel_name: channelName, token: existingToken }); var postOptions = { host: this.authHost, @@ -71,9 +72,9 @@ export class AblyAuth { this.authRequestExecuter = new SequentialAuthTokenRequestExecuter(token, requestTokenFn ?? this.requestToken); } - enableAuthorizeBeforeChannelAttach = (ablyConnector: AblyConnector) => { + enableAuthorizeBeforeChannelAttach(ablyConnector: AblyConnector) { const ablyClient: any = ablyConnector.ably; - ablyClient.auth.getTimestamp(this.authOptions.queryTime, ()=> {}); // generates serverTimeOffset in the background + ablyClient.auth.getTimestamp(this.authOptions.queryTime, () => { }); // generates serverTimeOffset in the background beforeChannelAttach(ablyClient, (realtimeChannel, errorCallback) => { const channelName = realtimeChannel.name; if (channelName.startsWith("public:")) { @@ -107,9 +108,11 @@ export class AblyAuth { }); } - onChannelFailed = (echoAblyChannel: AblyChannel) => stateChange => { - if (stateChange.reason?.code == 40160) { // channel capability rejected https://help.ably.io/error/40160 - this.handleChannelAuthError(echoAblyChannel); + onChannelFailed(echoAblyChannel: AblyChannel) { + return (stateChange: ChannelStateChange) => { + if (stateChange.reason?.code == 40160) { // channel capability rejected https://help.ably.io/error/40160 + this.handleChannelAuthError(echoAblyChannel); + } } } diff --git a/src/channel/ably/token-request.ts b/src/channel/ably/token-request.ts index 6cf9bd18..9a39aef8 100644 --- a/src/channel/ably/token-request.ts +++ b/src/channel/ably/token-request.ts @@ -9,19 +9,23 @@ export class SequentialAuthTokenRequestExecuter { this.queue = new TaskQueue(); } - execute = (tokenRequestFn : Function): Promise<{token : string, info: any}> => new Promise(async (resolve, reject) => { - await this.queue.run(async () => { - try { - const {token, info} = await tokenRequestFn(this.cachedToken); - this.cachedToken = token; - resolve({token, info}); - } catch (err) { - reject(err); - } + execute(tokenRequestFn: Function): Promise<{ token: string, info: any }> { + return new Promise(async (resolve, reject) => { + await this.queue.run(async () => { + try { + const { token, info } = await tokenRequestFn(this.cachedToken); + this.cachedToken = token; + resolve({ token, info }); + } catch (err) { + reject(err); + } + }) }) - }) + } - request = (channelName: string):Promise<{token : string, info: any}> => this.execute(token => this.requestTokenFn(channelName, token)); + request(channelName: string): Promise<{ token: string, info: any }> { + return this.execute(token => this.requestTokenFn(channelName, token)); + } } type Task = Function; @@ -31,16 +35,18 @@ class TaskQueue { running: Array; count: Number; - constructor(tasks : Array= [], concurrentCount = 1) { + constructor(tasks: Array = [], concurrentCount = 1) { this.total = tasks.length; this.todo = tasks; this.running = []; this.count = concurrentCount; } - canRunNext = () => (this.running.length < this.count) && this.todo.length; + canRunNext() { + return (this.running.length < this.count) && this.todo.length; + }; - run = async (task: Task) => { + async run(task: Task) { if (task) { this.todo.push(task); } diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index a957e6d0..2ba2b6ac 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -1,3 +1,5 @@ +import { TokenDetails } from "../../../typings/ably"; + export const isNullOrUndefined = (obj) => obj == null || obj === undefined; export const isEmptyString = (stringToCheck, ignoreSpaces = true) => (ignoreSpaces ? stringToCheck.trim() : stringToCheck) === ''; export const isNullOrUndefinedOrEmpty = (obj) => obj == null || obj === undefined || isEmptyString(obj); @@ -18,7 +20,7 @@ export const parseJwt = (jwtToken: string): { header: any, payload: any } => { } // RSA4f - tokenDetails size should't exceed 128kb, so omitted `capability` property -export const toTokenDetails = (jwtToken: string) => { +export const toTokenDetails = (jwtToken: string) : TokenDetails | any => { const { payload } = parseJwt(jwtToken); return { clientId: payload['x-ably-clientId'], diff --git a/typings/ably.ts b/typings/ably.ts index d2063b89..df4bb582 100644 --- a/typings/ably.ts +++ b/typings/ably.ts @@ -1,3 +1,6 @@ import * as ably from 'ably'; export type AblyRealtime = ably.Types.RealtimeCallbacks; export type AblyRealtimeChannel = ably.Types.RealtimeChannelCallbacks; +export type ChannelStateChange = ably.Types.ChannelStateChange; +export type AuthOptions = ably.Types.AuthOptions; +export type TokenDetails = ably.Types.TokenDetails; From 10120c13bd641e3cb1caa89b7a9f79bf1cd5054f Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Sun, 24 Jul 2022 18:42:53 +0530 Subject: [PATCH 106/111] Updated tests with class methods --- tests/ably/setup/mock-auth-server.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index 52280460..75368928 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -9,7 +9,7 @@ export class MockAuthServer { keySecret: string; ablyClient: Ably.Rest; clientId = 'sacOO7@github.com' - userInfo = {id : 'sacOO7@github.com', name: 'sacOO7'} + userInfo = { id: 'sacOO7@github.com', name: 'sacOO7' } shortLived: channels; banned: channels; @@ -18,20 +18,20 @@ export class MockAuthServer { const keys = apiKey.split(':'); this.keyName = keys[0]; this.keySecret = keys[1]; - this.ablyClient = new Ably.Rest({key: apiKey, environment}); + this.ablyClient = new Ably.Rest({ key: apiKey, environment }); } - broadcast = async (channelName: string, eventName : string, message : string) => { + async broadcast(channelName: string, eventName: string, message: string) { await this.ablyClient.channels.get(channelName).publish(eventName, message); } - tokenInvalidOrExpired = (serverTime, token) => { + tokenInvalidOrExpired(serverTime, token) { const tokenInvalid = false; const { payload } = parseJwt(token); return tokenInvalid || payload.exp * 1000 <= serverTime; }; - getSignedToken = async (channelName : any = null, token : any = null) => { + async getSignedToken(channelName: any = null, token: any = null) { const header = { "typ": "JWT", "alg": "HS256", @@ -61,24 +61,26 @@ export class MockAuthServer { "x-ably-capability": JSON.stringify(capabilities) } claims = this.validateShortLivedOrBannedChannels(channelName, claims); - const response = { token : jwt.sign(claims, this.keySecret, { header })}; + const response = { token: jwt.sign(claims, this.keySecret, { header }) }; if (channelName && this.isPresenceChannel(channelName)) { - return {...response, info: this.userInfo} + return { ...response, info: this.userInfo } } return response; } - isPresenceChannel = channelName => channelName.startsWith("presence:"); - - setAuthExceptions = (shortLived : channels = [], banned : channels = []) => { + isPresenceChannel(channelName) { + return channelName.startsWith("presence:") + }; + + setAuthExceptions(shortLived: channels = [], banned: channels = []) { this.shortLived = shortLived; this.banned = banned; } - validateShortLivedOrBannedChannels = (channelName : string, claims : any) => { + validateShortLivedOrBannedChannels(channelName: string, claims: any) { if (this.shortLived?.includes(channelName)) { const exp = claims.iat + 3; // if channel is shortlived, token expiry set to 3 seconds - return {...claims, exp }; + return { ...claims, exp }; } if (this.banned?.includes(channelName)) { throw new Error(`User can't be authenticated for ${channelName}`); From 708293e464d83c427fae7658bfe6b86a33b158eb Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 26 Jul 2022 01:19:52 +0530 Subject: [PATCH 107/111] Revert "Updated tests with class methods" This reverts commit 10120c13bd641e3cb1caa89b7a9f79bf1cd5054f. --- tests/ably/setup/mock-auth-server.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index 75368928..52280460 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -9,7 +9,7 @@ export class MockAuthServer { keySecret: string; ablyClient: Ably.Rest; clientId = 'sacOO7@github.com' - userInfo = { id: 'sacOO7@github.com', name: 'sacOO7' } + userInfo = {id : 'sacOO7@github.com', name: 'sacOO7'} shortLived: channels; banned: channels; @@ -18,20 +18,20 @@ export class MockAuthServer { const keys = apiKey.split(':'); this.keyName = keys[0]; this.keySecret = keys[1]; - this.ablyClient = new Ably.Rest({ key: apiKey, environment }); + this.ablyClient = new Ably.Rest({key: apiKey, environment}); } - async broadcast(channelName: string, eventName: string, message: string) { + broadcast = async (channelName: string, eventName : string, message : string) => { await this.ablyClient.channels.get(channelName).publish(eventName, message); } - tokenInvalidOrExpired(serverTime, token) { + tokenInvalidOrExpired = (serverTime, token) => { const tokenInvalid = false; const { payload } = parseJwt(token); return tokenInvalid || payload.exp * 1000 <= serverTime; }; - async getSignedToken(channelName: any = null, token: any = null) { + getSignedToken = async (channelName : any = null, token : any = null) => { const header = { "typ": "JWT", "alg": "HS256", @@ -61,26 +61,24 @@ export class MockAuthServer { "x-ably-capability": JSON.stringify(capabilities) } claims = this.validateShortLivedOrBannedChannels(channelName, claims); - const response = { token: jwt.sign(claims, this.keySecret, { header }) }; + const response = { token : jwt.sign(claims, this.keySecret, { header })}; if (channelName && this.isPresenceChannel(channelName)) { - return { ...response, info: this.userInfo } + return {...response, info: this.userInfo} } return response; } - isPresenceChannel(channelName) { - return channelName.startsWith("presence:") - }; - - setAuthExceptions(shortLived: channels = [], banned: channels = []) { + isPresenceChannel = channelName => channelName.startsWith("presence:"); + + setAuthExceptions = (shortLived : channels = [], banned : channels = []) => { this.shortLived = shortLived; this.banned = banned; } - validateShortLivedOrBannedChannels(channelName: string, claims: any) { + validateShortLivedOrBannedChannels = (channelName : string, claims : any) => { if (this.shortLived?.includes(channelName)) { const exp = claims.iat + 3; // if channel is shortlived, token expiry set to 3 seconds - return { ...claims, exp }; + return {...claims, exp }; } if (this.banned?.includes(channelName)) { throw new Error(`User can't be authenticated for ${channelName}`); From a1facdd0006c9fdc4552c8284639ed37b1448345 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 26 Jul 2022 19:08:46 +0530 Subject: [PATCH 108/111] Fixed subscribe/unsubscribe modified callback issue --- src/channel/ably-channel.ts | 25 +++++++++++++++++-------- tests/ably/ably-channel.test.ts | 12 ++++++------ tests/ably/ably-private-channel.test.ts | 2 +- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/channel/ably-channel.ts b/src/channel/ably-channel.ts index 17833719..5c8b86e4 100644 --- a/src/channel/ably-channel.ts +++ b/src/channel/ably-channel.ts @@ -41,6 +41,11 @@ export class AblyChannel extends Channel { */ errorListeners: Function[]; + /** + * Channel event subscribe callbacks, maps callback to modified implementation. + */ + callbacks: Map; + /** * Create a new class instance. */ @@ -54,6 +59,7 @@ export class AblyChannel extends Channel { this.subscribedListeners = []; this.errorListeners = []; this.channel = ably.channels.get(name); + this.callbacks = new Map(); if (autoSubscribe) { this.subscribe(); @@ -80,6 +86,7 @@ export class AblyChannel extends Channel { */ unsubscribe(): void { this.channel.unsubscribe(); + this.callbacks.clear(); this.unregisterError(); this.unregisterSubscribed(); this.channel.off(); @@ -89,9 +96,9 @@ export class AblyChannel extends Channel { /** * Listen for an event on the channel instance. */ - listen(event: string, callback: Function | any): AblyChannel { - this.channel.subscribe(this.eventFormatter.format(event), callback); - + listen(event: string, callback: Function): AblyChannel { + this.callbacks.set(callback, ({ data, ...metaData }) => callback(data, metaData)); + this.channel.subscribe(this.eventFormatter.format(event), this.callbacks.get(callback) as any); return this; } @@ -99,14 +106,14 @@ export class AblyChannel extends Channel { * Listen for all events on the channel instance. */ listenToAll(callback: Function): AblyChannel { - this.channel.subscribe(({ name, data }) => { + this.callbacks.set(callback, ({ name, data, ...metaData }) => { let namespace = this.options.namespace.replace(/\./g, '\\'); let formattedEvent = name.startsWith(namespace) ? name.substring(namespace.length + 1) : '.' + name; - callback(formattedEvent, data); + callback(formattedEvent, data, metaData); }); - + this.channel.subscribe(this.callbacks.get(callback) as any); return this; } @@ -115,7 +122,8 @@ export class AblyChannel extends Channel { */ stopListening(event: string, callback?: Function): AblyChannel { if (callback) { - this.channel.unsubscribe(this.eventFormatter.format(event), callback as any); + this.channel.unsubscribe(this.eventFormatter.format(event), this.callbacks.get(callback) as any); + this.callbacks.delete(callback); } else { this.channel.unsubscribe(this.eventFormatter.format(event)); } @@ -128,7 +136,8 @@ export class AblyChannel extends Channel { */ stopListeningToAll(callback?: Function): AblyChannel { if (callback) { - this.channel.unsubscribe(callback as any); + this.channel.unsubscribe(this.callbacks.get(callback) as any); + this.callbacks.delete(callback); } else { this.channel.unsubscribe(); } diff --git a/tests/ably/ably-channel.test.ts b/tests/ably/ably-channel.test.ts index f0df625b..a2aae809 100644 --- a/tests/ably/ably-channel.test.ts +++ b/tests/ably/ably-channel.test.ts @@ -50,7 +50,7 @@ describe('AblyChannel', () => { .subscribed(() => { mockAuthServer.broadcast('public:test', 'App\\Events\\testEvent', 'Hello there'); }) - .listen('testEvent', ({ data }) => { + .listen('testEvent', data => { safeAssert(() => expect(data).toBe('Hello there'), done, true); }); }); @@ -62,7 +62,7 @@ describe('AblyChannel', () => { .subscribed(() => { mockAuthServer.broadcast('public:test', 'testEvent', 'Hello there'); }) - .listen('.testEvent', ({ data }) => { + .listen('.testEvent', data => { safeAssert(() => expect(data).toBe('Hello there'), done, true); }); }); @@ -73,7 +73,7 @@ describe('AblyChannel', () => { .subscribed(() => { mockAuthServer.broadcast('public:test', 'client-msg', 'Hello there'); }) - .listenForWhisper('msg', ({ data }) => { + .listenForWhisper('msg', data => { safeAssert(() => expect(data).toBe('Hello there'), done, true); }); }) @@ -85,7 +85,7 @@ describe('AblyChannel', () => { .subscribed(() => { mockAuthServer.broadcast('public:test', 'Illuminate\\Notifications\\Events\\BroadcastNotificationCreated', 'Hello there'); }) - .notification(({ data }) => { + .notification( data => { safeAssert(() => expect(data).toBe('Hello there'), done, true); }); }) @@ -169,7 +169,7 @@ describe('AblyChannel', () => { execute(() => mockAuthServer.broadcast('public:test', 'client-msg', 'Hello there'), 4); execute(() => mockAuthServer.broadcast('public:test', 'client-msg2', 'Hello there'), 1); - + await waitForExpect(() => { expect(eventHandler1).toBeCalledTimes(4); expect(eventHandler2).toBeCalledTimes(4); @@ -181,7 +181,7 @@ describe('AblyChannel', () => { execute(() => mockAuthServer.broadcast('public:test', 'client-msg', 'Hello there'), 3); execute(() => mockAuthServer.broadcast('public:test', 'client-msg2', 'Hello there'), 2); - + await waitForExpect(() => { expect(eventHandler1).toBeCalledTimes(0); expect(eventHandler2).toBeCalledTimes(3); diff --git a/tests/ably/ably-private-channel.test.ts b/tests/ably/ably-private-channel.test.ts index 9d705e5d..e658888d 100644 --- a/tests/ably/ably-private-channel.test.ts +++ b/tests/ably/ably-private-channel.test.ts @@ -51,7 +51,7 @@ describe('AblyPrivateChannel', () => { .subscribed(() => { privateChannel.whisper('msg', 'Hello there jonny!'); }) - .listenForWhisper('msg', ({ data }) => { + .listenForWhisper('msg', data => { safeAssert(() => expect(data).toBe('Hello there jonny!'), done, true); }); }) From 32b5b506a031e5753b6b153d66819642a38bb254 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 26 Jul 2022 19:51:52 +0530 Subject: [PATCH 109/111] Updated presence callbacks for joining and leaving to return published data --- src/channel/ably-presence-channel.ts | 8 ++++---- tests/ably/ably-presence-channel.test.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/channel/ably-presence-channel.ts b/src/channel/ably-presence-channel.ts index 913b3540..6fed1e3e 100644 --- a/src/channel/ably-presence-channel.ts +++ b/src/channel/ably-presence-channel.ts @@ -36,8 +36,8 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel * Listen for someone joining the channel. */ joining(callback: Function): AblyPresenceChannel { - this.channel.presence.subscribe(['enter', 'update'], member => { - callback(member); + this.channel.presence.subscribe(['enter', 'update'], ({ data, ...metaData }) => { + callback(data, metaData); }); return this; @@ -47,8 +47,8 @@ export class AblyPresenceChannel extends AblyChannel implements PresenceChannel * Listen for someone leaving the channel. */ leaving(callback: Function): AblyPresenceChannel { - this.channel.presence.subscribe('leave', member => { - callback(member); + this.channel.presence.subscribe('leave', ({ data, ...metaData }) => { + callback(data, metaData); }); return this; diff --git a/tests/ably/ably-presence-channel.test.ts b/tests/ably/ably-presence-channel.test.ts index 85724742..5c7ad524 100644 --- a/tests/ably/ably-presence-channel.test.ts +++ b/tests/ably/ably-presence-channel.test.ts @@ -58,20 +58,20 @@ describe('AblyPresenceChannel', () => { test('member joined', done => { const presenceChannel = echo.join('test') as AblyPresenceChannel; - presenceChannel.joining(member => { + presenceChannel.joining((memberData, memberMetaData) => { safeAssert(() => { - expect(member.clientId).toBe('sacOO7@github.com'); - expect(member.data).toStrictEqual({ id: 'sacOO7@github.com', name: 'sacOO7' }); + expect(memberData).toStrictEqual({ id: 'sacOO7@github.com', name: 'sacOO7' }); + expect(memberMetaData.clientId).toBe('sacOO7@github.com'); }, done, true); }) }) test('member left', done => { const presenceChannel = echo.join('test') as AblyPresenceChannel; - presenceChannel.leaving(member => { + presenceChannel.leaving((memberData, memberMetaData) => { safeAssert(() => { - expect(member.clientId).toBe('sacOO7@github.com'); - expect(member.data).toStrictEqual({ name: 'sacOO7 leaving the channel' }); + expect(memberData).toStrictEqual({ name: 'sacOO7 leaving the channel' }); + expect(memberMetaData.clientId).toBe('sacOO7@github.com'); }, done, true); }); presenceChannel.joining(()=> presenceChannel.leave({name: 'sacOO7 leaving the channel'})); From d23beca0834ad7601c21e8b2cbf9be8c6f3c761f Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 26 Jul 2022 23:27:16 +0530 Subject: [PATCH 110/111] Fixed linting errors from the code --- src/channel/ably/attach.ts | 6 +++--- src/channel/ably/auth.ts | 16 +++++++++------- src/channel/ably/token-request.ts | 6 +++--- tests/ably/ably-presence-channel.test.ts | 1 + tests/ably/setup/sandbox.ts | 4 ++-- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/channel/ably/attach.ts b/src/channel/ably/attach.ts index 56dae3df..d69d5b7a 100644 --- a/src/channel/ably/attach.ts +++ b/src/channel/ably/attach.ts @@ -1,13 +1,13 @@ import { isNullOrUndefined } from './utils'; -let channelAttachPatched = false; +let channelAttachAuthorized = false; /** * Modifies existing channel attach with custom authz implementation */ export const beforeChannelAttach = (ablyClient, authorize: Function) => { const dummyRealtimeChannel = ablyClient.channels.get("dummy"); - if (channelAttachPatched) { //Only once all ably instance + if (channelAttachAuthorized) { //Only once all ably instance return; } const internalAttach = dummyRealtimeChannel.__proto__._attach; // get parent class method inferred from object, store it in temp. variable @@ -33,5 +33,5 @@ export const beforeChannelAttach = (ablyClient, authorize: Function) => { }) } dummyRealtimeChannel.__proto__._attach = customInternalAttach; // add updated extension method to parent class, auto binded - channelAttachPatched = true; + channelAttachAuthorized = true; } diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index 251a27aa..ba128dec 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -29,8 +29,8 @@ export class AblyAuth { } async requestToken(channelName: string, existingToken: string) { - var postData = JSON.stringify({ channel_name: channelName, token: existingToken }); - var postOptions = { + let postData = JSON.stringify({ channel_name: channelName, token: existingToken }); + let postOptions = { host: this.authHost, port: this.authPort, path: this.authEndpoint, @@ -59,22 +59,24 @@ export class AblyAuth { const { token, requestTokenFn, authEndpoint, authHost, authPort, authProtocol } = options; if (authEndpoint) { this.authEndpoint = authEndpoint; - }; + } if (authHost) { this.authHost = authHost; - }; + } if (authPort) { this.authPort = authPort; - }; + } if (authProtocol) { this.authProtocol = authProtocol; - }; + } this.authRequestExecuter = new SequentialAuthTokenRequestExecuter(token, requestTokenFn ?? this.requestToken); } enableAuthorizeBeforeChannelAttach(ablyConnector: AblyConnector) { const ablyClient: any = ablyConnector.ably; - ablyClient.auth.getTimestamp(this.authOptions.queryTime, () => { }); // generates serverTimeOffset in the background + ablyClient.auth.getTimestamp(this.authOptions.queryTime, () => { + // do nothing. + }); // generates serverTimeOffset in the background beforeChannelAttach(ablyClient, (realtimeChannel, errorCallback) => { const channelName = realtimeChannel.name; if (channelName.startsWith("public:")) { diff --git a/src/channel/ably/token-request.ts b/src/channel/ably/token-request.ts index 9a39aef8..c8266eef 100644 --- a/src/channel/ably/token-request.ts +++ b/src/channel/ably/token-request.ts @@ -10,8 +10,8 @@ export class SequentialAuthTokenRequestExecuter { } execute(tokenRequestFn: Function): Promise<{ token: string, info: any }> { - return new Promise(async (resolve, reject) => { - await this.queue.run(async () => { + return new Promise((resolve, reject) => { + this.queue.run(async () => { try { const { token, info } = await tokenRequestFn(this.cachedToken); this.cachedToken = token; @@ -44,7 +44,7 @@ class TaskQueue { canRunNext() { return (this.running.length < this.count) && this.todo.length; - }; + } async run(task: Task) { if (task) { diff --git a/tests/ably/ably-presence-channel.test.ts b/tests/ably/ably-presence-channel.test.ts index 5c7ad524..5f7bb89d 100644 --- a/tests/ably/ably-presence-channel.test.ts +++ b/tests/ably/ably-presence-channel.test.ts @@ -49,6 +49,7 @@ describe('AblyPresenceChannel', () => { const presenceChannel = echo.join('test') as AblyPresenceChannel; presenceChannel.here((members, err) => { safeAssert(() => { + expect(err).toBeFalsy(); expect(members).toHaveLength(1); expect(members[0].clientId).toBe('sacOO7@github.com'); expect(members[0].data).toStrictEqual({ id: 'sacOO7@github.com', name: 'sacOO7' }); diff --git a/tests/ably/setup/sandbox.ts b/tests/ably/setup/sandbox.ts index d008eee5..38b4b6c5 100644 --- a/tests/ably/setup/sandbox.ts +++ b/tests/ably/setup/sandbox.ts @@ -3,7 +3,7 @@ import got from 'got'; let sandboxUrl = 'https://sandbox-rest.ably.io/apps'; const creatNewApp = async () => { - var body = { + let body = { "keys": [{}], "namespaces": [] }; @@ -15,7 +15,7 @@ const creatNewApp = async () => { } const deleteApp = async (app) => { - var authKey = app.keys[0].keyStr; + let authKey = app.keys[0].keyStr; return got.delete(`${sandboxUrl}/${app.appId}?key=${authKey}`); } From a6c4c90db6cf97c882c4fcc76d0c4721e4c40cc5 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 27 Jul 2022 01:16:21 +0530 Subject: [PATCH 111/111] Updated auth and token-request to use class methods instead of functions --- src/channel/ably/auth.ts | 16 +++++++-------- src/channel/ably/token-request.ts | 33 +++++++++++++------------------ 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/channel/ably/auth.ts b/src/channel/ably/auth.ts index ba128dec..79d3f926 100644 --- a/src/channel/ably/auth.ts +++ b/src/channel/ably/auth.ts @@ -14,7 +14,7 @@ export class AblyAuth { authPort = typeof window != 'undefined' && window?.location?.port; authProtocol = typeof window != 'undefined' && window?.location?.protocol.replace(':', ''); - authOptions : AuthOptions = { + authOptions: AuthOptions = { queryTime: true, useTokenAuth: true, authCallback: async (_, callback) => { @@ -28,7 +28,7 @@ export class AblyAuth { } } - async requestToken(channelName: string, existingToken: string) { + requestToken = async (channelName: string, existingToken: string) => { let postData = JSON.stringify({ channel_name: channelName, token: existingToken }); let postOptions = { host: this.authHost, @@ -72,9 +72,9 @@ export class AblyAuth { this.authRequestExecuter = new SequentialAuthTokenRequestExecuter(token, requestTokenFn ?? this.requestToken); } - enableAuthorizeBeforeChannelAttach(ablyConnector: AblyConnector) { + enableAuthorizeBeforeChannelAttach = (ablyConnector: AblyConnector) => { const ablyClient: any = ablyConnector.ably; - ablyClient.auth.getTimestamp(this.authOptions.queryTime, () => { + ablyClient.auth.getTimestamp(this.authOptions.queryTime, () => { // do nothing. }); // generates serverTimeOffset in the background beforeChannelAttach(ablyClient, (realtimeChannel, errorCallback) => { @@ -110,11 +110,9 @@ export class AblyAuth { }); } - onChannelFailed(echoAblyChannel: AblyChannel) { - return (stateChange: ChannelStateChange) => { - if (stateChange.reason?.code == 40160) { // channel capability rejected https://help.ably.io/error/40160 - this.handleChannelAuthError(echoAblyChannel); - } + onChannelFailed = (echoAblyChannel: AblyChannel) => (stateChange: ChannelStateChange) => { + if (stateChange.reason?.code == 40160) { // channel capability rejected https://help.ably.io/error/40160 + this.handleChannelAuthError(echoAblyChannel); } } diff --git a/src/channel/ably/token-request.ts b/src/channel/ably/token-request.ts index c8266eef..ae81a550 100644 --- a/src/channel/ably/token-request.ts +++ b/src/channel/ably/token-request.ts @@ -9,23 +9,20 @@ export class SequentialAuthTokenRequestExecuter { this.queue = new TaskQueue(); } - execute(tokenRequestFn: Function): Promise<{ token: string, info: any }> { - return new Promise((resolve, reject) => { - this.queue.run(async () => { - try { - const { token, info } = await tokenRequestFn(this.cachedToken); - this.cachedToken = token; - resolve({ token, info }); - } catch (err) { - reject(err); - } - }) + execute = (tokenRequestFn: Function): Promise<{ token: string, info: any }> => new Promise((resolve, reject) => { + this.queue.run(async () => { + try { + const { token, info } = await tokenRequestFn(this.cachedToken); + this.cachedToken = token; + resolve({ token, info }); + } catch (err) { + reject(err); + } }) - } + }) + + request = (channelName: string): Promise<{ token: string, info: any }> => this.execute(token => this.requestTokenFn(channelName, token)); - request(channelName: string): Promise<{ token: string, info: any }> { - return this.execute(token => this.requestTokenFn(channelName, token)); - } } type Task = Function; @@ -42,11 +39,9 @@ class TaskQueue { this.count = concurrentCount; } - canRunNext() { - return (this.running.length < this.count) && this.todo.length; - } + canRunNext = () => (this.running.length < this.count) && this.todo.length; - async run(task: Task) { + run = async (task: Task) => { if (task) { this.todo.push(task); }