From bab2dfd0bfa721ce6e553b382f658f310b79f7ad Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 4 Jun 2024 10:29:40 -0300 Subject: [PATCH] wip private api recorder ECO-4821 --- docs/internal/private-api-usage.md | 8 +++ test/common/globals/named_dependencies.js | 4 ++ test/common/modules/private_api_recorder.js | 54 +++++++++++++++++++++ test/realtime/delta.test.js | 11 ++++- test/realtime/encoding.test.js | 17 ++++++- test/realtime/event_emitter.test.js | 5 +- test/realtime/presence.test.js | 50 ++++++++++++++++++- 7 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 test/common/modules/private_api_recorder.js diff --git a/docs/internal/private-api-usage.md b/docs/internal/private-api-usage.md index ec522c0f13..df05551721 100644 --- a/docs/internal/private-api-usage.md +++ b/docs/internal/private-api-usage.md @@ -21,17 +21,25 @@ None ### `test/realtime/delta.test.js` +Marked in code. + - `channel._lastPayload.messageId = null` - to make decoding fail +Note that this test passes an overridden VCDiff decoder, but some SDKs don't use a VCDiff decoder + ### `test/realtime/encoding.test.js` +Marked in code. + - `var BufferUtils = Ably.Realtime.Platform.BufferUtils;` - `var Defaults = Ably.Rest.Platform.Defaults;` - just used for accessing library’s baked-in protocol version, to pass to `request()` ### `test/realtime/presence.test.js` +Marked in code. + - `var createPM = Ably.protocolMessageFromDeserialized;` - `var PresenceMessage = Ably.Realtime.PresenceMessage;` - replacing `channel.sendPresence` with a version that checks the presence message’s client ID diff --git a/test/common/globals/named_dependencies.js b/test/common/globals/named_dependencies.js index 0558b575e7..f406284b6c 100644 --- a/test/common/globals/named_dependencies.js +++ b/test/common/globals/named_dependencies.js @@ -13,5 +13,9 @@ define(function () { shared_helper: { browser: 'test/common/modules/shared_helper', node: 'test/common/modules/shared_helper' }, async: { browser: 'node_modules/async/lib/async' }, chai: { browser: 'node_modules/chai/chai', node: 'node_modules/chai/chai' }, + private_api_recorder: { + browser: 'test/common/modules/private_api_recorder', + node: 'test/common/modules/private_api_recorder', + }, }); }); diff --git a/test/common/modules/private_api_recorder.js b/test/common/modules/private_api_recorder.js new file mode 100644 index 0000000000..b21c94c3b0 --- /dev/null +++ b/test/common/modules/private_api_recorder.js @@ -0,0 +1,54 @@ +'use strict'; + +define([], function () { + const privateAPIIdentifiers = [ + 'modifyChannelLastPayload', + 'BufferUtils.hexEncode', + 'BufferUtils.base64Decode', + 'readDefaults.protocolVersion', + 'protocolMessageFromDeserialized', + 'Utils.mixin', + 'PresenceMessage.fromValues', + 'replaceChannelSendPresence', + 'callChannelSendPresence', + 'listen.connectionManager.transport.active', + 'replace.transport.send', + 'call.transport.send', + 'call.presence.waitSync', + 'read.connectionManager.connectionId', + 'call.presence._myMembers.put', + 'call.channel.sync', + 'replace.channel.attachImpl', + 'call.channel.checkPendingState', + 'call.Platform.nextTick', + 'call.channel.processMessage', + 'call.EventEmitter.emit', + ]; + + class PrivateApiRecorder { + privateAPIUsages = []; + + /** + * Creates a recording context for the current Mocha test case. + * + * @param testCase The value of `this` inside a Mocha test case. + */ + createContext(testCase) { + if (!testCase) { + throw new Error('No test case passed to createContext'); + } + + return { + record: (privateAPIIdentifier) => { + console.log(`recorded private API in test ${testCase.test.fullTitle()}: ${privateAPIIdentifier}`); + if (privateAPIIdentifiers.indexOf(privateAPIIdentifier) == -1) { + throw new Error('Recorded unknown private API:', privateAPIIdentifier); + } + this.privateAPIUsages.push({ testCase, privateAPIIdentifier }); + }, + }; + } + } + + return (module.exports = new PrivateApiRecorder()); +}); diff --git a/test/realtime/delta.test.js b/test/realtime/delta.test.js index fb8f5edb72..f633c7e3df 100644 --- a/test/realtime/delta.test.js +++ b/test/realtime/delta.test.js @@ -1,6 +1,12 @@ 'use strict'; -define(['shared_helper', 'vcdiff-decoder', 'async', 'chai'], function (helper, vcdiffDecoder, async, chai) { +define(['shared_helper', 'vcdiff-decoder', 'async', 'chai', 'private_api_recorder'], function ( + helper, + vcdiffDecoder, + async, + chai, + privateApiRecorder, +) { var expect = chai.expect; var displayError = helper.displayError; var closeAndFinish = helper.closeAndFinish; @@ -130,6 +136,8 @@ define(['shared_helper', 'vcdiff-decoder', 'async', 'chai'], function (helper, v }); it('lastMessageNotFoundRecovery', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + var testName = 'lastMessageNotFoundRecovery'; try { var testVcdiffDecoder = getTestVcdiffDecoder(); @@ -154,6 +162,7 @@ define(['shared_helper', 'vcdiff-decoder', 'async', 'chai'], function (helper, v if (index === 1) { /* Simulate issue */ + context.record('modifyChannelLastPayload'); channel._lastPayload.messageId = null; channel.once('attaching', function (stateChange) { try { diff --git a/test/realtime/encoding.test.js b/test/realtime/encoding.test.js index 5f27673e03..fc1e756c22 100644 --- a/test/realtime/encoding.test.js +++ b/test/realtime/encoding.test.js @@ -1,6 +1,12 @@ 'use strict'; -define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { +define(['ably', 'shared_helper', 'async', 'chai', 'private_api_recorder'], function ( + Ably, + helper, + async, + chai, + privateApiRecorder, +) { var expect = chai.expect; var loadTestData = helper.loadTestData; var BufferUtils = Ably.Realtime.Platform.BufferUtils; @@ -27,6 +33,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async * realtime, and check everything decodes correctly */ it('message_decoding', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + loadTestData(encodingFixturesPath, function (err, testData) { if (err) { done(err); @@ -64,6 +72,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async channel.subscribe(name, function (msg) { try { if (encodingSpec.expectedHexValue) { + privateApiContext.record('BufferUtils.hexEncode'); expect(BufferUtils.hexEncode(msg.data)).to.equal( encodingSpec.expectedHexValue, 'Check data matches', @@ -82,6 +91,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async binarychannel.subscribe(name, function (msg) { try { if (encodingSpec.expectedHexValue) { + privateApiContext.record('BufferUtils.hexEncode'); expect(BufferUtils.hexEncode(msg.data)).to.equal( encodingSpec.expectedHexValue, 'Check data matches', @@ -97,6 +107,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async }); }, function (parallelCb) { + privateApiContext.record('readDefaults.protocolVersion'); whenPromiseSettles( realtime.request( 'post', @@ -128,6 +139,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async * manually, and check everything was encoded correctly */ it('message_encoding', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + loadTestData(encodingFixturesPath, function (err, testData) { if (err) { done(new Error('Unable to get test assets; err = ' + displayError(err))); @@ -161,6 +174,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async var data, name = index.toString(); if (encodingSpec.expectedHexValue) { + privateApiContext.record('BufferUtils.base64Decode'); data = BufferUtils.base64Decode(encodingSpec.data); } else { data = encodingSpec.expectedValue; @@ -179,6 +193,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async eachOfCb(err); return; } + privateApiContext.record('readDefaults.protocolVersion'); whenPromiseSettles( realtime.request('get', channelPath, Defaults.protocolVersion, null, null, null), function (err, resultPage) { diff --git a/test/realtime/event_emitter.test.js b/test/realtime/event_emitter.test.js index 91b7f4f343..faad2aea11 100644 --- a/test/realtime/event_emitter.test.js +++ b/test/realtime/event_emitter.test.js @@ -1,6 +1,6 @@ 'use strict'; -define(['shared_helper', 'chai'], function (helper, chai) { +define(['shared_helper', 'chai', 'private_api_recorder'], function (helper, chai, privateApiRecorder) { var expect = chai.expect; var displayError = helper.displayError; var closeAndFinish = helper.closeAndFinish; @@ -70,6 +70,8 @@ define(['shared_helper', 'chai'], function (helper, chai) { }); it('emitCallsAllCallbacksIgnoringExceptions', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + var realtime = helper.AblyRealtime({ autoConnect: false }), callbackCalled = false, eventEmitter = realtime.connection; @@ -84,6 +86,7 @@ define(['shared_helper', 'chai'], function (helper, chai) { callbackCalled = true; }); + privateApiContext.record('call.EventEmitter.emit'); eventEmitter.emit('custom'); try { expect(callbackCalled, 'Last callback should have been called').to.be.ok; diff --git a/test/realtime/presence.test.js b/test/realtime/presence.test.js index 688d4b6b07..3b38c960b5 100644 --- a/test/realtime/presence.test.js +++ b/test/realtime/presence.test.js @@ -1,6 +1,12 @@ 'use strict'; -define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { +define(['ably', 'shared_helper', 'async', 'chai', 'private_api_recorder'], function ( + Ably, + helper, + async, + chai, + privateApiRecorder, +) { var expect = chai.expect; var utils = helper.Utils; var createPM = Ably.protocolMessageFromDeserialized; @@ -377,6 +383,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async * PresenceMessage has extras. Then do the same for leaving presence. */ it('presenceMessageExtras', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + var clientRealtime = helper.AblyRealtime({ clientId: testClientId, tokenDetails: authToken }); var channelName = 'presenceEnterWithExtras'; var clientChannel = clientRealtime.channels.get(channelName); @@ -401,6 +409,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async } cb(); }); + + privateApiContext.record('PresenceMessage.fromValues'); presence.enter( PresenceMessage.fromValues({ extras: { headers: { key: 'value' } }, @@ -421,6 +431,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async } cb(); }); + privateApiContext.record('PresenceMessage.fromValues'); presence.leave( PresenceMessage.fromValues({ extras: { headers: { otherKey: 'otherValue' } }, @@ -1153,12 +1164,15 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async * Client ID is implicit in the connection so should not be sent for current client operations */ it('presenceClientIdIsImplicit', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + var clientId = 'implicitClient', client = helper.AblyRealtime({ clientId: clientId }); var channel = client.channels.get('presenceClientIdIsImplicit'), presence = channel.presence; + privateApiContext.record('replaceChannelSendPresence'); var originalSendPresence = channel.sendPresence; channel.sendPresence = function (presence, callback) { try { @@ -1167,6 +1181,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async closeAndFinish(done, client, err); return; } + privateApiContext.record('callChannelSendPresence'); originalSendPresence.apply(channel, arguments); }; @@ -1195,6 +1210,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async * Check that encodable presence messages are encoded correctly */ it('presenceEncoding', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + var data = { foo: 'bar' }, encodedData = JSON.stringify(data), options = { @@ -1204,13 +1221,16 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async transports: [helper.bestTransport], }; + privateApiContext.record('Utils.mixin'); var realtimeBin = helper.AblyRealtime(utils.mixin(options, { useBinaryProtocol: true })); var realtimeJson = helper.AblyRealtime(utils.mixin(options, { useBinaryProtocol: false })); var runTest = function (realtime, callback) { + privateApiContext.record('listen.connectionManager.transport.active'); realtime.connection.connectionManager.once('transport.active', function (transport) { var originalSend = transport.send; + privateApiContext.record('replace.transport.send'); transport.send = function (message) { if (message.action === 14) { /* Message is formatted for Ably by the toJSON method, so need to @@ -1224,9 +1244,11 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async callback(err); return; } + privateApiContext.record('replace.transport.send'); transport.send = originalSend; callback(); } + privateApiContext.record('call.transport.send'); originalSend.apply(transport, arguments); }; @@ -1504,6 +1526,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async * member to be sent to realtime and, with luck, make its way into the normal * presence set */ it('presence_auto_reenter', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + var channelName = 'presence_auto_reenter'; var realtime = helper.AblyRealtime(); var channel = realtime.channels.get(channelName); @@ -1520,6 +1544,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async }, function (cb) { if (!channel.presence.syncComplete) { + privateApiContext.record('call.presence.waitSync'); channel.presence.members.waitSync(cb); } else { cb(); @@ -1534,7 +1559,9 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async }, function (cb) { /* inject an additional member into the myMember set, then force a suspended state */ + privateApiContext.record('read.connectionManager.connectionId'); var connId = realtime.connection.connectionManager.connectionId; + privateApiContext.record('call.presence._myMembers.put'); channel.presence._myMembers.put({ action: 'enter', clientId: 'two', @@ -1562,8 +1589,10 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async * that realtime will feel it necessary to do a sync - if it doesn't, * we request one */ if (channel.presence.syncComplete) { + privateApiContext.record('call.channel.sync'); channel.sync(); } + privateApiContext.record('call.presence.waitSync'); channel.presence.members.waitSync(cb); }, function (cb) { @@ -1613,6 +1642,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async /* RTP17e * Test failed presence auto-re-entering */ it.skip('presence_failed_auto_reenter', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + var channelName = 'presence_failed_auto_reenter', realtime, channel, @@ -1641,6 +1672,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async }, function (cb) { if (!channel.presence.syncComplete) { + privateApiContext.record('call.presence.waitSync'); channel.presence.members.waitSync(cb); } else { cb(); @@ -1659,7 +1691,9 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async }, function (cb) { /* inject an additional member into the myMember set, then force a suspended state */ + privateApiContext.record('read.connectionManager.connectionId'); var connId = realtime.connection.connectionManager.connectionId; + privateApiContext.record('call.presence._myMembers.put'); channel.presence._myMembers.put({ action: 'enter', clientId: 'me', @@ -1708,6 +1742,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async /* Enter ten clients while attaching, finish the attach, check they were all entered correctly */ it('multiple_pending', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + var realtime = helper.AblyRealtime(), channel = realtime.channels.get('multiple_pending'), originalAttachImpl = channel.attachImpl; @@ -1721,6 +1757,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async }, function (cb) { /* stub out attachimpl */ + privateApiContext.record('replace.channel.attachImpl'); channel.attachImpl = function () {}; channel.attach(); @@ -1728,13 +1765,17 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async channel.presence.enterClient('client_' + i.toString(), i.toString()); } + privateApiContext.record('replace.channel.attachImpl'); channel.attachImpl = originalAttachImpl; + + privateApiContext.record('call.channel.checkPendingState'); channel.checkPendingState(); /* Now just wait for an enter. One enter implies all, they'll all be * sent in one protocol message */ channel.presence.subscribe('enter', function () { channel.presence.unsubscribe('enter'); + privateApiContext.record('call.Platform.nextTick'); Ably.Realtime.Platform.Config.nextTick(cb); }); }, @@ -1760,6 +1801,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async * Check that a LEAVE message is published for anyone in the local presence * set but missing from a sync */ it('leave_published_for_member_missing_from_sync', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + var realtime = helper.AblyRealtime({ transports: helper.availableTransports }), continuousClientId = 'continuous', goneClientId = 'gone', @@ -1804,6 +1847,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async }, function (cb) { /* Inject an additional member locally */ + privateApiContext.record('call.channel.processMessage'); channel .processMessage({ action: 14, @@ -1847,6 +1891,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async } cb(); }); + privateApiContext.record('call.channel.sync'); channel.sync(); }, function (cb) { @@ -1875,6 +1920,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async * Check that a LEAVE message is published for anyone in the local presence * set if get an ATTACHED with no HAS_PRESENCE */ it('leave_published_for_members_on_presenceless_attached', function (done) { + const privateApiContext = privateApiRecorder.createContext(this); + var realtime = helper.AblyRealtime(), channelName = 'leave_published_for_members_on_presenceless_attached', channel = realtime.channels.get(channelName), @@ -1936,6 +1983,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async cb(); }); /* Inject an ATTACHED with RESUMED and HAS_PRESENCE both false */ + privateApiContext.record('protocolMessageFromDeserialized'); channel.processMessage( createPM({ action: 11,