UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

737 lines (673 loc) 25.1 kB
/* global describe, beforeEach, afterEach, it, after */ /* eslint no-console: 0 */ /* eslint-disable max-len */ import assert from 'assert'; import nock from 'nock'; import * as MessageActions from '../../../src/core/types/api/message-action'; import * as Publish from '../../../src/core/endpoints/publish'; import { Payload } from '../../../src/core/types/api'; import PubNub from '../../../src/node/index'; import utils from '../../utils'; import { PubNubError } from '../../../src/errors/pubnub-error'; /** * Published test message shape. */ type TestMessage = { messageIdx: string; time: number }; /** * 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 customMessageType - User-provided message type (ignored if empty). * @param completion - Messages set publish completion function. */ function publishMessagesToChannel( client: PubNub, count: number, channel: string, customMessageType: string, completion: (published: { message: TestMessage; timetoken: string }[]) => void, ) { let messages: { message: TestMessage; timetoken: string }[] = []; let publishCompleted = 0; const publish = (messageIdx: number) => { const payload: Publish.PublishParameters = { message: { messageIdx: [channel, messageIdx].join(': '), time: Date.now() }, channel, }; if (customMessageType.length) payload.customMessageType = customMessageType; if (messageIdx % 2 === 0) payload.meta = { time: (payload.message as TestMessage).time }; client.publish(payload, (status, response) => { publishCompleted += 1; if (!status.error && response) { messages.push({ message: payload.message as TestMessage, timetoken: response.timetoken }); messages = messages.sort((left, right) => parseInt(left.timetoken, 10) - parseInt(right.timetoken, 10)); } else { console.error('Publish did fail:', status); } if (publishCompleted < count) { publish(publishCompleted); } else if (publishCompleted === count) { completion(messages); } }); }; 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 addActionsInChannel( client: PubNub, count: number, messageTimetokens: string[], channel: string, completion: (added: MessageActions.MessageAction[]) => 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(), ]; let actions: MessageActions.MessageAction[] = []; const actionsToAdd: { messageTimetoken: string; action: Pick<MessageActions.AddMessageActionParameters['action'], 'type' | 'value'>; }[] = []; 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 = { 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) { actions.push(response.data); actions = actions.sort( (left, right) => parseInt(left.actionTimetoken, 10) - parseInt(right.actionTimetoken, 10), ); } else { console.error('Action add did fail:', status); } if (actionsAdded < actionsToAdd.length) { addAction(actionsAdded); } else if (actionsAdded === actionsToAdd.length) { completion(actions); } }); }; addAction(actionsAdded); } describe('fetch messages 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', // @ts-expect-error Force override default value. useRequestId: false, useRandomIVs: false, }); }); it('supports payload', (done) => { nock.disableNetConnect(); const scope = utils .createNock() .get(`/v3/history/sub-key/${subscribeKey}/channel/ch1,ch2`) .query({ max: '10', pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`, uuid: 'myUUID', include_uuid: 'true', include_message_type: 'true', }) .reply( 200, '{ "channels": { "ch1": [{"message":{"text":"hey1"},"timetoken":"11"}, {"message":{"text":"hey2"},"timetoken":"12"}], "ch2": [{"message":{"text":"hey3"},"timetoken":"21"}, {"message":{"text":"hey2"},"timetoken":"22"}] } }', { 'content-type': 'text/javascript' }, ); pubnub.fetchMessages({ channels: ['ch1', 'ch2'], count: 10 }, (status, response) => { try { assert.equal(status.error, false); assert.deepEqual(response, { channels: { ch1: [ { channel: 'ch1', message: { text: 'hey1', }, timetoken: '11', messageType: undefined, uuid: undefined, }, { channel: 'ch1', message: { text: 'hey2', }, timetoken: '12', messageType: undefined, uuid: undefined, }, ], ch2: [ { channel: 'ch2', message: { text: 'hey3', }, timetoken: '21', messageType: undefined, uuid: undefined, }, { channel: 'ch2', message: { text: 'hey2', }, timetoken: '22', messageType: undefined, uuid: undefined, }, ], }, }); assert.equal(scope.isDone(), true); done(); } catch (error) { done(error); } }); }); it('supports encrypted payload', (done) => { nock.disableNetConnect(); const scope = utils .createNock() .get(`/v3/history/sub-key/${subscribeKey}/channel/ch1,ch2`) .query({ max: '10', pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`, uuid: 'myUUID', include_uuid: 'true', include_message_type: 'true', }) .reply( 200, '{ "channels": { "ch1": [{"message":"zFJeF9BVABL80GUiQEBjLg==","timetoken":"11"}, {"message":"zFJeF9BVABL80GUiQEBjLg==","timetoken":"12"}], "ch2": [{"message":"HIq4MTi9nk/KEYlHOKpMCaH78ZXppGynDHrgY9nAd3s=","timetoken":"21"}, {"message":"HIq4MTi9nk/KEYlHOKpMCaH78ZXppGynDHrgY9nAd3s=","timetoken":"22"}] } }', { 'content-type': 'text/javascript' }, ); pubnub.setCipherKey('cipherKey'); pubnub.fetchMessages({ channels: ['ch1', 'ch2'], count: 10 }, (status, response) => { try { assert.equal(status.error, false); assert.deepEqual(response, { channels: { ch1: [ { channel: 'ch1', message: { text: 'hey', }, timetoken: '11', messageType: undefined, uuid: undefined, }, { channel: 'ch1', message: { text: 'hey', }, timetoken: '12', messageType: undefined, uuid: undefined, }, ], ch2: [ { channel: 'ch2', message: { text2: 'hey2', }, timetoken: '21', messageType: undefined, uuid: undefined, }, { channel: 'ch2', message: { text2: 'hey2', }, timetoken: '22', messageType: undefined, uuid: undefined, }, ], }, }); assert.equal(scope.isDone(), true); done(); } catch (error) { done(error); } }); }); it('supports metadata', (done) => { const channel = PubNub.generateUUID(); const expectedMessagesCount = 10; publishMessagesToChannel(pubnub, expectedMessagesCount, channel, '', (messages) => { pubnub.fetchMessages({ channels: [channel], count: 25, includeMeta: true }, (_, response) => { try { assert(response !== null); const channelMessages = response.channels[channel]; assert.deepEqual(channelMessages[0].meta, { time: messages[0].message.time }); assert(!channelMessages[1].meta); done(); } catch (error) { done(error); } }); }); }).timeout(60000); it('throws when requested actions for multiple channels', async () => { let errorCatched = false; try { await pubnub.fetchMessages({ channels: ['channelA', 'channelB'], includeMessageActions: true }); } catch (error) { assert(error instanceof PubNubError); assert.equal( error.status!.message, 'History can return actions data for a single channel only. Either pass a single channel or disable the includeMessageActions flag.', ); errorCatched = true; } assert(errorCatched); }); it('supports custom message type', (done) => { const channel = PubNub.generateUUID(); const expectedMessagesCount = 2; publishMessagesToChannel(pubnub, expectedMessagesCount, channel, 'test-message-type', (messages) => { const messageTimetokens = messages.map((message) => message.timetoken); pubnub.fetchMessages({ channels: [channel], includeCustomMessageType: true }, (status, response) => { assert.equal(status.error, false, `Fetch messages error: ${JSON.stringify(status.errorData)}`); try { assert.equal(status.error, false); assert(response !== null); const fetchedMessages = response.channels[channel]; fetchedMessages.forEach((message) => { assert.equal(message.customMessageType, 'test-message-type'); }); done(); } catch (error) { done(error); } }); }); }).timeout(60000); it("supports actions (stored as 'data' field)", (done) => { const channel = PubNub.generateUUID(); const expectedMessagesCount = 2; const expectedActionsCount = 4; publishMessagesToChannel(pubnub, expectedMessagesCount, channel, '', (messages) => { const messageTimetokens = messages.map((message) => message.timetoken); addActionsInChannel(pubnub, expectedActionsCount, messageTimetokens, channel, (actions) => { setTimeout(() => { pubnub.fetchMessages({ channels: [channel], includeMessageActions: true }, (status, response) => { try { assert.equal(status.error, false); assert(response !== null); const fetchedMessages = response.channels[channel]; // TypeScript types system now requires to figure out type of object before using it. assert('actions' in fetchedMessages[0]); const actionsByType = fetchedMessages[0].data ?? {}; let historyActionsCount = 0; Object.keys(actionsByType).forEach((actionType) => { Object.keys(actionsByType[actionType]).forEach((actionValue) => { let actionFound = false; historyActionsCount += 1; actions.forEach((action) => { if (action.value === actionValue) actionFound = true; }); assert.equal(actionFound, true); }); }); assert.equal(historyActionsCount, expectedActionsCount); assert.equal(fetchedMessages[0].timetoken, messageTimetokens[0]); assert.equal( fetchedMessages[fetchedMessages.length - 1].timetoken, messageTimetokens[messageTimetokens.length - 1], ); done(); } catch (error) { done(error); } }); }, 2000); }); }); }).timeout(60000); it("supports actions (stored as 'actions' field)", (done) => { const channel = PubNub.generateUUID(); const expectedMessagesCount = 2; const expectedActionsCount = 4; publishMessagesToChannel(pubnub, expectedMessagesCount, channel, '', (messages) => { const messageTimetokens = messages.map((message) => message.timetoken); addActionsInChannel(pubnub, expectedActionsCount, messageTimetokens, channel, (actions) => { setTimeout(() => { pubnub.fetchMessages({ channels: [channel], includeMessageActions: true }, (status, response) => { try { assert.equal(status.error, false); assert(response !== null); const fetchedMessages = response.channels[channel]; // TypeScript types system now requires to figure out type of object before using it. assert('actions' in fetchedMessages[0]); const actionsByType = fetchedMessages[0].actions ?? {}; let historyActionsCount = 0; Object.keys(actionsByType).forEach((actionType) => { Object.keys(actionsByType[actionType]).forEach((actionValue) => { let actionFound = false; historyActionsCount += 1; actions.forEach((action) => { if (action.value === actionValue) { actionFound = true; } }); assert.equal(actionFound, true); }); }); assert.equal(historyActionsCount, expectedActionsCount); assert.equal(fetchedMessages[0].timetoken, messageTimetokens[0]); assert.equal( fetchedMessages[fetchedMessages.length - 1].timetoken, messageTimetokens[messageTimetokens.length - 1], ); done(); } catch (error) { done(error); } }); }, 2000); }); }); }).timeout(60000); it('should return "more" field when server sends it', (done) => { nock.disableNetConnect(); const scope = utils .createNock() .get(`/v3/history-with-actions/sub-key/${subscribeKey}/channel/ch1`) .query({ pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`, uuid: 'myUUID', max: '25', include_uuid: 'true', include_message_type: 'true', }) .reply( 200, '{"status":200,"error":false,"error_message":"","channels":{"demo-channel":[{"message":"Hi","timetoken":15610547826970040,"actions":{"receipt":{"read":[{"uuid":"user-7","actionTimetoken":15610547826970044}]}}},{"message":"Hello","timetoken":15610547826970000,"actions":{"reaction":{"smiley_face":[{"uuid":"user-456","actionTimetoken":15610547826970050}]}}}]},"more":{"url":"/v3/history-with-actions/sub-key/s/channel/c?start=15610547826970000&max=98","start":"15610547826970000","max":98}}', { 'content-type': 'text/javascript' }, ); pubnub.fetchMessages({ channels: ['ch1'], includeMessageActions: true }, (status, response) => { try { assert.equal(scope.isDone(), true); assert.equal(status.error, false); assert(response !== null); // TypeScript types system now requires to figure out type of object before using it. assert('more' in response); assert.equal(response.more.url, '/v3/history-with-actions/sub-key/s/channel/c?start=15610547826970000&max=98'); assert.equal(response.more.start, '15610547826970000'); assert.equal(response.more.max, 98); done(); } catch (error) { done(error); } }); }); it('should request 100 messages when count not provided with single channel', (done) => { nock.disableNetConnect(); const scope = utils .createNock() .get(`/v3/history/sub-key/${subscribeKey}/channel/ch1`) .query({ max: '100', pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`, uuid: 'myUUID', include_uuid: 'true', include_message_type: 'true', }) .reply( 200, '{ "error": false, "error_message": "", "channels": { "ch1": [ { "message_type": null, "message": "hello world", "timetoken": "16048329933709932", "uuid": "test-uuid"} ] } }', { 'content-type': 'text/javascript' }, ); pubnub.fetchMessages({ channels: ['ch1'] }, (status) => { try { assert.equal(scope.isDone(), true); assert.equal(status.error, false); done(); } catch (error) { done(error); } }); }); it('should request 25 messages when count not provided with multiple channels', (done) => { nock.disableNetConnect(); const scope = utils .createNock() .get(`/v3/history/sub-key/${subscribeKey}/channel/ch1,ch2`) .query({ max: '25', pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`, uuid: 'myUUID', include_uuid: 'true', include_message_type: 'true', }) .reply( 200, '{ "error": false, "error_message": "", "channels": { "ch1": [ { "message_type": null, "message": "hello world", "timetoken": "16048329933709932", "uuid": "test-uuid"} ] } }', { 'content-type': 'text/javascript' }, ); pubnub.fetchMessages({ channels: ['ch1', 'ch2'] }, (status) => { try { assert.equal(scope.isDone(), true); assert.equal(status.error, false); done(); } catch (error) { done(error); } }); }); it('should request 25 messages when count not provided for history-with-actions', (done) => { nock.disableNetConnect(); const scope = utils .createNock() .get(`/v3/history-with-actions/sub-key/${subscribeKey}/channel/ch1`) .query({ max: '25', pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`, uuid: 'myUUID', include_uuid: 'true', include_message_type: 'true', }) .reply( 200, '{ "error": false, "error_message": "", "channels": { "ch1": [ { "message_type": null, "message": "hello world", "timetoken": "16048329933709932", "uuid": "test-uuid"} ] } }', { 'content-type': 'text/javascript' }, ); pubnub.fetchMessages({ channels: ['ch1'], includeMessageActions: true }, (status) => { try { assert.equal(scope.isDone(), true); assert.equal(status.error, false); done(); } catch (error) { done(error); } }); }); it('should request provided number of messages when count is specified for history-with-actions', (done) => { nock.disableNetConnect(); const scope = utils .createNock() .get(`/v3/history-with-actions/sub-key/${subscribeKey}/channel/ch1`) .query({ max: '10', pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`, uuid: 'myUUID', include_uuid: 'true', include_message_type: 'true', }) .reply( 200, '{ "error": false, "error_message": "", "channels": { "ch1": [ { "message_type": null, "message": "hello world", "timetoken": "16048329933709932", "uuid": "test-uuid"} ] } }', { 'content-type': 'text/javascript' }, ); pubnub.fetchMessages({ channels: ['ch1'], includeMessageActions: true, count: 10 }, (status) => { try { assert.equal(scope.isDone(), true); assert.equal(status.error, false); done(); } catch (error) { done(error); } }); }); it('should request provided number of messages when count is specified for batch history with single channel', (done) => { nock.disableNetConnect(); const scope = utils .createNock() .get(`/v3/history/sub-key/${subscribeKey}/channel/ch1`) .query({ max: '10', pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`, uuid: 'myUUID', include_uuid: 'true', include_message_type: 'true', }) .reply( 200, '{ "error": false, "error_message": "", "channels": { "ch1": [ { "message_type": null, "message": "hello world", "timetoken": "16048329933709932", "uuid": "test-uuid"} ] } }', { 'content-type': 'text/javascript' }, ); pubnub.fetchMessages({ channels: ['ch1'], count: 10 }, (status) => { try { assert.equal(scope.isDone(), true); assert.equal(status.error, false); done(); } catch (error) { done(error); } }); }); it('should request provided number of messages when count is specified for batch history with multiple channels', (done) => { nock.disableNetConnect(); const scope = utils .createNock() .get(`/v3/history/sub-key/${subscribeKey}/channel/ch1,ch2`) .query({ max: '10', pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`, uuid: 'myUUID', include_uuid: 'true', include_message_type: 'true', }) .reply( 200, '{ "error": false, "error_message": "", "channels": { "ch1": [ { "message_type": null, "message": "hello world", "timetoken": "16048329933709932", "uuid": "test-uuid"} ] } }', { 'content-type': 'text/javascript' }, ); pubnub.fetchMessages({ channels: ['ch1', 'ch2'], count: 10 }, (status) => { try { assert.equal(scope.isDone(), true); assert.equal(status.error, false); done(); } catch (error) { done(error); } }); }); it('handles unencrypted payload when cryptomodule configured', (done) => { nock.disableNetConnect(); const scope = utils .createNock() .get(`/v3/history/sub-key/${subscribeKey}/channel/ch1`) .query({ max: '10', pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`, uuid: 'myUUID', include_uuid: 'true', include_message_type: 'true', }) .reply( 200, '{ "channels": { "ch1": [{"message":"hello","timetoken":"11"}, {"message":"hey","timetoken":"12"}] } }', { 'content-type': 'text/javascript' }, ); pubnub.setCipherKey('cipherKey'); pubnub.fetchMessages({ channels: ['ch1'], count: 10 }, (status, response) => { try { assert.equal(status.error, false); assert.deepEqual(response, { channels: { ch1: [ { channel: 'ch1', message: 'hello', timetoken: '11', messageType: undefined, uuid: undefined, error: 'Error while decrypting message content: Decryption error: invalid header version', }, { channel: 'ch1', message: 'hey', timetoken: '12', messageType: undefined, uuid: undefined, error: 'Error while decrypting message content: Decryption error: invalid header version', }, ], }, }); assert.equal(scope.isDone(), true); done(); } catch (error) { done(error); } }); }); });