UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

509 lines (434 loc) 17.7 kB
/* global describe, it, beforeEach */ import assert from 'assert'; import { PublishRequest, PublishParameters, PublishResponse } from '../../../src/core/endpoints/publish'; import { TransportMethod } from '../../../src/core/types/transport-request'; import { TransportResponse } from '../../../src/core/types/transport-response'; import RequestOperation from '../../../src/core/constants/operations'; import { ICryptoModule } from '../../../src/core/interfaces/crypto-module'; import { KeySet } from '../../../src/core/types/api'; import { encode } from '../../../src/core/components/base64_codec'; describe('PublishRequest', () => { let defaultKeySet: KeySet; let defaultParameters: PublishParameters & { keySet: KeySet }; beforeEach(() => { defaultKeySet = { publishKey: 'test_publish_key', subscribeKey: 'test_subscribe_key', }; defaultParameters = { channel: 'test_channel', message: { test: 'message' }, keySet: defaultKeySet, }; }); describe('validation', () => { it('should validate required parameters', () => { // Test missing channel const requestWithoutChannel = new PublishRequest({ ...defaultParameters, channel: '', }); assert.equal(requestWithoutChannel.validate(), "Missing 'channel'"); // Test missing message const requestWithoutMessage = new PublishRequest({ ...defaultParameters, message: undefined as any, }); assert.equal(requestWithoutMessage.validate(), "Missing 'message'"); // Test missing publishKey const requestWithoutPublishKey = new PublishRequest({ ...defaultParameters, keySet: { ...defaultKeySet, publishKey: '' }, }); assert.equal(requestWithoutPublishKey.validate(), "Missing 'publishKey'"); // Test valid parameters const validRequest = new PublishRequest(defaultParameters); assert.equal(validRequest.validate(), undefined); }); }); describe('operation', () => { it('should return PNPublishOperation', () => { const request = new PublishRequest(defaultParameters); assert.equal(request.operation(), RequestOperation.PNPublishOperation); }); }); describe('URL construction', () => { it('should construct GET URL with encoded payload and channel', () => { const request = new PublishRequest({ ...defaultParameters, channel: 'test channel', message: { test: 'message' }, sendByPost: false, }); const transportRequest = request.request(); assert.equal(transportRequest.method, TransportMethod.GET); const expectedPath = `/publish/${defaultKeySet.publishKey}/${defaultKeySet.subscribeKey}/0/test%20channel/0/${encodeURIComponent(JSON.stringify({ test: 'message' }))}`; assert.equal(transportRequest.path, expectedPath); }); it('should construct POST URL without payload in path', () => { const request = new PublishRequest({ ...defaultParameters, sendByPost: true, }); const transportRequest = request.request(); assert.equal(transportRequest.method, TransportMethod.POST); const expectedPath = `/publish/${defaultKeySet.publishKey}/${defaultKeySet.subscribeKey}/0/${defaultParameters.channel}/0`; assert.equal(transportRequest.path, expectedPath); }); it('should encode special character channels', () => { const specialChannel = 'a b/#'; const request = new PublishRequest({ ...defaultParameters, channel: specialChannel, sendByPost: false, }); const transportRequest = request.request(); const encodedChannel = encodeURIComponent(specialChannel).replace(/[!~*'()]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); assert(transportRequest.path.includes(encodedChannel)); }); }); describe('POST method handling', () => { it('should set correct headers for POST requests', () => { const request = new PublishRequest({ ...defaultParameters, sendByPost: true, }); const transportRequest = request.request(); assert.equal(transportRequest.headers?.['Content-Type'], 'application/json'); assert.equal(transportRequest.headers?.['Accept-Encoding'], 'gzip, deflate'); }); it('should include message in body for POST requests', () => { const testMessage = { test: 'data' }; const request = new PublishRequest({ ...defaultParameters, message: testMessage, sendByPost: true, }); const transportRequest = request.request(); assert.equal(transportRequest.body, JSON.stringify(testMessage)); }); it('should not include headers for GET requests', () => { const request = new PublishRequest({ ...defaultParameters, sendByPost: false, }); const transportRequest = request.request(); assert.notEqual(transportRequest.headers?.['Content-Type'], 'application/json'); }); }); describe('query parameters mapping', () => { it('should map storeInHistory parameter', () => { // Test storeInHistory: true const requestStoreTrue = new PublishRequest({ ...defaultParameters, storeInHistory: true, }); const queryParams1 = requestStoreTrue.request().queryParameters; assert.equal(queryParams1?.store, '1'); // Test storeInHistory: false const requestStoreFalse = new PublishRequest({ ...defaultParameters, storeInHistory: false, }); const queryParams2 = requestStoreFalse.request().queryParameters; assert.equal(queryParams2?.store, '0'); // Test storeInHistory: undefined (should not be present) const requestStoreUndefined = new PublishRequest(defaultParameters); const queryParams3 = requestStoreUndefined.request().queryParameters; assert.equal(queryParams3?.store, undefined); }); it('should map ttl parameter', () => { const request = new PublishRequest({ ...defaultParameters, ttl: 24, }); const queryParams4 = request.request().queryParameters; assert.equal(queryParams4?.ttl, 24); }); it('should map replicate parameter when false', () => { const request = new PublishRequest({ ...defaultParameters, replicate: false, }); const queryParams5 = request.request().queryParameters; assert.equal(queryParams5?.norep, 'true'); // Should not be present when true const requestReplicateTrue = new PublishRequest({ ...defaultParameters, replicate: true, }); const queryParams6 = requestReplicateTrue.request().queryParameters; assert.equal(queryParams6?.norep, undefined); }); it('should map meta parameter when object', () => { const metaData = { userId: '123', type: 'chat' }; const request = new PublishRequest({ ...defaultParameters, meta: metaData, }); const queryParams7 = request.request().queryParameters; assert.equal(queryParams7?.meta, JSON.stringify(metaData)); // Should not be present when not an object const requestNonObjectMeta = new PublishRequest({ ...defaultParameters, meta: 'string_meta' as any, }); const queryParams8 = requestNonObjectMeta.request().queryParameters; assert.equal(queryParams8?.meta, undefined); }); it('should map custom_message_type parameter', () => { const customType = 'test-message-type'; const request = new PublishRequest({ ...defaultParameters, customMessageType: customType, }); const queryParams9 = request.request().queryParameters; assert.equal(queryParams9?.custom_message_type, customType); }); it('should combine multiple query parameters', () => { const request = new PublishRequest({ ...defaultParameters, storeInHistory: false, ttl: 12, replicate: false, meta: { test: 'meta' }, customMessageType: 'test-type', }); const queryParams = request.request().queryParameters; assert.equal(queryParams?.store, '0'); assert.equal(queryParams?.ttl, 12); assert.equal(queryParams?.norep, 'true'); assert.equal(queryParams?.meta, JSON.stringify({ test: 'meta' })); assert.equal(queryParams?.custom_message_type, 'test-type'); }); }); describe('encryption handling', () => { const mockCryptoModule: ICryptoModule = { set logger(_logger: any) {}, encrypt: (data: string) => `encrypted_${data}`, decrypt: (data: string | ArrayBuffer) => data.toString().replace('encrypted_', ''), encryptFile: undefined as any, decryptFile: undefined as any, }; it('should encrypt message with cryptoModule for GET', () => { const testMessage = { secret: 'data' }; const request = new PublishRequest({ ...defaultParameters, message: testMessage, crypto: mockCryptoModule, sendByPost: false, }); const transportRequest = request.request(); const expectedEncryptedMessage = JSON.stringify(`encrypted_${JSON.stringify(testMessage)}`); assert(transportRequest.path.includes(encodeURIComponent(expectedEncryptedMessage))); }); it('should encrypt message with cryptoModule for POST', () => { const testMessage = { secret: 'data' }; const request = new PublishRequest({ ...defaultParameters, message: testMessage, crypto: mockCryptoModule, sendByPost: true, }); const transportRequest = request.request(); const expectedEncryptedMessage = JSON.stringify(`encrypted_${JSON.stringify(testMessage)}`); assert.equal(transportRequest.body, expectedEncryptedMessage); }); it('should handle ArrayBuffer encryption result', () => { const arrayBufferCrypto: ICryptoModule = { set logger(_logger: any) {}, encrypt: (data: string) => { const buffer = new ArrayBuffer(8); const view = new Uint8Array(buffer); view.set([1, 2, 3, 4, 5, 6, 7, 8]); return buffer; }, decrypt: undefined as any, encryptFile: undefined as any, decryptFile: undefined as any, }; const request = new PublishRequest({ ...defaultParameters, crypto: arrayBufferCrypto, sendByPost: true, }); const transportRequest = request.request(); const expectedEncodedBuffer = JSON.stringify(encode(new ArrayBuffer(8))); // We can't easily compare ArrayBuffer content, so just verify it's a string assert.equal(typeof transportRequest.body, 'string'); }); it('should handle encryption failure', () => { const failingCrypto: ICryptoModule = { set logger(_logger: any) {}, encrypt: () => { throw new Error('Encryption failed'); }, decrypt: undefined as any, encryptFile: undefined as any, decryptFile: undefined as any, }; assert.throws(() => { new PublishRequest({ ...defaultParameters, crypto: failingCrypto, }).request(); }, /Encryption failed/); }); }); describe('payload types support', () => { it('should handle string payloads', () => { const request = new PublishRequest({ ...defaultParameters, message: 'test string', sendByPost: false, }); const transportRequest = request.request(); assert(transportRequest.path.includes(encodeURIComponent(JSON.stringify('test string')))); }); it('should handle number payloads', () => { const request = new PublishRequest({ ...defaultParameters, message: 42, sendByPost: false, }); const transportRequest = request.request(); assert(transportRequest.path.includes(encodeURIComponent(JSON.stringify(42)))); }); it('should handle boolean payloads', () => { const request = new PublishRequest({ ...defaultParameters, message: true, sendByPost: false, }); const transportRequest = request.request(); assert(transportRequest.path.includes(encodeURIComponent(JSON.stringify(true)))); }); it('should handle object payloads', () => { const objectMessage = { key: 'value', nested: { prop: 123 } }; const request = new PublishRequest({ ...defaultParameters, message: objectMessage, sendByPost: false, }); const transportRequest = request.request(); assert(transportRequest.path.includes(encodeURIComponent(JSON.stringify(objectMessage)))); }); it('should handle array payloads', () => { const arrayMessage = [1, 2, 'three', { four: 4 }]; const request = new PublishRequest({ ...defaultParameters, message: arrayMessage, sendByPost: false, }); const transportRequest = request.request(); assert(transportRequest.path.includes(encodeURIComponent(JSON.stringify(arrayMessage)))); }); }); describe('response parsing', () => { it('should parse timetoken from service response tuple', async () => { const request = new PublishRequest(defaultParameters); const mockResponse: TransportResponse = { status: 200, url: 'https://test.pubnub.com', headers: { 'content-type': 'application/json' }, body: new TextEncoder().encode('[1, "Sent", "14647523059145592"]'), }; const parsedResponse = await request.parse(mockResponse); assert.equal(parsedResponse.timetoken, '14647523059145592'); }); it('should handle different service response formats', async () => { const request = new PublishRequest(defaultParameters); const mockResponse: TransportResponse = { status: 200, url: 'https://test.pubnub.com', headers: { 'content-type': 'application/json' }, body: new TextEncoder().encode('[0, "Failed", "123456789"]'), }; const parsedResponse = await request.parse(mockResponse); assert.equal(parsedResponse.timetoken, '123456789'); }); }); describe('request configuration', () => { it('should default to GET method when sendByPost is false', () => { const request = new PublishRequest({ ...defaultParameters, sendByPost: false, }); const transportRequest = request.request(); assert.equal(transportRequest.method, TransportMethod.GET); }); it('should use POST method when sendByPost is true', () => { const request = new PublishRequest({ ...defaultParameters, sendByPost: true, }); const transportRequest = request.request(); assert.equal(transportRequest.method, TransportMethod.POST); }); it('should default sendByPost to false', () => { const request = new PublishRequest(defaultParameters); const transportRequest = request.request(); assert.equal(transportRequest.method, TransportMethod.GET); }); it('should set compressible flag based on sendByPost', () => { const getRequest = new PublishRequest({ ...defaultParameters, sendByPost: false, }); assert.equal(getRequest.request().compressible, false); const postRequest = new PublishRequest({ ...defaultParameters, sendByPost: true, }); assert.equal(postRequest.request().compressible, true); }); }); describe('concurrent operations simulation', () => { it('should handle multiple publish requests with different methods', () => { const getRequest = new PublishRequest({ ...defaultParameters, channel: 'channel1', message: 'GET message', sendByPost: false, }); const postRequest = new PublishRequest({ ...defaultParameters, channel: 'channel2', message: 'POST message', sendByPost: true, }); const getTransportRequest = getRequest.request(); const postTransportRequest = postRequest.request(); // Verify they maintain their individual configurations assert.equal(getTransportRequest.method, TransportMethod.GET); assert.equal(postTransportRequest.method, TransportMethod.POST); assert(getTransportRequest.path.includes('channel1')); assert(postTransportRequest.path.includes('channel2')); assert(getTransportRequest.path.includes(encodeURIComponent(JSON.stringify('GET message')))); assert.equal(postTransportRequest.body, JSON.stringify('POST message')); }); it('should maintain request isolation', () => { const requests = Array.from({ length: 5 }, (_, i) => new PublishRequest({ ...defaultParameters, channel: `channel_${i}`, message: `message_${i}`, sendByPost: i % 2 !== 0, // Alternate between GET and POST }) ); requests.forEach((request, index) => { const transportRequest = request.request(); assert(transportRequest.path.includes(`channel_${index}`)); if (index % 2 === 0) { // GET request assert.equal(transportRequest.method, TransportMethod.GET); assert(transportRequest.path.includes(encodeURIComponent(JSON.stringify(`message_${index}`)))); } else { // POST request assert.equal(transportRequest.method, TransportMethod.POST); assert.equal(transportRequest.body, JSON.stringify(`message_${index}`)); } }); }); }); });