Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECO-4977] Fix broadcast to others #42

Merged
merged 12 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/channel/ably/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
8 changes: 7 additions & 1 deletion src/connector/ably-connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 = {
connectionKey : this.ably.connection.key,
clientId : this.ably.auth.clientId ?? null,
}
return toBase64(JSON.stringify(socketIdObject));
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/echo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions tests/ably/ably-user-login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>= []
// Initial clientId is null
expect(mockAuthServer.clientId).toBeNull();
Expand Down Expand Up @@ -70,7 +70,7 @@ describe('AblyUserLogin', () => {
expect(echo.connector.ablyAuth.existingToken().clientId).toBe('[email protected]');
});

test('user logs in with previous (guest) channels', async () => {
test('user logs in with previous (public) channels', async () => {
let connectionStates : Array<any>= []
let publicChannelStates : Array<any>= []

Expand Down
167 changes: 167 additions & 0 deletions tests/ably/ably-user-rest-publishing.test.ts
Original file line number Diff line number Diff line change
@@ -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<Echo>;
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved

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<Promise<Boolean>> = []
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
for (const echo of echoInstanes) {
echo.disconnect();
const promise = new Promise<Boolean>(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 = '[email protected]';
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("[email protected]");
expect(loggedInUser.connector.ablyAuth.existingToken().clientId).toBe("[email protected]")
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<string> = []
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("[email protected]");
});

test('Logged in user publishes message via rest API', async () => {
let messagesReceived: Array<string> = []
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);
});
});
});
27 changes: 24 additions & 3 deletions tests/ably/setup/mock-auth-server.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,49 @@
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<string>;

/**
* 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 = '[email protected]';
userInfo = { id: '[email protected]', name: 'sacOO7' };

shortLived: channels;
banned: channels;

userInfo = { id: '[email protected]', name: 'sacOO7' }; // Used for presence

constructor(apiKey: string, environment = 'sandbox') {
const keys = apiKey.split(':');
this.keyName = keys[0];
this.keySecret = keys[1];
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) => {
Expand Down
5 changes: 2 additions & 3 deletions tests/ably/setup/utils.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 = {
Expand Down
Loading