pubnub
Version:
Publish & Subscribe Real-time Messaging with PubNub
755 lines (667 loc) • 27.2 kB
text/typescript
/* 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);
});
});