From fbb8b6343167250927a2f28d8f6d949a2603c26f Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 20 Sep 2024 17:48:20 +0530 Subject: [PATCH 01/11] Updated socketId method to return base64 encoded connectionKey and clientId --- src/channel/ably/utils.ts | 9 ++++++++- src/connector/ably-connector.ts | 8 +++++++- src/echo.ts | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 04d9316c..4fa68b9e 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -33,13 +33,20 @@ export const toTokenDetails = (jwtToken: string): TokenDetails | any => { const isBrowser = typeof window === 'object'; -const toText = (base64: string) => { +export const toText = (base64: string) => { if (isBrowser) { return atob(base64); } return Buffer.from(base64, 'base64').toString('binary'); }; +export const toBase64 = (text: string) => { + if (isBrowser) { + return btoa(text); + } + return Buffer.from(text, 'binary').toString('base64'); +}; + const isAbsoluteUrl = (url: string) => (url && url.indexOf('http://') === 0) || url.indexOf('https://') === 0; export const fullUrl = (url: string) => { diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index f4a0a3c1..01b09911 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -2,6 +2,7 @@ import { Connector } from './connector'; import { AblyChannel, AblyPrivateChannel, AblyPresenceChannel, AblyAuth } from './../channel'; import { AblyRealtime, TokenDetails } from '../../typings/ably'; +import { toBase64 } from '../channel/ably/utils'; /** * This class creates a connector to Ably. @@ -118,9 +119,14 @@ export class AblyConnector extends Connector { /** * Get the socket ID for the connection. + * For ably, returns base64 encoded json with keys {connectionKey, clientId} */ socketId(): string { - return this.ably.connection.key; + let socketIdObject = { + connectioKey : this.ably.connection.key, + clientId : this.ably.auth.clientId, + } + return toBase64(JSON.stringify(socketIdObject)); } /** diff --git a/src/echo.ts b/src/echo.ts index 5e63590e..fd3f492f 100644 --- a/src/echo.ts +++ b/src/echo.ts @@ -117,6 +117,7 @@ export default class Echo { /** * Get the Socket ID for the connection. + * For ably, returns base64 encoded json with keys {connectionKey, clientId} */ socketId(): string { return this.connector.socketId(); From 50fdae419a71a78a511ec8b34539a78efa547e91 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 20 Sep 2024 23:22:33 +0530 Subject: [PATCH 02/11] Refactored mock-auth-server, included method to broadcast to others --- tests/ably/ably-user-login.test.ts | 4 ++-- tests/ably/setup/mock-auth-server.ts | 27 ++++++++++++++++++++++++--- tests/ably/setup/utils.ts | 5 ++--- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/tests/ably/ably-user-login.test.ts b/tests/ably/ably-user-login.test.ts index 2e986028..77ec69bf 100644 --- a/tests/ably/ably-user-login.test.ts +++ b/tests/ably/ably-user-login.test.ts @@ -39,7 +39,7 @@ describe('AblyUserLogin', () => { }); }); - test('user logs in without previous (guest) channels', async () => { + test('user logs in without previous (public) channels', async () => { let connectionStates : Array= [] // Initial clientId is null expect(mockAuthServer.clientId).toBeNull(); @@ -70,7 +70,7 @@ describe('AblyUserLogin', () => { expect(echo.connector.ablyAuth.existingToken().clientId).toBe('sacOO7@github.com'); }); - test('user logs in with previous (guest) channels', async () => { + test('user logs in with previous (public) channels', async () => { let connectionStates : Array= [] let publicChannelStates : Array= [] diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index 8d244a6d..c2f806b0 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -1,19 +1,25 @@ -import { isNullOrUndefinedOrEmpty, parseJwt } from '../../../src/channel/ably/utils'; +import { isNullOrUndefinedOrEmpty, parseJwt, toBase64, toText } from '../../../src/channel/ably/utils'; import * as Ably from 'ably/promises'; import * as jwt from 'jsonwebtoken'; type channels = Array; +/** + * MockAuthServer mimicks {@link https://github.com/ably/laravel-broadcaster/blob/main/src/AblyBroadcaster.php AblyBroadcaster.php}. + * Aim is to keep implementation and behaviour in sync with AblyBroadcaster.php, so that it can be tested + * without running actual PHP server. + */ export class MockAuthServer { keyName: string; keySecret: string; ablyClient: Ably.Rest; clientId: string | null = 'sacOO7@github.com'; - userInfo = { id: 'sacOO7@github.com', name: 'sacOO7' }; shortLived: channels; banned: channels; + userInfo = { id: 'sacOO7@github.com', name: 'sacOO7' }; // Used for presence + constructor(apiKey: string, environment = 'sandbox') { const keys = apiKey.split(':'); this.keyName = keys[0]; @@ -21,8 +27,23 @@ export class MockAuthServer { this.ablyClient = new Ably.Rest({ key: apiKey, environment }); } + /** + * Broadcast to all clients subscribed to given channel. + */ broadcast = async (channelName: string, eventName: string, message: string) => { - await this.ablyClient.channels.get(channelName).publish(eventName, message); + await this.broadcastToOthers("", {channelName, eventName, payload: message}); + }; + + /** + * Broadcast on behalf of a given realtime client. + */ + broadcastToOthers = async (socketId: string, {channelName, eventName, payload}) => { + let protoMsg = {name: eventName, data: payload}; + if (!isNullOrUndefinedOrEmpty(socketId)) { + const socketIdObj = JSON.parse(toText(socketId)); + protoMsg = {...protoMsg, ...socketIdObj} + } + await this.ablyClient.channels.get(channelName).publish(protoMsg); }; tokenInvalidOrExpired = (serverTime, token) => { diff --git a/tests/ably/setup/utils.ts b/tests/ably/setup/utils.ts index 9213fde1..5d9a964c 100644 --- a/tests/ably/setup/utils.ts +++ b/tests/ably/setup/utils.ts @@ -1,4 +1,5 @@ -import { httpRequestAsync } from '../../../src/channel/ably/utils'; +import { httpRequestAsync} from '../../../src/channel/ably/utils'; +export { toBase64 } from '../../../src/channel/ably/utils'; const safeAssert = (assertions: Function, done: Function, finalAssertion = false) => { try { @@ -17,8 +18,6 @@ export const execute = (fn: Function, times: number) => { } }; -export const toBase64 = (text: string) => Buffer.from(text, 'binary').toString('base64'); - export const httpPostAsync = async (url: string, postData: any) => { postData = JSON.stringify(postData); let postOptions = { From 77644e633ea4ac09613537fb227594c374e0b636 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Sun, 22 Sep 2024 17:46:47 +0530 Subject: [PATCH 03/11] Fixed socketId method to return properly encoded connectionKey and clientId keys --- src/connector/ably-connector.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index 01b09911..253f8bcb 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -123,8 +123,8 @@ export class AblyConnector extends Connector { */ socketId(): string { let socketIdObject = { - connectioKey : this.ably.connection.key, - clientId : this.ably.auth.clientId, + connectionKey : this.ably.connection.key, + clientId : this.ably.auth.clientId ?? null, } return toBase64(JSON.stringify(socketIdObject)); } From 1d69777a87e0267b60995925bc13c9b3b1d17621 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Sun, 22 Sep 2024 17:48:24 +0530 Subject: [PATCH 04/11] Added tests to check for guest and logged in user behaviour for rest publishing --- tests/ably/ably-user-rest-publishing.test.ts | 167 +++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 tests/ably/ably-user-rest-publishing.test.ts diff --git a/tests/ably/ably-user-rest-publishing.test.ts b/tests/ably/ably-user-rest-publishing.test.ts new file mode 100644 index 00000000..d6cf91b3 --- /dev/null +++ b/tests/ably/ably-user-rest-publishing.test.ts @@ -0,0 +1,167 @@ +import { setup, tearDown } from './setup/sandbox'; +import Echo from '../../src/echo'; +import { MockAuthServer } from './setup/mock-auth-server'; +import { AblyChannel, AblyPrivateChannel } from '../../src/channel'; +import * as Ably from 'ably'; +import waitForExpect from 'wait-for-expect'; +import { toText } from '../../src/channel/ably/utils'; + +jest.setTimeout(30000); +describe('AblyUserRestPublishing', () => { + let testApp: any; + let mockAuthServer: MockAuthServer; + let echoInstanes: Array; + + beforeAll(async () => { + global.Ably = Ably; + testApp = await setup(); + mockAuthServer = new MockAuthServer(testApp.keys[0].keyStr); + }); + + afterAll(async () => { + return await tearDown(testApp); + }); + + beforeEach(() => { + echoInstanes = []; + }) + + afterEach((done) => { + let promises: Array> = [] + for (const echo of echoInstanes) { + echo.disconnect(); + const promise = new Promise(res => { + echo.connector.ably.connection.once('closed', () => { + res(true); + }); + }) + promises.push(promise); + } + Promise.all(promises).then(_ => { + done(); + }) + }); + + async function getGuestUserChannel(channelName: string) { + mockAuthServer.clientId = null; + const guestUser = new Echo({ + broadcaster: 'ably', + useTls: true, + environment: 'sandbox', + requestTokenFn: mockAuthServer.getSignedToken + }); + echoInstanes.push(guestUser); + const publicChannel = guestUser.channel(channelName) as AblyChannel; + await new Promise((resolve) => publicChannel.subscribed(resolve)); + expect(guestUser.connector.ably.auth.clientId).toBeFalsy(); + expect(guestUser.connector.ablyAuth.existingToken().clientId).toBeNull(); + return publicChannel; + } + + async function getLoggedInUserChannel(channelName: string) { + mockAuthServer.clientId = 'sacOO7@github.com'; + const loggedInUser = new Echo({ + broadcaster: 'ably', + useTls: true, + environment: 'sandbox', + requestTokenFn: mockAuthServer.getSignedToken + }); + echoInstanes.push(loggedInUser); + const privateChannel = loggedInUser.private(channelName) as AblyPrivateChannel; + await new Promise((resolve) => privateChannel.subscribed(resolve)); + expect(loggedInUser.connector.ably.auth.clientId).toBe("sacOO7@github.com"); + expect(loggedInUser.connector.ablyAuth.existingToken().clientId).toBe("sacOO7@github.com") + mockAuthServer.clientId = null; + return privateChannel; + } + + test('Guest user return socketId as base64 encoded connectionkey and null clientId', async () => { + await getGuestUserChannel("dummyChannel"); + const guestUser = echoInstanes[0]; + const socketIdObj = JSON.parse(toText(guestUser.socketId())); + + const expectedConnectionKey = guestUser.connector.ably.connection.key; + + expect(socketIdObj.connectionKey).toBe(expectedConnectionKey); + expect(socketIdObj.connectionKey).toBeTruthy(); + + expect(socketIdObj.clientId).toBeNull(); + }); + + test('Guest user publishes message via rest API', async () => { + let messagesReceived: Array = [] + let channelName = "testChannel"; + + const publicChannel1 = await getGuestUserChannel(channelName); + publicChannel1.listenToAll((eventName, data) => { + messagesReceived.push(eventName); + }); + + const publicChannel2 = await getGuestUserChannel(channelName); + publicChannel2.listenToAll((eventName, data) => { + messagesReceived.push(eventName); + }) + + // Publish message to all clients + await mockAuthServer.broadcast(`public:${channelName}`, "testEvent", "mydata") + await waitForExpect(() => { + expect(messagesReceived.length).toBe(2); + expect(messagesReceived.filter(m => m == ".testEvent").length).toBe(2) + }); + + // Publish message to other client + messagesReceived = [] + const firstClientSocketId = echoInstanes[0].socketId(); + await mockAuthServer.broadcastToOthers(firstClientSocketId, + { channelName: `public:${channelName}`, eventName: "toOthers", payload: "data" }) + await waitForExpect(() => { + expect(messagesReceived.length).toBe(1); + expect(messagesReceived.filter(m => m == ".toOthers").length).toBe(1); + }); + }); + + test('Logged in user return socketId as base64 encoded connectionkey and clientId', async () => { + await getLoggedInUserChannel("dummyChannel"); + const loggedInUser = echoInstanes[0]; + const socketIdObj = JSON.parse(toText(loggedInUser.socketId())); + + const expectedConnectionKey = loggedInUser.connector.ably.connection.key; + + expect(socketIdObj.connectionKey).toBe(expectedConnectionKey); + expect(socketIdObj.connectionKey).toBeTruthy(); + + expect(socketIdObj.clientId).toBe("sacOO7@github.com"); + }); + + test('Logged in user publishes message via rest API', async () => { + let messagesReceived: Array = [] + let channelName = "testChannel"; + + const privateChannel1 = await getLoggedInUserChannel(channelName); + privateChannel1.listenToAll((eventName, data) => { + messagesReceived.push(eventName); + }); + + const privateChannel2 = await getLoggedInUserChannel(channelName); + privateChannel2.listenToAll((eventName, data) => { + messagesReceived.push(eventName); + }) + + // Publish message to all clients + await mockAuthServer.broadcast(`private:${channelName}`, "testEvent", "mydata") + await waitForExpect(() => { + expect(messagesReceived.length).toBe(2); + expect(messagesReceived.filter(m => m == ".testEvent").length).toBe(2); + }); + + // Publish message to other client + messagesReceived = [] + const firstClientSocketId = echoInstanes[0].socketId(); + await mockAuthServer.broadcastToOthers(firstClientSocketId, + { channelName: `private:${channelName}`, eventName: "toOthers", payload: "data" }); + await waitForExpect(() => { + expect(messagesReceived.length).toBe(1); + expect(messagesReceived.filter(m => m == ".toOthers").length).toBe(1); + }); + }); +}); From adfc1b7ee89740750a425ac6da06edb0d72c5782 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 24 Sep 2024 15:51:21 +0530 Subject: [PATCH 05/11] Modified toText and toBase64 helper methods to handle base64 url encoded strings --- src/channel/ably/utils.ts | 27 +++++++++++++++++++++++---- src/connector/ably-connector.ts | 2 +- src/echo.ts | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 4fa68b9e..9ad85db3 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -33,18 +33,37 @@ export const toTokenDetails = (jwtToken: string): TokenDetails | any => { const isBrowser = typeof window === 'object'; +/** + * Helper method to decode base64 url encoded string + * https://stackoverflow.com/a/78178053 + * @param base64 base64 url encoded string + * @returns decoded text string + */ export const toText = (base64: string) => { + const base64Encoded = base64.replace(/-/g, '+').replace(/_/g, '/'); + const padding = base64.length % 4 === 0 ? '' : '='.repeat(4 - (base64.length % 4)); + const base64WithPadding = base64Encoded + padding; + if (isBrowser) { - return atob(base64); + return atob(base64WithPadding); } - return Buffer.from(base64, 'base64').toString('binary'); + return Buffer.from(base64WithPadding, 'base64').toString('binary'); }; +/** + * Helper method to encode text into base64 url encoded string + * https://stackoverflow.com/a/78178053 + * @param base64 text + * @returns base64 url encoded string + */ export const toBase64 = (text: string) => { + let encoded = '' if (isBrowser) { - return btoa(text); + encoded = btoa(text); + } else { + encoded = Buffer.from(text, 'binary').toString('base64'); } - return Buffer.from(text, 'binary').toString('base64'); + return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }; const isAbsoluteUrl = (url: string) => (url && url.indexOf('http://') === 0) || url.indexOf('https://') === 0; diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index 253f8bcb..038757ec 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -119,7 +119,7 @@ export class AblyConnector extends Connector { /** * Get the socket ID for the connection. - * For ably, returns base64 encoded json with keys {connectionKey, clientId} + * For ably, returns base64 url encoded json with keys {connectionKey, clientId} */ socketId(): string { let socketIdObject = { diff --git a/src/echo.ts b/src/echo.ts index fd3f492f..d646fa32 100644 --- a/src/echo.ts +++ b/src/echo.ts @@ -117,7 +117,7 @@ export default class Echo { /** * Get the Socket ID for the connection. - * For ably, returns base64 encoded json with keys {connectionKey, clientId} + * For ably, returns base64 url encoded json with keys {connectionKey, clientId} */ socketId(): string { return this.connector.socketId(); From 3076d163b6a0c9214b650c3a4775321ac6d7d747 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 25 Sep 2024 16:16:31 +0530 Subject: [PATCH 06/11] Refactored helper methods with names containing base64UrlEncode as per review comment --- src/channel/ably/utils.ts | 8 ++++---- src/connector/ably-connector.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index 9ad85db3..ee833978 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -12,11 +12,11 @@ export const parseJwt = (jwtToken: string): { header: any; payload: any } => { // Get Token Header const base64HeaderUrl = jwtToken.split('.')[0]; const base64Header = base64HeaderUrl.replace('-', '+').replace('_', '/'); - const header = JSON.parse(toText(base64Header)); + const header = JSON.parse(fromBase64UrlEncoded(base64Header)); // Get Token payload const base64Url = jwtToken.split('.')[1]; const base64 = base64Url.replace('-', '+').replace('_', '/'); - const payload = JSON.parse(toText(base64)); + const payload = JSON.parse(fromBase64UrlEncoded(base64)); return { header, payload }; }; @@ -39,7 +39,7 @@ const isBrowser = typeof window === 'object'; * @param base64 base64 url encoded string * @returns decoded text string */ -export const toText = (base64: string) => { +export const fromBase64UrlEncoded = (base64: string) => { const base64Encoded = base64.replace(/-/g, '+').replace(/_/g, '/'); const padding = base64.length % 4 === 0 ? '' : '='.repeat(4 - (base64.length % 4)); const base64WithPadding = base64Encoded + padding; @@ -56,7 +56,7 @@ export const toText = (base64: string) => { * @param base64 text * @returns base64 url encoded string */ -export const toBase64 = (text: string) => { +export const toBase64UrlEncoded = (text: string) => { let encoded = '' if (isBrowser) { encoded = btoa(text); diff --git a/src/connector/ably-connector.ts b/src/connector/ably-connector.ts index 038757ec..cebb15ec 100644 --- a/src/connector/ably-connector.ts +++ b/src/connector/ably-connector.ts @@ -2,7 +2,7 @@ import { Connector } from './connector'; import { AblyChannel, AblyPrivateChannel, AblyPresenceChannel, AblyAuth } from './../channel'; import { AblyRealtime, TokenDetails } from '../../typings/ably'; -import { toBase64 } from '../channel/ably/utils'; +import { toBase64UrlEncoded } from '../channel/ably/utils'; /** * This class creates a connector to Ably. @@ -126,7 +126,7 @@ export class AblyConnector extends Connector { connectionKey : this.ably.connection.key, clientId : this.ably.auth.clientId ?? null, } - return toBase64(JSON.stringify(socketIdObject)); + return toBase64UrlEncoded(JSON.stringify(socketIdObject)); } /** From 7456e254f0c6faec3c68015728bc88def413a0bc Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 25 Sep 2024 17:24:50 +0530 Subject: [PATCH 07/11] Fixed base64Encode and base64Decode usage across tests --- tests/ably/ably-user-rest-publishing.test.ts | 6 +++--- tests/ably/setup/mock-auth-server.ts | 4 ++-- tests/ably/setup/sandbox.ts | 4 ++-- tests/ably/setup/utils.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/ably/ably-user-rest-publishing.test.ts b/tests/ably/ably-user-rest-publishing.test.ts index d6cf91b3..b5349d69 100644 --- a/tests/ably/ably-user-rest-publishing.test.ts +++ b/tests/ably/ably-user-rest-publishing.test.ts @@ -4,7 +4,7 @@ import { MockAuthServer } from './setup/mock-auth-server'; import { AblyChannel, AblyPrivateChannel } from '../../src/channel'; import * as Ably from 'ably'; import waitForExpect from 'wait-for-expect'; -import { toText } from '../../src/channel/ably/utils'; +import { fromBase64UrlEncoded } from '../../src/channel/ably/utils'; jest.setTimeout(30000); describe('AblyUserRestPublishing', () => { @@ -78,7 +78,7 @@ describe('AblyUserRestPublishing', () => { test('Guest user return socketId as base64 encoded connectionkey and null clientId', async () => { await getGuestUserChannel("dummyChannel"); const guestUser = echoInstanes[0]; - const socketIdObj = JSON.parse(toText(guestUser.socketId())); + const socketIdObj = JSON.parse(fromBase64UrlEncoded(guestUser.socketId())); const expectedConnectionKey = guestUser.connector.ably.connection.key; @@ -123,7 +123,7 @@ describe('AblyUserRestPublishing', () => { test('Logged in user return socketId as base64 encoded connectionkey and clientId', async () => { await getLoggedInUserChannel("dummyChannel"); const loggedInUser = echoInstanes[0]; - const socketIdObj = JSON.parse(toText(loggedInUser.socketId())); + const socketIdObj = JSON.parse(fromBase64UrlEncoded(loggedInUser.socketId())); const expectedConnectionKey = loggedInUser.connector.ably.connection.key; diff --git a/tests/ably/setup/mock-auth-server.ts b/tests/ably/setup/mock-auth-server.ts index c2f806b0..940a8589 100644 --- a/tests/ably/setup/mock-auth-server.ts +++ b/tests/ably/setup/mock-auth-server.ts @@ -1,4 +1,4 @@ -import { isNullOrUndefinedOrEmpty, parseJwt, toBase64, toText } from '../../../src/channel/ably/utils'; +import { isNullOrUndefinedOrEmpty, parseJwt, fromBase64UrlEncoded } from '../../../src/channel/ably/utils'; import * as Ably from 'ably/promises'; import * as jwt from 'jsonwebtoken'; @@ -40,7 +40,7 @@ export class MockAuthServer { broadcastToOthers = async (socketId: string, {channelName, eventName, payload}) => { let protoMsg = {name: eventName, data: payload}; if (!isNullOrUndefinedOrEmpty(socketId)) { - const socketIdObj = JSON.parse(toText(socketId)); + const socketIdObj = JSON.parse(fromBase64UrlEncoded(socketId)); protoMsg = {...protoMsg, ...socketIdObj} } await this.ablyClient.channels.get(channelName).publish(protoMsg); diff --git a/tests/ably/setup/sandbox.ts b/tests/ably/setup/sandbox.ts index a6884d9a..de9be9a3 100644 --- a/tests/ably/setup/sandbox.ts +++ b/tests/ably/setup/sandbox.ts @@ -1,4 +1,4 @@ -import { httpDeleteAsync, httpPostAsync, toBase64 } from './utils'; +import { httpDeleteAsync, httpPostAsync, toBase64UrlEncoded } from './utils'; let sandboxUrl = 'https://sandbox-rest.ably.io/apps'; @@ -16,7 +16,7 @@ const creatNewApp = async () => { const deleteApp = async (app) => { let authKey = app.keys[0].keyStr; - const headers = { Authorization: 'Basic ' + toBase64(authKey) }; + const headers = { Authorization: 'Basic ' + toBase64UrlEncoded(authKey) }; return await httpDeleteAsync(`${sandboxUrl}/${app.appId}`, headers); }; diff --git a/tests/ably/setup/utils.ts b/tests/ably/setup/utils.ts index 5d9a964c..9929bc0f 100644 --- a/tests/ably/setup/utils.ts +++ b/tests/ably/setup/utils.ts @@ -1,5 +1,5 @@ import { httpRequestAsync} from '../../../src/channel/ably/utils'; -export { toBase64 } from '../../../src/channel/ably/utils'; +export { toBase64UrlEncoded } from '../../../src/channel/ably/utils'; const safeAssert = (assertions: Function, done: Function, finalAssertion = false) => { try { From 4e78bf60849d16be1eb658521c1c915c2504a1d7 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 25 Sep 2024 17:27:36 +0530 Subject: [PATCH 08/11] Refactored ably-user-rest-publishing.test as per review comments --- tests/ably/ably-user-rest-publishing.test.ts | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/ably/ably-user-rest-publishing.test.ts b/tests/ably/ably-user-rest-publishing.test.ts index b5349d69..6975ea3e 100644 --- a/tests/ably/ably-user-rest-publishing.test.ts +++ b/tests/ably/ably-user-rest-publishing.test.ts @@ -10,7 +10,7 @@ jest.setTimeout(30000); describe('AblyUserRestPublishing', () => { let testApp: any; let mockAuthServer: MockAuthServer; - let echoInstanes: Array; + let echoInstances: Array; beforeAll(async () => { global.Ably = Ably; @@ -23,14 +23,14 @@ describe('AblyUserRestPublishing', () => { }); beforeEach(() => { - echoInstanes = []; + echoInstances = []; }) afterEach((done) => { - let promises: Array> = [] - for (const echo of echoInstanes) { + let promises: Array> = [] + for (const echo of echoInstances) { echo.disconnect(); - const promise = new Promise(res => { + const promise = new Promise(res => { echo.connector.ably.connection.once('closed', () => { res(true); }); @@ -50,7 +50,7 @@ describe('AblyUserRestPublishing', () => { environment: 'sandbox', requestTokenFn: mockAuthServer.getSignedToken }); - echoInstanes.push(guestUser); + echoInstances.push(guestUser); const publicChannel = guestUser.channel(channelName) as AblyChannel; await new Promise((resolve) => publicChannel.subscribed(resolve)); expect(guestUser.connector.ably.auth.clientId).toBeFalsy(); @@ -66,7 +66,7 @@ describe('AblyUserRestPublishing', () => { environment: 'sandbox', requestTokenFn: mockAuthServer.getSignedToken }); - echoInstanes.push(loggedInUser); + echoInstances.push(loggedInUser); const privateChannel = loggedInUser.private(channelName) as AblyPrivateChannel; await new Promise((resolve) => privateChannel.subscribed(resolve)); expect(loggedInUser.connector.ably.auth.clientId).toBe("sacOO7@github.com"); @@ -77,7 +77,7 @@ describe('AblyUserRestPublishing', () => { test('Guest user return socketId as base64 encoded connectionkey and null clientId', async () => { await getGuestUserChannel("dummyChannel"); - const guestUser = echoInstanes[0]; + const guestUser = echoInstances[0]; const socketIdObj = JSON.parse(fromBase64UrlEncoded(guestUser.socketId())); const expectedConnectionKey = guestUser.connector.ably.connection.key; @@ -111,7 +111,7 @@ describe('AblyUserRestPublishing', () => { // Publish message to other client messagesReceived = [] - const firstClientSocketId = echoInstanes[0].socketId(); + const firstClientSocketId = echoInstances[0].socketId(); await mockAuthServer.broadcastToOthers(firstClientSocketId, { channelName: `public:${channelName}`, eventName: "toOthers", payload: "data" }) await waitForExpect(() => { @@ -122,7 +122,7 @@ describe('AblyUserRestPublishing', () => { test('Logged in user return socketId as base64 encoded connectionkey and clientId', async () => { await getLoggedInUserChannel("dummyChannel"); - const loggedInUser = echoInstanes[0]; + const loggedInUser = echoInstances[0]; const socketIdObj = JSON.parse(fromBase64UrlEncoded(loggedInUser.socketId())); const expectedConnectionKey = loggedInUser.connector.ably.connection.key; @@ -156,7 +156,7 @@ describe('AblyUserRestPublishing', () => { // Publish message to other client messagesReceived = [] - const firstClientSocketId = echoInstanes[0].socketId(); + const firstClientSocketId = echoInstances[0].socketId(); await mockAuthServer.broadcastToOthers(firstClientSocketId, { channelName: `private:${channelName}`, eventName: "toOthers", payload: "data" }); await waitForExpect(() => { From f6969da0b8424cf9b4338847ddbc2566c0cdc258 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 25 Sep 2024 21:39:27 +0530 Subject: [PATCH 09/11] Added unit tests for base64Url encoding and decoding --- src/channel/ably/utils.ts | 8 ++++---- tests/ably/utils.test.ts | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/channel/ably/utils.ts b/src/channel/ably/utils.ts index ee833978..6a13efe9 100644 --- a/src/channel/ably/utils.ts +++ b/src/channel/ably/utils.ts @@ -39,7 +39,7 @@ const isBrowser = typeof window === 'object'; * @param base64 base64 url encoded string * @returns decoded text string */ -export const fromBase64UrlEncoded = (base64: string) => { +export const fromBase64UrlEncoded = (base64: string): string => { const base64Encoded = base64.replace(/-/g, '+').replace(/_/g, '/'); const padding = base64.length % 4 === 0 ? '' : '='.repeat(4 - (base64.length % 4)); const base64WithPadding = base64Encoded + padding; @@ -47,7 +47,7 @@ export const fromBase64UrlEncoded = (base64: string) => { if (isBrowser) { return atob(base64WithPadding); } - return Buffer.from(base64WithPadding, 'base64').toString('binary'); + return Buffer.from(base64WithPadding, 'base64').toString(); }; /** @@ -56,12 +56,12 @@ export const fromBase64UrlEncoded = (base64: string) => { * @param base64 text * @returns base64 url encoded string */ -export const toBase64UrlEncoded = (text: string) => { +export const toBase64UrlEncoded = (text: string): string => { let encoded = '' if (isBrowser) { encoded = btoa(text); } else { - encoded = Buffer.from(text, 'binary').toString('base64'); + encoded = Buffer.from(text).toString('base64'); } return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }; diff --git a/tests/ably/utils.test.ts b/tests/ably/utils.test.ts index 5afb9d5b..2f4859db 100644 --- a/tests/ably/utils.test.ts +++ b/tests/ably/utils.test.ts @@ -1,4 +1,4 @@ -import { parseJwt, toTokenDetails } from '../../src/channel/ably/utils'; +import { fromBase64UrlEncoded, parseJwt, toBase64UrlEncoded, toTokenDetails } from '../../src/channel/ably/utils'; describe('Utils', () => { test('should parse JWT properly', () => { @@ -29,4 +29,17 @@ describe('Utils', () => { expect(tokenDetails.issued).toBe(1654634212000); expect(tokenDetails.token).toBe(token); }); + + test('should encode text into Base64UrlEncoded string', () => { + const normalText = "laravel-echo codebase is of best quality, period!" + const encodedText = toBase64UrlEncoded(normalText); + expect(encodedText).toBe('bGFyYXZlbC1lY2hvIGNvZGViYXNlIGlzIG9mIGJlc3QgcXVhbGl0eSwgcGVyaW9kIQ') + }); + + test('should decode Base64UrlEncoded string into text', () => { + const normalText = "bGFyYXZlbC1lY2hvIGNvZGViYXNlIGlzIG9mIGJlc3QgcXVhbGl0eSwgcGVyaW9kIQ" + const encodedText = fromBase64UrlEncoded(normalText); + expect(encodedText).toBe('laravel-echo codebase is of best quality, period!') + }); + }); From d33fffaeedaca803386c7e837a330e321bb285eb Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 25 Sep 2024 21:58:56 +0530 Subject: [PATCH 10/11] Added edge cases to base64 url encoding decoding, grouped utils tests --- tests/ably/utils.test.ts | 81 +++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/tests/ably/utils.test.ts b/tests/ably/utils.test.ts index 2f4859db..3bba4848 100644 --- a/tests/ably/utils.test.ts +++ b/tests/ably/utils.test.ts @@ -1,45 +1,58 @@ import { fromBase64UrlEncoded, parseJwt, toBase64UrlEncoded, toTokenDetails } from '../../src/channel/ably/utils'; describe('Utils', () => { - test('should parse JWT properly', () => { - const token = + describe('JWT handling', () => { + 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"]}', - }; + 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).toStrictEqual(expectedHeader); - expect(parseJwt(token).payload).toStrictEqual(expectedPayload); - }); - - test('should convert to tokenDetails', () => { - const token = + expect(parseJwt(token).header).toStrictEqual(expectedHeader); + expect(parseJwt(token).payload).toStrictEqual(expectedPayload); + }); + + 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); + const tokenDetails = toTokenDetails(token); + expect(tokenDetails.clientId).toBe('user123'); + expect(tokenDetails.expires).toBe(1654637812000); + expect(tokenDetails.issued).toBe(1654634212000); + expect(tokenDetails.token).toBe(token); + }); }); - test('should encode text into Base64UrlEncoded string', () => { - const normalText = "laravel-echo codebase is of best quality, period!" - const encodedText = toBase64UrlEncoded(normalText); - expect(encodedText).toBe('bGFyYXZlbC1lY2hvIGNvZGViYXNlIGlzIG9mIGJlc3QgcXVhbGl0eSwgcGVyaW9kIQ') - }); + describe('Base64 URL encoding/decoding', () => { + test('should encode text into Base64UrlEncoded string', () => { + const normalText = "laravel-echo codebase is of best quality, period!" + const encodedText = toBase64UrlEncoded(normalText); + expect(encodedText).toBe('bGFyYXZlbC1lY2hvIGNvZGViYXNlIGlzIG9mIGJlc3QgcXVhbGl0eSwgcGVyaW9kIQ') - test('should decode Base64UrlEncoded string into text', () => { - const normalText = "bGFyYXZlbC1lY2hvIGNvZGViYXNlIGlzIG9mIGJlc3QgcXVhbGl0eSwgcGVyaW9kIQ" - const encodedText = fromBase64UrlEncoded(normalText); - expect(encodedText).toBe('laravel-echo codebase is of best quality, period!') - }); + // edge cases + expect(toBase64UrlEncoded('')).toBe(''); + expect(toBase64UrlEncoded('Hello, 世界! 🌍')).toBe('SGVsbG8sIOS4lueVjCEg8J-MjQ'); + expect(toBase64UrlEncoded('a')).toBe('YQ'); // Would be 'YQ==' in standard Base64 + }); + + test('should decode Base64UrlEncoded string into text', () => { + const normalText = "bGFyYXZlbC1lY2hvIGNvZGViYXNlIGlzIG9mIGJlc3QgcXVhbGl0eSwgcGVyaW9kIQ" + const encodedText = fromBase64UrlEncoded(normalText); + expect(encodedText).toBe('laravel-echo codebase is of best quality, period!') + // edge cases + expect(fromBase64UrlEncoded('')).toBe(''); + expect(fromBase64UrlEncoded('SGVsbG8sIOS4lueVjCEg8J-MjQ')).toBe('Hello, 世界! 🌍'); + expect(fromBase64UrlEncoded('YQ')).toBe('a'); // No padding in Base64Url + }); + }); }); From ce52bfb1f1b5d5f50167227f8d5bc68b15d3654b Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 25 Sep 2024 22:29:50 +0530 Subject: [PATCH 11/11] Added more edge cases to utils.test.ts for base64 encode/decode --- tests/ably/utils.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/ably/utils.test.ts b/tests/ably/utils.test.ts index 3bba4848..658f384e 100644 --- a/tests/ably/utils.test.ts +++ b/tests/ably/utils.test.ts @@ -30,6 +30,11 @@ describe('Utils', () => { expect(tokenDetails.issued).toBe(1654634212000); expect(tokenDetails.token).toBe(token); }); + + test('should throw error for invalid JWT', () => { + const invalidToken = 'invalid.token.string'; + expect(() => parseJwt(invalidToken)).toThrow('Unexpected token'); + }); }); describe('Base64 URL encoding/decoding', () => { @@ -41,7 +46,9 @@ describe('Utils', () => { // edge cases expect(toBase64UrlEncoded('')).toBe(''); expect(toBase64UrlEncoded('Hello, 世界! 🌍')).toBe('SGVsbG8sIOS4lueVjCEg8J-MjQ'); + expect(toBase64UrlEncoded('Hello+World/123')).toBe('SGVsbG8rV29ybGQvMTIz'); expect(toBase64UrlEncoded('a')).toBe('YQ'); // Would be 'YQ==' in standard Base64 + expect(toBase64UrlEncoded('\x8EÇdwïìvÇ')).toBe('wo7Dh2R3w6_DrHbDhw'); }); test('should decode Base64UrlEncoded string into text', () => { @@ -52,7 +59,9 @@ describe('Utils', () => { // edge cases expect(fromBase64UrlEncoded('')).toBe(''); expect(fromBase64UrlEncoded('SGVsbG8sIOS4lueVjCEg8J-MjQ')).toBe('Hello, 世界! 🌍'); + expect(fromBase64UrlEncoded('SGVsbG8rV29ybGQvMTIz')).toBe('Hello+World/123'); expect(fromBase64UrlEncoded('YQ')).toBe('a'); // No padding in Base64Url + expect(fromBase64UrlEncoded('wo7Dh2R3w6_DrHbDhw')).toBe('\x8EÇdwïìvÇ'); }); }); });