UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

755 lines (667 loc) 27.2 kB
/* global describe, beforeEach, afterEach, it, after */ /* eslint no-console: 0 */ import assert from 'assert'; import nock from 'nock'; import * as MessageActions from '../../../src/core/types/api/message-action'; import PubNub from '../../../src/node/index'; import utils from '../../utils'; /** * Message action object. */ type MessageAction = MessageActions.AddMessageActionParameters['action']; /** * Prepare messages history. * * @param client - PubNub client instance which will be used to publish messages. * @param count - How many messages should be published. * @param channel - Name of the channel into which messages should be published. * @param completion - Messages set publish completion function. */ function publishMessages(client: PubNub, count: number, channel: string, completion: (timetokens: string[]) => void) { let publishCompleted = 0; const timetokens: string[] = []; const publish = (messageIdx: number) => { const message = { messageIdx, time: Date.now() }; client.publish({ message, channel }, (status, response) => { publishCompleted += 1; if (!status.error && response) { timetokens.push(response.timetoken); } else { console.error('Publish did fail:', status); } if (publishCompleted < count) { publish(publishCompleted); } else if (publishCompleted === count) { completion(timetokens.sort((left, right) => parseInt(left, 10) - parseInt(right, 10))); } }); }; publish(publishCompleted); } /** * Attach message actions to the previously published messages. * * @param client - PubNub client instance which should be used to add message action to the message. * @param count - How many message actions should be added to each referenced message. * @param messageTimetokens - List of referenced messages' timetokens. * @param channel - Name of the channel where referenced messages has been published. * @param completion - Message actions addition completion function. */ function addActions( client: PubNub, count: number, messageTimetokens: string[], channel: string, completion: (timetokens: string[]) => void, ) { const types = ['reaction', 'receipt', 'custom']; const values = [ PubNub.generateUUID(), PubNub.generateUUID(), PubNub.generateUUID(), PubNub.generateUUID(), PubNub.generateUUID(), PubNub.generateUUID(), PubNub.generateUUID(), PubNub.generateUUID(), PubNub.generateUUID(), PubNub.generateUUID(), ]; const actionsToAdd: { messageTimetoken: string; action: MessageAction }[] = []; const timetokens: string[] = []; let actionsAdded = 0; for (let messageIdx = 0; messageIdx < messageTimetokens.length; messageIdx += 1) { const messageTimetoken = messageTimetokens[messageIdx]; for (let messageActionIdx = 0; messageActionIdx < count; messageActionIdx += 1) { const action: MessageAction = { type: types[(messageActionIdx + 1) % 3], value: values[(messageActionIdx + 1) % 10], }; actionsToAdd.push({ messageTimetoken, action }); } } /** * Attach set of message actions. * * @param actionIdx - Index of currently adding message action. */ const addAction = (actionIdx: number) => { const { messageTimetoken, action } = actionsToAdd[actionIdx]; client.addMessageAction({ channel, messageTimetoken, action }, (status, response) => { actionsAdded += 1; if (!status.error && response) { timetokens.push(response.data.actionTimetoken); } else { console.error('Action add did fail:', action, '\n', status); } if (actionsAdded < actionsToAdd.length) { addAction(actionsAdded); } else if (actionsAdded === actionsToAdd.length) { setTimeout(() => { completion(timetokens.sort((left, right) => parseInt(left, 10) - parseInt(right, 10))); }, 500); } }); }; addAction(actionsAdded); } describe('message actions endpoints', () => { const subscribeKey = process.env.SUBSCRIBE_KEY || 'demo'; const publishKey = process.env.PUBLISH_KEY || 'demo'; let pubnub: PubNub; afterEach(() => { nock.enableNetConnect(); pubnub.removeAllListeners(); pubnub.unsubscribeAll(); pubnub.destroy(true); }); beforeEach(() => { nock.cleanAll(); pubnub = new PubNub({ subscribeKey, publishKey, uuid: 'myUUID', authKey: 'myAuthKey', // @ts-expect-error Force override default value. useRequestId: false, }); }); describe('addMessageAction', () => { describe('##validation', () => { it("fails if 'action' is missing", (done) => { nock.disableNetConnect(); const scope = utils .createNock() .post(`/v1/message-actions/${subscribeKey}/channel/test-channel/message/1234567890`) .reply(200, {}); pubnub // @ts-expect-error Intentionally don't include `action`. .addMessageAction({ channel: 'test-channel', messageTimetoken: '1234567890', }) .catch((err) => { try { assert.equal(scope.isDone(), false); assert.equal(err.status.message, 'Missing Action'); done(); } catch (error) { done(error); } }); }); it("fails if 'type' is missing", (done) => { nock.disableNetConnect(); const scope = utils .createNock() .post(`/v1/message-actions/${subscribeKey}/channel/test-channel/message/1234567890`) .reply(200, {}); const action = { value: 'test value' }; pubnub .addMessageAction({ channel: 'test-channel', messageTimetoken: '1234567890', // @ts-expect-error Intentionally don't include `type` field into `action`. action, }) .catch((err) => { try { assert.equal(scope.isDone(), false); assert.equal(err.status.message, 'Missing Action.type'); done(); } catch (error) { done(error); } }); }); it("fails if 'type' is too long", (done) => { nock.disableNetConnect(); const scope = utils .createNock() .post(`/v1/message-actions/${subscribeKey}/channel/test-channel/message/1234567890`) .reply(200, {}); const action: MessageAction = { type: PubNub.generateUUID(), value: 'test value' }; pubnub .addMessageAction({ channel: 'test-channel', messageTimetoken: '1234567890', action, }) .catch((err) => { try { assert.equal(scope.isDone(), false); assert.equal(err.status.message, 'Action.type value exceed maximum length of 15'); done(); } catch (error) { done(error); } }); }); it("fails if 'value' is missing", (done) => { nock.disableNetConnect(); const scope = utils .createNock() .post(`/v1/message-actions/${subscribeKey}/channel/test-channel/message/1234567890`) .reply(200, {}); const action = { type: 'custom' }; pubnub .addMessageAction({ channel: 'test-channel', messageTimetoken: '1234567890', // @ts-expect-error Intentionally don't include `value` field into `action`. action, }) .catch((err) => { try { assert.equal(scope.isDone(), false); assert.equal(err.status.message, 'Missing Action.value'); done(); } catch (error) { done(error); } }); }); it("fails if 'messageTimetoken' is missing", (done) => { nock.disableNetConnect(); const scope = utils .createNock() .post(`/v1/message-actions/${subscribeKey}/channel/test-channel/message/`) .reply(200, {}); const action: MessageAction = { type: 'custom', value: 'test value' }; pubnub // @ts-expect-error Intentionally don't include `messageTimetoken`. .addMessageAction({ channel: 'test-channel', action, }) .catch((err) => { try { assert.equal(scope.isDone(), false); assert.equal(err.status.message, 'Missing message timetoken'); done(); } catch (error) { done(error); } }); }); it("fails if 'channel' is missing", (done) => { nock.disableNetConnect(); const scope = utils .createNock() .post(`/v1/message-actions/${subscribeKey}/channel/test-channel/message/1234567890`) .reply(200, {}); const action: MessageAction = { type: 'custom', value: 'test value' }; pubnub // @ts-expect-error Intentionally don't include `channel`. .addMessageAction({ messageTimetoken: '1234567890', action, }) .catch((err) => { try { assert.equal(scope.isDone(), false); assert.equal(err.status.message, 'Missing message channel'); done(); } catch (error) { done(error); } }); }); }); it('add message action', (done) => { const messageAction: MessageAction = { type: 'custom', value: PubNub.generateUUID() }; const channel = PubNub.generateUUID(); publishMessages(pubnub, 1, channel, (timetokens) => { pubnub.addMessageAction( { channel, messageTimetoken: timetokens[0], action: messageAction }, (status, response) => { try { assert.equal(status.error, false); assert(response !== null); assert.equal(response.data.type, messageAction.type); assert.equal(response.data.value, messageAction.value); assert.equal(response.data.uuid, pubnub.getUUID()); assert.equal(response.data.messageTimetoken, timetokens[0]); assert(response.data.actionTimetoken); done(); } catch (error) { done(error); } }, ); }); }).timeout(60000); it('add message action with encoded channel', (done) => { const messageAction: MessageAction = { type: 'custom', value: PubNub.generateUUID() }; const channel = `${PubNub.generateUUID()}#1`; publishMessages(pubnub, 1, channel, (timetokens) => { pubnub.addMessageAction( { channel, messageTimetoken: timetokens[0], action: messageAction }, (status, response) => { try { assert.equal(status.error, false); assert(response !== null); if (undefined === response.data.uuid) { console.log(`Received unexpected response:`); console.dir(response, { depth: 20 }); } assert.equal(response.data.type, messageAction.type); assert.equal(response.data.value, messageAction.value); assert.equal(response.data.uuid, pubnub.getUUID()); assert.equal(response.data.messageTimetoken, timetokens[0]); assert(response.data.actionTimetoken); done(); } catch (error) { done(error); } }, ); }); }).timeout(60000); it('add message action and 207 status code', (done) => { nock.disableNetConnect(); const scope = utils .createNock() .post(`/v1/message-actions/${subscribeKey}/channel/test-channel/message/1234567890`) .query({ pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`, uuid: 'myUUID', auth: 'myAuthKey', }) .reply(207, { status: 207, data: { type: 'reaction', value: 'smiley_face', uuid: 'user-456', actionTimetoken: '15610547826970050', messageTimetoken: '15610547826969050', }, error: { message: 'Stored but failed to publish message action.', source: 'actions', }, }); pubnub.addMessageAction( { channel: 'test-channel', messageTimetoken: '1234567890', action: { type: 'custom', value: 'test' } }, (status) => { try { assert.equal(scope.isDone(), true); assert.equal(status.statusCode, 207); // @ts-expect-error `errorData` may contain a dictionary (Payload) with an arbitrary set of fields. assert(status.errorData!.message); done(); } catch (error) { done(error); } }, ); }); it('add message action should trigger event', (done) => { const messageAction: MessageAction = { type: 'custom', value: PubNub.generateUUID() }; const channel = PubNub.generateUUID(); let messageTimetoken: string | null = null; pubnub.addListener({ status: (status) => { if (status.category === 'PNConnectedCategory') { pubnub.publish({ channel, message: { hello: 'test' }, sendByPost: true }, (publishStatus, response) => { assert(response !== null); messageTimetoken = response.timetoken; pubnub.addMessageAction({ channel, messageTimetoken, action: messageAction }); }); } }, messageAction: (messageActionEvent) => { try { assert(messageActionEvent.data); assert.equal(messageActionEvent.data.type, messageAction.type); assert.equal(messageActionEvent.data.value, messageAction.value); assert.equal(messageActionEvent.data.uuid, pubnub.getUUID()); assert.equal(messageActionEvent.data.messageTimetoken, messageTimetoken); assert(messageActionEvent.data.actionTimetoken); assert.equal(messageActionEvent.event, 'added'); pubnub.unsubscribeAll(); done(); } catch (error) { done(error); } }, }); pubnub.subscribe({ channels: [channel] }); }).timeout(60000); }); describe('removeMessageAction', () => { describe('##validation', () => { it("fails if 'messageTimetoken' is missing", (done) => { nock.disableNetConnect(); const scope = utils .createNock() .delete(`/v1/message-actions/${subscribeKey}/channel/test-channel/message/1234567890/action/12345678901`) .reply(200, {}); pubnub // @ts-expect-error Intentionally don't include `messageTimetoken`. .removeMessageAction({ channel: 'test-channel', actionTimetoken: '1234567890', }) .catch((err) => { try { assert.equal(scope.isDone(), false); assert.equal(err.status.message, 'Missing message timetoken'); done(); } catch (error) { done(error); } }); }); it("fails if 'actionTimetoken' is missing", (done) => { nock.disableNetConnect(); const scope = utils .createNock() .delete(`/v1/message-actions/${subscribeKey}/channel/test-channel/message/1234567890/action/12345678901`) .reply(200, {}); pubnub // @ts-expect-error Intentionally don't include `actionTimetoken`. .removeMessageAction({ channel: 'test-channel', messageTimetoken: '1234567890', }) .catch((err) => { try { assert.equal(scope.isDone(), false); assert.equal(err.status.message, 'Missing action timetoken'); done(); } catch (error) { done(error); } }); }); it("fails if 'channel' is missing", (done) => { nock.disableNetConnect(); const scope = utils .createNock() .delete(`/v1/message-actions/${subscribeKey}/channel/test-channel/message/1234567890/action/12345678901`) .reply(200, {}); pubnub // @ts-expect-error Intentionally don't include `channel`. .removeMessageAction({ messageTimetoken: '1234567890', actionTimetoken: '12345678901', }) .catch((err) => { try { assert.equal(scope.isDone(), false); assert.equal(err.status.message, 'Missing message action channel'); done(); } catch (error) { done(error); } }); }); }); it('remove message action', (done) => { const channel = PubNub.generateUUID(); publishMessages(pubnub, 1, channel, (messageTimetokens) => { addActions(pubnub, 1, messageTimetokens, channel, (actionTimetokens) => { pubnub.getMessageActions({ channel }, (status, response) => { assert.equal(status.error, false); assert(response !== null); assert.equal(response.data.length, actionTimetokens.length); pubnub.removeMessageAction( { channel, actionTimetoken: actionTimetokens[0], messageTimetoken: messageTimetokens[0] }, (removeMessageStatus) => { assert.equal(removeMessageStatus.error, false); setTimeout(() => { pubnub.getMessageActions({ channel }, (getMessagesStatus, getMessagesResponse) => { try { assert.equal(getMessagesStatus.error, false); assert(getMessagesResponse !== null); assert.equal(getMessagesResponse.data.length, 0); done(); } catch (error) { done(error); } }); }, 2000); }, ); }); }); }); }).timeout(60000); it('remove message action with encoded channel', (done) => { const channel = `${PubNub.generateUUID()}#1`; publishMessages(pubnub, 1, channel, (messageTimetokens) => { addActions(pubnub, 1, messageTimetokens, channel, (actionTimetokens) => { pubnub.getMessageActions({ channel }, (status, response) => { assert.equal(status.error, false); assert(response !== null); assert.equal(response.data.length, actionTimetokens.length); pubnub.removeMessageAction( { channel, actionTimetoken: actionTimetokens[0], messageTimetoken: messageTimetokens[0] }, (removeMessageStatus) => { assert.equal(removeMessageStatus.error, false); setTimeout(() => { pubnub.getMessageActions({ channel }, (getMessagesStatus, getMessagesResponse) => { try { assert.equal(getMessagesStatus.error, false); assert(getMessagesResponse !== null); assert.equal(getMessagesResponse.data.length, 0); done(); } catch (error) { done(error); } }); }, 2000); }, ); }); }); }); }).timeout(60000); it('remove message action should trigger event', (done) => { const channel = PubNub.generateUUID(); publishMessages(pubnub, 1, channel, (messageTimetokens) => { addActions(pubnub, 1, messageTimetokens, channel, (actionTimetokens) => { pubnub.addListener({ status: (status) => { if (status.category === 'PNConnectedCategory') { pubnub.removeMessageAction( { channel, actionTimetoken: actionTimetokens[0], messageTimetoken: messageTimetokens[0] }, (removeMessagesStatus) => { assert.equal(removeMessagesStatus.error, false); }, ); } }, messageAction: (messageActionEvent) => { try { assert(messageActionEvent.data); assert.equal(messageActionEvent.data.uuid, pubnub.getUUID()); assert.equal(messageActionEvent.data.messageTimetoken, messageTimetokens[0]); assert.equal(messageActionEvent.data.actionTimetoken, actionTimetokens[0]); assert.equal(messageActionEvent.event, 'removed'); pubnub.unsubscribeAll(); done(); } catch (error) { done(error); } }, }); pubnub.subscribe({ channels: [channel] }); }); }); }).timeout(60000); }); describe('getMessageAction', () => { describe('##validation', () => { it("fails if 'channel' is missing", (done) => { nock.disableNetConnect(); const scope = utils.createNock().get(`/v1/message-actions/${subscribeKey}/channel/test-channel`).reply(200, {}); // @ts-expect-error Intentionally don't include `channel`. pubnub.getMessageActions({}).catch((err) => { try { assert.equal(scope.isDone(), false); assert.equal(err.status.message, 'Missing message channel'); done(); } catch (error) { done(error); } }); }); }); it('fetch message actions', (done) => { const channel = PubNub.generateUUID(); publishMessages(pubnub, 2, channel, (messageTimetokens) => { addActions(pubnub, 3, messageTimetokens, channel, (actionTimetokens) => { const lastPublishedActionTimetoken = actionTimetokens[actionTimetokens.length - 1]; const firstPublishedActionTimetoken = actionTimetokens[0]; pubnub.getMessageActions({ channel }, (status, response) => { try { assert.equal(status.error, false); assert(response !== null); const firstFetchedActionTimetoken = response.data[0].actionTimetoken; const lastFetchedActionTimetoken = response.data[response.data.length - 1].actionTimetoken; assert.equal(firstFetchedActionTimetoken, firstPublishedActionTimetoken); assert.equal(lastFetchedActionTimetoken, lastPublishedActionTimetoken); assert.equal(response.data.length, actionTimetokens.length); assert.equal(response.start, firstPublishedActionTimetoken); assert.equal(response.end, lastPublishedActionTimetoken); done(); } catch (error) { done(error); } }); }); }); }).timeout(60000); it('fetch message actions with encoded channel', (done) => { const channel = `${PubNub.generateUUID()}#1`; publishMessages(pubnub, 2, channel, (messageTimetokens) => { addActions(pubnub, 3, messageTimetokens, channel, (actionTimetokens) => { const lastPublishedActionTimetoken = actionTimetokens[actionTimetokens.length - 1]; const firstPublishedActionTimetoken = actionTimetokens[0]; pubnub.getMessageActions({ channel }, (status, response) => { try { assert.equal(status.error, false); assert(response !== null); const firstFetchedActionTimetoken = response.data[0].actionTimetoken; const lastFetchedActionTimetoken = response.data[response.data.length - 1].actionTimetoken; assert.equal(firstFetchedActionTimetoken, firstPublishedActionTimetoken); assert.equal(lastFetchedActionTimetoken, lastPublishedActionTimetoken); assert.equal(response.data.length, actionTimetokens.length); assert.equal(response.start, firstPublishedActionTimetoken); assert.equal(response.end, lastPublishedActionTimetoken); done(); } catch (error) { done(error); } }); }); }); }).timeout(60000); it('fetch next message actions page', (done) => { const channel = PubNub.generateUUID(); publishMessages(pubnub, 2, channel, (messageTimetokens) => { addActions(pubnub, 5, messageTimetokens, channel, (actionTimetokens) => { const lastPublishedActionTimetoken = actionTimetokens[actionTimetokens.length - 1]; const halfSize = Math.floor(actionTimetokens.length * 0.5); const firstPublishedActionTimetoken = actionTimetokens[0]; const middleMinusOnePublishedActionTimetoken = actionTimetokens[halfSize - 1]; const middlePublishedActionTimetoken = actionTimetokens[halfSize]; pubnub.getMessageActions({ channel, limit: halfSize }, (status, response) => { assert.equal(status.error, false); assert(response !== null); let firstFetchedActionTimetoken = response.data[0].actionTimetoken; let lastFetchedActionTimetoken = response.data[response.data.length - 1].actionTimetoken; assert.equal(firstFetchedActionTimetoken, middlePublishedActionTimetoken); assert.equal(lastFetchedActionTimetoken, lastPublishedActionTimetoken); assert.equal(response.data.length, halfSize); assert.equal(response.start, middlePublishedActionTimetoken); assert.equal(response.end, lastPublishedActionTimetoken); pubnub.getMessageActions( { channel, start: middlePublishedActionTimetoken, limit: halfSize }, (getMessageActionsStatus, getMessageActionsResponse) => { try { assert.equal(getMessageActionsStatus.error, false); assert(getMessageActionsResponse !== null); firstFetchedActionTimetoken = getMessageActionsResponse.data[0].actionTimetoken; lastFetchedActionTimetoken = getMessageActionsResponse.data[getMessageActionsResponse.data.length - 1].actionTimetoken; assert.equal(firstFetchedActionTimetoken, firstPublishedActionTimetoken); assert.equal(lastFetchedActionTimetoken, middleMinusOnePublishedActionTimetoken); assert.equal(getMessageActionsResponse.data.length, halfSize); assert.equal(getMessageActionsResponse.start, firstPublishedActionTimetoken); assert.equal(getMessageActionsResponse.end, middleMinusOnePublishedActionTimetoken); done(); } catch (error) { done(error); } }, ); }); }); }); }).timeout(60000); }); });