Files

325 lines
13 KiB
TypeScript

import { expect } from 'chai';
// eslint-disable-next-line import/order
import Sinon, * as sinon from 'sinon';
import { SignalService } from '../../../../protobuf';
import { OpenGroupMessageV2 } from '../../../../session/apis/open_group_api/opengroupV2/OpenGroupMessageV2';
import { OpenGroupPollingUtils } from '../../../../session/apis/open_group_api/opengroupV2/OpenGroupPollingUtils';
import { SogsBlinding } from '../../../../session/apis/open_group_api/sogsv3/sogsBlinding';
import { BatchRequests } from '../../../../session/apis/snode_api/batchRequest';
import { SnodeNamespaces } from '../../../../session/apis/snode_api/namespaces';
import { Onions } from '../../../../session/apis/snode_api/onions';
import { ConvoHub } from '../../../../session/conversations/ConversationController';
import { OnionSending } from '../../../../session/onions/onionSend';
import { OnionV4 } from '../../../../session/onions/onionv4';
import { MessageSender } from '../../../../session/sending';
import { OutgoingRawMessage } from '../../../../session/types';
import { MessageUtils, UserUtils } from '../../../../session/utils';
import { fromBase64ToArrayBuffer } from '../../../../session/utils/String';
import { TestUtils } from '../../../test-utils';
import {
TypedStub,
stubData,
stubLibSessionWorker,
stubUtilWorker,
stubValidSnodeSwarm,
stubWindowLog,
} from '../../../test-utils/utils';
import { NetworkTime } from '../../../../util/NetworkTime';
import { longOrNumberToNumber } from '../../../../types/long/longOrNumberToNumber';
const proGeneratedPrivKeyHex =
'01234567890123456789012345678901234567890123456789012345678901230123456789012345678901234567890123456789012345678901234567890123';
describe('MessageSender', () => {
beforeEach(async () => {
TestUtils.stubWindowLog();
TestUtils.stubWindowFeatureFlags();
ConvoHub.use().reset();
TestUtils.stubData('getItemById').resolves();
stubData('getAllConversations').resolves([]);
stubData('saveConversation').resolves();
await ConvoHub.use().load();
Sinon.stub(UserUtils, 'getProRotatingPrivateKeyHex').resolves(proGeneratedPrivKeyHex);
});
afterEach(() => {
sinon.restore();
});
describe('send', () => {
let doSnodeBatchRequestStub: TypedStub<typeof BatchRequests, 'doSnodeBatchRequestNoRetries'>;
beforeEach(async () => {
doSnodeBatchRequestStub = Sinon.stub(
BatchRequests,
'doSnodeBatchRequestNoRetries'
).resolves();
stubData('getMessageById').resolves();
const userKeys = await TestUtils.generateUserKeyPairs();
stubWindowLog();
Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(userKeys.x25519KeyPair.pubkeyHex);
Sinon.stub(UserUtils, 'getIdentityKeyPair').resolves({
privKey: userKeys.x25519KeyPair.privKey,
pubKey: userKeys.x25519KeyPair.pubKey,
ed25519KeyPair: userKeys.ed25519KeyPair,
});
});
describe('retry', () => {
let rawMessage: OutgoingRawMessage;
beforeEach(async () => {
rawMessage = MessageUtils.toRawMessage(
TestUtils.generateFakePubKey(),
TestUtils.generateVisibleMessage(),
SnodeNamespaces.Default
);
});
it('should only call lokiMessageAPI once if no errors occurred', async () => {
stubValidSnodeSwarm();
stubLibSessionWorker(null);
TestUtils.stubMultiEncryptWrapper();
await MessageSender.sendSingleMessage({
message: rawMessage,
attempts: 3,
retryMinTimeout: 10,
isSyncMessage: false,
abortSignal: null,
});
expect(doSnodeBatchRequestStub.callCount).to.equal(1);
});
it('should only retry the specified amount of times before throwing', async () => {
stubValidSnodeSwarm();
TestUtils.stubMultiEncryptWrapper();
doSnodeBatchRequestStub.throws(new Error('API error'));
const attempts = 2;
const promise = MessageSender.sendSingleMessage({
message: rawMessage,
attempts,
retryMinTimeout: 10,
isSyncMessage: false,
abortSignal: null,
});
await expect(promise).is.rejectedWith('API error');
expect(doSnodeBatchRequestStub.callCount).to.equal(attempts);
});
it('should not throw error if successful send occurs within the retry limit', async () => {
stubValidSnodeSwarm();
TestUtils.stubMultiEncryptWrapper();
doSnodeBatchRequestStub.onFirstCall().throws(new Error('API error'));
await MessageSender.sendSingleMessage({
message: rawMessage,
attempts: 3,
retryMinTimeout: 10,
isSyncMessage: false,
abortSignal: null,
});
expect(doSnodeBatchRequestStub.callCount).to.equal(2);
});
});
describe('logic', () => {
it('should pass the correct values to lokiMessageAPI', async () => {
TestUtils.setupTestWithSending();
TestUtils.stubMultiEncryptWrapper();
const device = TestUtils.generateFakePubKey();
const visibleMessage = TestUtils.generateVisibleMessage();
Sinon.stub(ConvoHub.use(), 'get').returns(undefined as any);
const rawMessage = MessageUtils.toRawMessage(
device,
visibleMessage,
SnodeNamespaces.Default
);
await MessageSender.sendSingleMessage({
message: rawMessage,
attempts: 3,
retryMinTimeout: 10,
isSyncMessage: false,
abortSignal: null,
});
const args = doSnodeBatchRequestStub.getCall(0).args;
expect(args[0].associatedWith).to.equal(device.key);
const firstArg = args[0];
expect(firstArg.subRequests.length).to.equal(1);
const firstSubRequest = firstArg.subRequests[0];
if (firstSubRequest.method !== 'store') {
throw new Error('expected a store request with data');
}
// expect(args[3]).to.equal(visibleMessage.timestamp); the timestamp is overwritten on sending by the network clock offset
expect(firstSubRequest.params.ttl).to.equal(visibleMessage.ttl());
expect(firstSubRequest.params.pubkey).to.equal(device.key);
expect(firstSubRequest.params.namespace).to.equal(SnodeNamespaces.Default);
// the request timestamp is always used fresh with the offset as the request will be denied with a 406 otherwise (clock out of sync)
expect(firstSubRequest.params.timestamp).to.be.above(Date.now() - 10);
expect(firstSubRequest.params.timestamp).to.be.below(Date.now() + 10);
});
it('should correctly build the envelope and override the request timestamp but not the msg one', async () => {
TestUtils.setupTestWithSending();
TestUtils.stubMultiEncryptWrapper();
// This test assumes the encryption stub returns the plainText passed into it.
const device = TestUtils.generateFakePubKey();
Sinon.stub(ConvoHub.use(), 'get').returns(undefined as any);
const visibleMessage = TestUtils.generateVisibleMessage();
const rawMessage = MessageUtils.toRawMessage(
device,
visibleMessage,
SnodeNamespaces.Default
);
const offset = 200000;
Sinon.stub(NetworkTime, 'getLatestTimestampOffset').returns(offset);
await MessageSender.sendSingleMessage({
message: rawMessage,
attempts: 3,
retryMinTimeout: 10,
isSyncMessage: false,
abortSignal: null,
});
const firstArg = doSnodeBatchRequestStub.getCall(0).args[0];
const firstSubRequest = firstArg.subRequests[0];
if (firstSubRequest.method !== 'store') {
throw new Error('expected a store request with data');
}
const data = fromBase64ToArrayBuffer(firstSubRequest.params.data);
const webSocketMessage = SignalService.WebSocketMessage.decode(new Uint8Array(data));
expect(webSocketMessage.request?.body).to.not.equal(
undefined,
'Request body should not be undefined'
);
expect(webSocketMessage.request?.body).to.not.equal(
null,
'Request body should not be null'
);
const envelope = SignalService.Envelope.decode(
webSocketMessage.request?.body as Uint8Array
);
expect(envelope.source).to.equal('');
// the timestamp in the message is not overridden on sending as it should be set with the network offset when created.
// we need that timestamp to not be overridden as the signature of the message depends on it.
const decodedTimestampFromSending = longOrNumberToNumber(envelope.timestamp);
expect(decodedTimestampFromSending).to.be.eq(visibleMessage.createAtNetworkTimestamp);
});
});
});
describe('sendToOpenGroupV2', () => {
beforeEach(async () => {
const userKeys = await TestUtils.generateUserKeyPairs();
stubWindowLog();
Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(userKeys.x25519KeyPair.pubkeyHex);
Sinon.stub(UserUtils, 'getIdentityKeyPair').resolves({
privKey: userKeys.x25519KeyPair.privKey,
pubKey: userKeys.x25519KeyPair.pubKey,
ed25519KeyPair: userKeys.ed25519KeyPair,
});
Sinon.stub(SogsBlinding, 'getSogsSignature').resolves(new Uint8Array());
stubUtilWorker('arrayBufferToStringBase64', 'ba64');
Sinon.stub(OnionSending, 'getOnionPathForSending').resolves([{}] as any);
Sinon.stub(OnionSending, 'endpointRequiresDecoding').returnsArg(0);
stubData('getGuardNodes').resolves([]);
Sinon.stub(OpenGroupPollingUtils, 'getAllValidRoomInfos').returns([
{ roomId: 'room', serverPublicKey: 'whatever', serverUrl: 'serverUrl' },
]);
Sinon.stub(OpenGroupPollingUtils, 'getOurOpenGroupHeaders').resolves({
'X-SOGS-Pubkey': '00bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc',
'X-SOGS-Timestamp': '1642472103',
'X-SOGS-Nonce': 'CdB5nyKVmQGCw6s0Bvv8Ww==',
'X-SOGS-Signature':
'gYqpWZX6fnF4Gb2xQM3xaXs0WIYEI49+B8q4mUUEg8Rw0ObaHUWfoWjMHMArAtP9QlORfiydsKWz1o6zdPVeCQ==',
});
Sinon.stub(OpenGroupMessageV2, 'fromJson').resolves();
});
afterEach(() => {
Sinon.restore();
});
it('should call sendOnionRequestHandlingSnodeEjectStub', async () => {
TestUtils.stubMultiEncryptWrapper();
const sendOnionRequestHandlingSnodeEjectStub = Sinon.stub(
Onions,
'sendOnionRequestHandlingSnodeEjectNoRetries'
).resolves({} as any);
Sinon.stub(OnionV4, 'decodeV4Response').returns({
metadata: { code: 200 },
body: {},
bodyBinary: new Uint8Array(),
bodyContentType: 'a',
});
Sinon.stub(OnionSending, 'getMinTimeoutForSogs').returns(5);
const message = TestUtils.generateOpenGroupVisibleMessage();
const roomInfos = TestUtils.generateOpenGroupV2RoomInfos();
await MessageSender.sendToOpenGroupV2(message, roomInfos, false, []);
expect(sendOnionRequestHandlingSnodeEjectStub.callCount).to.eq(1);
});
it('should retry sendOnionRequestHandlingSnodeEjectStub ', async () => {
TestUtils.stubMultiEncryptWrapper();
const message = TestUtils.generateOpenGroupVisibleMessage();
const roomInfos = TestUtils.generateOpenGroupV2RoomInfos();
Sinon.stub(Onions, 'sendOnionRequestHandlingSnodeEjectNoRetries').resolves({} as any);
Sinon.stub(OnionSending, 'getMinTimeoutForSogs').returns(5);
const decodeV4responseStub = Sinon.stub(OnionV4, 'decodeV4Response');
decodeV4responseStub.throws('whatever');
decodeV4responseStub.onThirdCall().returns({
metadata: { code: 200 },
body: {},
bodyBinary: new Uint8Array(),
bodyContentType: 'a',
});
await MessageSender.sendToOpenGroupV2(message, roomInfos, false, []);
expect(decodeV4responseStub.callCount).to.eq(3);
});
it('should not retry more than 3 sendOnionRequestHandlingSnodeEjectStub ', async () => {
const message = TestUtils.generateOpenGroupVisibleMessage();
const roomInfos = TestUtils.generateOpenGroupV2RoomInfos();
Sinon.stub(Onions, 'sendOnionRequestHandlingSnodeEjectNoRetries').resolves({} as any);
Sinon.stub(OnionSending, 'getMinTimeoutForSogs').returns(5);
const decodeV4responseStub = Sinon.stub(OnionV4, 'decodeV4Response');
decodeV4responseStub.throws('whatever');
decodeV4responseStub.onCall(4).returns({
metadata: { code: 200 },
body: {},
bodyBinary: new Uint8Array(),
bodyContentType: 'a',
});
try {
await MessageSender.sendToOpenGroupV2(message, roomInfos, false, []);
} catch (e) {}
// we made the fourth call success, but we should not get there. We should stop at 3 the retries (1+2)
expect(decodeV4responseStub.calledThrice);
});
});
});