fix: first working test ios to desktop

still have some tests to fix
pull/2873/head
Audric Ackermann 2 years ago
parent ceffa1e13b
commit 9492fdc51e

@ -30,14 +30,10 @@ import { assertUnreachable } from '../types/sqlSharedTypes';
import { BlockedNumberController } from '../util'; import { BlockedNumberController } from '../util';
import { ReadReceipts } from '../util/readReceipts'; import { ReadReceipts } from '../util/readReceipts';
import { Storage } from '../util/storage'; import { Storage } from '../util/storage';
import { ContactsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
import { handleCallMessage } from './callMessage'; import { handleCallMessage } from './callMessage';
import { getAllCachedECKeyPair, sentAtMoreRecentThanWrapper } from './closedGroups'; import { getAllCachedECKeyPair, sentAtMoreRecentThanWrapper } from './closedGroups';
import { ECKeyPair } from './keypairs'; import { ECKeyPair } from './keypairs';
import {
ContactsWrapperActions,
MetaGroupWrapperActions,
} from '../webworker/workers/browser/libsession_worker_interface';
import { PreConditionFailed } from '../session/utils/errors';
export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) { export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) {
try { try {
@ -58,27 +54,6 @@ export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageH
} }
} }
async function decryptForGroupV2(envelope: EnvelopePlus) {
window?.log?.info('received closed group message v2');
try {
const groupPk = envelope.source;
if (!PubKey.isClosedGroupV2(groupPk)) {
throw new PreConditionFailed('decryptForGroupV2: not a 03 prefixed group');
}
const decrypted = await MetaGroupWrapperActions.decryptMessage(groupPk, envelope.content);
// the receiving pipeline relies on the envelope.senderIdentity field to know who is the author of a message
// eslint-disable-next-line no-param-reassign
envelope.senderIdentity = decrypted.pubkeyHex;
return decrypted.plaintext;
} catch (e) {
window.log.warn('failed to decrypt message with error: ', e.message);
return null;
}
}
async function decryptForClosedGroup(envelope: EnvelopePlus) { async function decryptForClosedGroup(envelope: EnvelopePlus) {
window?.log?.info('received closed group message'); window?.log?.info('received closed group message');
try { try {
@ -291,10 +266,11 @@ async function decrypt(envelope: EnvelopePlus): Promise<any> {
break; break;
case SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE: case SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE:
if (PubKey.isClosedGroupV2(envelope.source)) { if (PubKey.isClosedGroupV2(envelope.source)) {
plaintext = await decryptForGroupV2(envelope); // groupv2 messages are decrypted way earlier than this via libsession, and what we get here is already decrypted
} else { return envelope.content;
plaintext = await decryptForClosedGroup(envelope);
} }
plaintext = await decryptForClosedGroup(envelope);
break; break;
default: default:
assertUnreachable(envelope.type, `Unknown message type:${envelope.type}`); assertUnreachable(envelope.type, `Unknown message type:${envelope.type}`);

@ -1,6 +1,6 @@
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
import _, { isEmpty, last } from 'lodash';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import _ from 'lodash';
import { EnvelopePlus } from './types'; import { EnvelopePlus } from './types';
@ -70,18 +70,22 @@ function queueSwarmEnvelope(envelope: EnvelopePlus, messageHash: string) {
} }
} }
function contentIsEnvelope(content: Uint8Array | EnvelopePlus): content is EnvelopePlus {
return !isEmpty((content as EnvelopePlus).content);
}
async function handleRequestDetail( async function handleRequestDetail(
plaintext: Uint8Array, data: Uint8Array | EnvelopePlus,
inConversation: string | null, inConversation: string | null,
lastPromise: Promise<any>, lastPromise: Promise<any>,
messageHash: string messageHash: string
): Promise<void> { ): Promise<void> {
const envelope: any = SignalService.Envelope.decode(plaintext); const envelope: any = contentIsEnvelope(data) ? data : SignalService.Envelope.decode(data);
// After this point, decoding errors are not the server's // After this point, decoding errors are not the server's
// fault, and we should handle them gracefully and tell the // fault, and we should handle them gracefully and tell the
// user they received an invalid message // user they received an invalid message
// The message is for a medium size group // The message is for a group
if (inConversation) { if (inConversation) {
const ourNumber = UserUtils.getOurPubKeyStrFromCache(); const ourNumber = UserUtils.getOurPubKeyStrFromCache();
const senderIdentity = envelope.source; const senderIdentity = envelope.source;
@ -95,7 +99,7 @@ async function handleRequestDetail(
envelope.source = inConversation; envelope.source = inConversation;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
plaintext = SignalService.Envelope.encode(envelope).finish(); data = SignalService.Envelope.encode(envelope).finish();
envelope.senderIdentity = senderIdentity; envelope.senderIdentity = senderIdentity;
} }
@ -109,7 +113,7 @@ async function handleRequestDetail(
// need to handle senderIdentity separately)... // need to handle senderIdentity separately)...
perfStart(`addToCache-${envelope.id}`); perfStart(`addToCache-${envelope.id}`);
await addToCache(envelope, plaintext, messageHash); await addToCache(envelope, contentIsEnvelope(data) ? data.content : data, messageHash);
perfEnd(`addToCache-${envelope.id}`, 'addToCache'); perfEnd(`addToCache-${envelope.id}`, 'addToCache');
// To ensure that we queue in the same order we receive messages // To ensure that we queue in the same order we receive messages
@ -133,7 +137,7 @@ export function handleRequest(
inConversation: string | null, inConversation: string | null,
messageHash: string messageHash: string
): void { ): void {
const lastPromise = _.last(incomingMessagePromises) || Promise.resolve(); const lastPromise = last(incomingMessagePromises) || Promise.resolve();
const promise = handleRequestDetail(plaintext, inConversation, lastPromise, messageHash).catch( const promise = handleRequestDetail(plaintext, inConversation, lastPromise, messageHash).catch(
e => { e => {

@ -3,6 +3,7 @@
/* eslint-disable @typescript-eslint/no-misused-promises */ /* eslint-disable @typescript-eslint/no-misused-promises */
import { GroupPubkeyType } from 'libsession_util_nodejs'; import { GroupPubkeyType } from 'libsession_util_nodejs';
import { compact, concat, difference, flatten, isArray, last, sample, uniqBy } from 'lodash'; import { compact, concat, difference, flatten, isArray, last, sample, uniqBy } from 'lodash';
import { v4 } from 'uuid';
import { Data, Snode } from '../../../data/data'; import { Data, Snode } from '../../../data/data';
import { SignalService } from '../../../protobuf'; import { SignalService } from '../../../protobuf';
import * as Receiver from '../../../receiver/receiver'; import * as Receiver from '../../../receiver/receiver';
@ -12,6 +13,8 @@ import * as snodePool from './snodePool';
import { ConversationModel } from '../../../models/conversation'; import { ConversationModel } from '../../../models/conversation';
import { ConversationTypeEnum } from '../../../models/conversationAttributes'; import { ConversationTypeEnum } from '../../../models/conversationAttributes';
import { signalservice } from '../../../protobuf/compiled';
import { EnvelopePlus } from '../../../receiver/types';
import { updateIsOnline } from '../../../state/ducks/onion'; import { updateIsOnline } from '../../../state/ducks/onion';
import { assertUnreachable } from '../../../types/sqlSharedTypes'; import { assertUnreachable } from '../../../types/sqlSharedTypes';
import { import {
@ -24,6 +27,7 @@ import { ConvoHub } from '../../conversations';
import { ed25519Str } from '../../onions/onionPath'; import { ed25519Str } from '../../onions/onionPath';
import { StringUtils, UserUtils } from '../../utils'; import { StringUtils, UserUtils } from '../../utils';
import { perfEnd, perfStart } from '../../utils/Performance'; import { perfEnd, perfStart } from '../../utils/Performance';
import { PreConditionFailed } from '../../utils/errors';
import { LibSessionUtil } from '../../utils/libsession/libsession_utils'; import { LibSessionUtil } from '../../utils/libsession/libsession_utils';
import { SnodeNamespace, SnodeNamespaces, UserConfigNamespaces } from './namespaces'; import { SnodeNamespace, SnodeNamespaces, UserConfigNamespaces } from './namespaces';
import { PollForGroup, PollForLegacy, PollForUs } from './pollingTypes'; import { PollForGroup, PollForLegacy, PollForUs } from './pollingTypes';
@ -37,13 +41,7 @@ import {
RetrieveRequestResult, RetrieveRequestResult,
} from './types'; } from './types';
export function extractWebSocketContent( export function extractWebSocketContent(message: string): null | Uint8Array {
message: string,
messageHash: string
): null | {
body: Uint8Array;
messageHash: string;
} {
try { try {
const dataPlaintext = new Uint8Array(StringUtils.encode(message, 'base64')); const dataPlaintext = new Uint8Array(StringUtils.encode(message, 'base64'));
const messageBuf = SignalService.WebSocketMessage.decode(dataPlaintext); const messageBuf = SignalService.WebSocketMessage.decode(dataPlaintext);
@ -51,11 +49,9 @@ export function extractWebSocketContent(
messageBuf.type === SignalService.WebSocketMessage.Type.REQUEST && messageBuf.type === SignalService.WebSocketMessage.Type.REQUEST &&
messageBuf.request?.body?.length messageBuf.request?.body?.length
) { ) {
return { return messageBuf.request.body;
body: messageBuf.request.body,
messageHash,
};
} }
return null; return null;
} catch (error) { } catch (error) {
window?.log?.warn('extractWebSocketContent from message failed with:', error.message); window?.log?.warn('extractWebSocketContent from message failed with:', error.message);
@ -374,21 +370,38 @@ export class SwarmPolling {
perfStart(`handleSeenMessages-${pubkey}`); perfStart(`handleSeenMessages-${pubkey}`);
const newMessages = await this.handleSeenMessages(uniqOtherMsgs); const newMessages = await this.handleSeenMessages(uniqOtherMsgs);
perfEnd(`handleSeenMessages-${pubkey}`, 'handleSeenMessages'); perfEnd(`handleSeenMessages-${pubkey}`, 'handleSeenMessages');
if (type === ConversationTypeEnum.GROUPV3) {
for (let index = 0; index < newMessages.length; index++) {
const msg = newMessages[index];
const retrieveResult = new Uint8Array(StringUtils.encode(msg.data, 'base64'));
try {
const envelopePlus = await decryptForGroupV2({
content: retrieveResult,
groupPk: pubkey,
sentTimestamp: msg.timestamp,
});
if (!envelopePlus) {
throw new Error('decryptForGroupV2 returned empty envelope');
}
// this is the processing of the message itself, which can be long.
Receiver.handleRequest(envelopePlus.content, envelopePlus.source, msg.hash);
} catch (e) {
window.log.warn('failed to handle groupv2 otherMessage because of: ', e.message);
}
}
return;
}
// trigger the handling of all the other messages, not shared config related // trigger the handling of all the other messages, not shared config related
newMessages.forEach(m => { newMessages.forEach(m => {
const content = extractWebSocketContent(m.data, m.hash); const content = extractWebSocketContent(m.data);
if (!content) { if (!content) {
return; return;
} }
Receiver.handleRequest( Receiver.handleRequest(content, type === ConversationTypeEnum.GROUP ? pubkey : null, m.hash);
content.body,
type === ConversationTypeEnum.GROUP || type === ConversationTypeEnum.GROUPV3
? pubkey
: null,
content.messageHash
);
}); });
} }
@ -721,3 +734,34 @@ function filterMessagesPerTypeOfConvo<T extends ConversationTypeEnum>(
return { confMessages: null, otherMessages: [] }; return { confMessages: null, otherMessages: [] };
} }
} }
async function decryptForGroupV2(retrieveResult: {
groupPk: string;
content: Uint8Array;
sentTimestamp: number;
}): Promise<EnvelopePlus | null> {
window?.log?.info('received closed group message v2');
try {
const groupPk = retrieveResult.groupPk;
if (!PubKey.isClosedGroupV2(groupPk)) {
throw new PreConditionFailed('decryptForGroupV2: not a 03 prefixed group');
}
const decrypted = await MetaGroupWrapperActions.decryptMessage(groupPk, retrieveResult.content);
const envelopePlus: EnvelopePlus = {
id: v4(),
senderIdentity: decrypted.pubkeyHex,
receivedAt: Date.now(),
content: decrypted.plaintext,
source: groupPk,
type: signalservice.Envelope.Type.CLOSED_GROUP_MESSAGE,
timestamp: retrieveResult.sentTimestamp,
};
// the receiving pipeline relies on the envelope.senderIdentity field to know who is the author of a message
return envelopePlus;
} catch (e) {
window.log.warn('failed to decrypt message with error: ', e.message);
return null;
}
}

@ -307,6 +307,50 @@ type EncryptAndWrapMessageResults = {
namespace: number; namespace: number;
} & SharedEncryptAndWrap; } & SharedEncryptAndWrap;
async function encryptForGroupV2(
params: EncryptAndWrapMessage
): Promise<EncryptAndWrapMessageResults> {
// Group v2 encryption works a bit differently: we encrypt the envelope itself through libsession.
// We essentially need to do the opposite of the usual encryption which is send envelope unencrypted with content encrypted.
const {
destination,
identifier,
isSyncMessage: syncMessage,
namespace,
plainTextBuffer,
ttl,
} = params;
const { overRiddenTimestampBuffer, networkTimestamp } =
overwriteOutgoingTimestampWithNetworkTimestamp({ plainTextBuffer });
const envelope = await buildEnvelope(
SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE,
destination,
networkTimestamp,
overRiddenTimestampBuffer
);
const recipient = PubKey.cast(destination);
const { cipherText } = await MessageEncrypter.encrypt(
recipient,
SignalService.Envelope.encode(envelope).finish(),
encryptionBasedOnConversation(recipient)
);
const data64 = ByteBuffer.wrap(cipherText).toString('base64');
return {
data64,
networkTimestamp,
data: cipherText,
namespace,
ttl,
identifier,
isSyncMessage: syncMessage,
};
}
async function encryptMessageAndWrap( async function encryptMessageAndWrap(
params: EncryptAndWrapMessage params: EncryptAndWrapMessage
): Promise<EncryptAndWrapMessageResults> { ): Promise<EncryptAndWrapMessageResults> {
@ -319,6 +363,10 @@ async function encryptMessageAndWrap(
ttl, ttl,
} = params; } = params;
if (PubKey.isClosedGroupV2(destination)) {
return encryptForGroupV2(params);
}
const { overRiddenTimestampBuffer, networkTimestamp } = const { overRiddenTimestampBuffer, networkTimestamp } =
overwriteOutgoingTimestampWithNetworkTimestamp({ plainTextBuffer }); overwriteOutgoingTimestampWithNetworkTimestamp({ plainTextBuffer });
const recipient = PubKey.cast(destination); const recipient = PubKey.cast(destination);
@ -330,8 +378,7 @@ async function encryptMessageAndWrap(
); );
const envelope = await buildEnvelope(envelopeType, recipient.key, networkTimestamp, cipherText); const envelope = await buildEnvelope(envelopeType, recipient.key, networkTimestamp, cipherText);
const data = wrapEnvelopeInWebSocketMessage(envelope);
const data = wrapEnvelope(envelope);
const data64 = ByteBuffer.wrap(data).toString('base64'); const data64 = ByteBuffer.wrap(data).toString('base64');
return { return {
@ -423,7 +470,7 @@ async function buildEnvelope(
* This is an outdated practice and we should probably just send the envelope data directly. * This is an outdated practice and we should probably just send the envelope data directly.
* Something to think about in the future. * Something to think about in the future.
*/ */
function wrapEnvelope(envelope: SignalService.Envelope): Uint8Array { function wrapEnvelopeInWebSocketMessage(envelope: SignalService.Envelope): Uint8Array {
const request = SignalService.WebSocketRequestMessage.create({ const request = SignalService.WebSocketRequestMessage.create({
id: 0, id: 0,
body: SignalService.Envelope.encode(envelope).finish(), body: SignalService.Envelope.encode(envelope).finish(),

@ -1,6 +1,6 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { PubkeyType } from 'libsession_util_nodejs'; import { PubkeyType } from 'libsession_util_nodejs';
import { isArray, isEmpty, isNumber } from 'lodash'; import { isArray, isEmpty, isNumber, isString } from 'lodash';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { UserUtils } from '../..'; import { UserUtils } from '../..';
import { ConfigDumpData } from '../../../../data/configDump/configDump'; import { ConfigDumpData } from '../../../../data/configDump/configDump';
@ -17,9 +17,9 @@ import { LibSessionUtil, UserSuccessfulChange } from '../../libsession/libsessio
import { runners } from '../JobRunner'; import { runners } from '../JobRunner';
import { import {
AddJobCheckReturn, AddJobCheckReturn,
UserSyncPersistedData,
PersistedJob, PersistedJob,
RunJobResult, RunJobResult,
UserSyncPersistedData,
} from '../PersistedJob'; } from '../PersistedJob';
const defaultMsBetweenRetries = 15000; // a long time between retries, to avoid running multiple jobs at the same time, when one was postponed at the same time as one already planned (5s) const defaultMsBetweenRetries = 15000; // a long time between retries, to avoid running multiple jobs at the same time, when one was postponed at the same time as one already planned (5s)
@ -61,8 +61,8 @@ function triggerConfSyncJobDone() {
window.Whisper.events.trigger(UserSyncJobDone); window.Whisper.events.trigger(UserSyncJobDone);
} }
function isPubkey(us: string): us is PubkeyType { function isPubkey(us: unknown): us is PubkeyType {
return us.startsWith('05'); return isString(us) && us.startsWith('05');
} }
async function pushChangesToUserSwarmIfNeeded() { async function pushChangesToUserSwarmIfNeeded() {
@ -153,7 +153,7 @@ class UserSyncJob extends PersistedJob<UserSyncPersistedData> {
return RunJobResult.PermanentFailure; return RunJobResult.PermanentFailure;
} }
return await pushChangesToUserSwarmIfNeeded(); return await UserSync.pushChangesToUserSwarmIfNeeded();
// eslint-disable-next-line no-useless-catch // eslint-disable-next-line no-useless-catch
} catch (e) { } catch (e) {
throw e; throw e;
@ -228,5 +228,6 @@ async function queueNewJobIfNeeded() {
export const UserSync = { export const UserSync = {
UserSyncJob, UserSyncJob,
pushChangesToUserSwarmIfNeeded,
queueNewJobIfNeeded: () => allowOnlyOneAtATime('UserSyncJob-oneAtAtTime', queueNewJobIfNeeded), queueNewJobIfNeeded: () => allowOnlyOneAtATime('UserSyncJob-oneAtAtTime', queueNewJobIfNeeded),
}; };

@ -215,7 +215,7 @@ describe('LibSessionUtil pendingChangesForGroup', () => {
}); });
}); });
describe('LibSessionUtil pendingChangesForUser', () => { describe('LibSessionUtil pendingChangesForUs', () => {
beforeEach(async () => {}); beforeEach(async () => {});
afterEach(() => { afterEach(() => {

@ -281,6 +281,7 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
}); });
it('calls sendEncryptedDataToSnode with the right data and retry if network returned nothing', async () => { it('calls sendEncryptedDataToSnode with the right data and retry if network returned nothing', async () => {
throw null; // this test might not be right
const info = validInfo(sodium); const info = validInfo(sodium);
const member = validMembers(sodium); const member = validMembers(sodium);
const networkTimestamp = 4444; const networkTimestamp = 4444;
@ -300,7 +301,6 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
expect(saveDumpsToDbStub.firstCall.args).to.be.deep.eq([groupPk]); expect(saveDumpsToDbStub.firstCall.args).to.be.deep.eq([groupPk]);
function expected(details: any) { function expected(details: any) {
console.warn('details', details);
return { return {
namespace: details.namespace, namespace: details.namespace,
data: details.ciphertext, data: details.ciphertext,
@ -320,6 +320,7 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
}); });
it('calls sendEncryptedDataToSnode with the right data (and keys) and retry if network returned nothing', async () => { it('calls sendEncryptedDataToSnode with the right data (and keys) and retry if network returned nothing', async () => {
throw null; // this test might not be right
const info = validInfo(sodium); const info = validInfo(sodium);
const member = validMembers(sodium); const member = validMembers(sodium);
const keys = validKeys(sodium); const keys = validKeys(sodium);

@ -0,0 +1,367 @@
import { expect } from 'chai';
import { PubkeyType } from 'libsession_util_nodejs';
import { omit } from 'lodash';
import Long from 'long';
import Sinon from 'sinon';
import { getSodiumNode } from '../../../../../../node/sodiumNode';
import { NotEmptyArrayOfBatchResults } from '../../../../../../session/apis/snode_api/SnodeRequestTypes';
import { GetNetworkTime } from '../../../../../../session/apis/snode_api/getNetworkTime';
import {
SnodeNamespaces,
UserConfigNamespaces,
} from '../../../../../../session/apis/snode_api/namespaces';
import { TTL_DEFAULT } from '../../../../../../session/constants';
import { ConvoHub } from '../../../../../../session/conversations';
import { LibSodiumWrappers } from '../../../../../../session/crypto';
import { MessageSender } from '../../../../../../session/sending';
import { UserUtils } from '../../../../../../session/utils';
import { RunJobResult } from '../../../../../../session/utils/job_runners/PersistedJob';
import { UserSync } from '../../../../../../session/utils/job_runners/jobs/UserSyncJob';
import {
LibSessionUtil,
PendingChangesForUs,
UserDestinationChanges,
UserSuccessfulChange,
} from '../../../../../../session/utils/libsession/libsession_utils';
import { GenericWrapperActions } from '../../../../../../webworker/workers/browser/libsession_worker_interface';
import { TestUtils } from '../../../../../test-utils';
import { TypedStub, stubConfigDumpData } from '../../../../../test-utils/utils';
function userChange(
sodium: LibSodiumWrappers,
namespace: UserConfigNamespaces,
seqno: number
): PendingChangesForUs {
return {
ciphertext: sodium.randombytes_buf(120),
namespace,
seqno: Long.fromNumber(seqno),
};
}
describe('UserSyncJob run()', () => {
afterEach(() => {
Sinon.restore();
});
it('throws if no user keys', async () => {
const job = new UserSync.UserSyncJob({});
const func = async () => job.run();
await expect(func()).to.be.eventually.rejected;
});
it('throws if our pubkey is set but not valid', async () => {
const job = new UserSync.UserSyncJob({});
Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').returns({ something: false } as any);
Sinon.stub(UserUtils, 'getUserED25519KeyPairBytes').resolves({ something: true } as any);
Sinon.stub(ConvoHub.use(), 'get').resolves({}); // anything not falsy
const func = async () => job.run();
await expect(func()).to.be.eventually.rejected;
});
it('permanent failure if user has no ed keypair', async () => {
Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(TestUtils.generateFakePubKeyStr());
Sinon.stub(UserUtils, 'getUserED25519KeyPairBytes').resolves(undefined);
Sinon.stub(ConvoHub.use(), 'get').resolves({}); // anything not falsy
const job = new UserSync.UserSyncJob({});
const result = await job.run();
expect(result).to.be.eq(RunJobResult.PermanentFailure);
});
it('permanent failure if user has no own conversation', async () => {
Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(TestUtils.generateFakePubKeyStr());
Sinon.stub(UserUtils, 'getUserED25519KeyPairBytes').resolves({} as any); // anything not falsy
Sinon.stub(ConvoHub.use(), 'get').returns(undefined as any);
const job = new UserSync.UserSyncJob({});
const result = await job.run();
expect(result).to.be.eq(RunJobResult.PermanentFailure);
});
it('calls pushChangesToUserSwarmIfNeeded if preconditions are fine', async () => {
Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(TestUtils.generateFakePubKeyStr());
Sinon.stub(UserUtils, 'getUserED25519KeyPairBytes').resolves({} as any); // anything not falsy
const taskedRun = Sinon.stub(UserSync, 'pushChangesToUserSwarmIfNeeded').resolves(
RunJobResult.Success
);
Sinon.stub(ConvoHub.use(), 'get').returns({} as any); // anything not falsy
const job = new UserSync.UserSyncJob({});
const result = await job.run();
expect(result).to.be.eq(RunJobResult.Success);
expect(taskedRun.callCount).to.be.eq(1);
});
});
describe('UserSyncJob resultsToSuccessfulChange', () => {
let sodium: LibSodiumWrappers;
beforeEach(async () => {
sodium = await getSodiumNode();
});
it('no or empty results return empty array', () => {
expect(
LibSessionUtil.batchResultsToUserSuccessfulChange(null, {
allOldHashes: new Set(),
messages: [],
})
).to.be.deep.eq([]);
expect(
LibSessionUtil.batchResultsToUserSuccessfulChange([] as any as NotEmptyArrayOfBatchResults, {
allOldHashes: new Set(),
messages: [],
})
).to.be.deep.eq([]);
});
it('extract one result with 200 and messagehash', () => {
const profile = userChange(sodium, SnodeNamespaces.UserProfile, 321);
const contact = userChange(sodium, SnodeNamespaces.UserContacts, 123);
const batchResults: NotEmptyArrayOfBatchResults = [{ code: 200, body: { hash: 'hash1' } }];
const request: UserDestinationChanges = {
allOldHashes: new Set(),
messages: [profile, contact],
};
const results = LibSessionUtil.batchResultsToUserSuccessfulChange(batchResults, request);
expect(results).to.be.deep.eq([
{
updatedHash: 'hash1',
pushed: profile,
},
]);
});
it('extract two results with 200 and messagehash', () => {
const profile = userChange(sodium, SnodeNamespaces.UserProfile, 321);
const contact = userChange(sodium, SnodeNamespaces.UserContacts, 123);
const batchResults: NotEmptyArrayOfBatchResults = [
{ code: 200, body: { hash: 'hash1' } },
{ code: 200, body: { hash: 'hash2' } },
];
const request: UserDestinationChanges = {
allOldHashes: new Set(),
messages: [contact, profile],
};
const results = LibSessionUtil.batchResultsToUserSuccessfulChange(batchResults, request);
expect(results).to.be.deep.eq([
{
updatedHash: 'hash1',
pushed: contact,
},
{
updatedHash: 'hash2',
pushed: profile,
},
]);
});
it('skip message hashes not a string', () => {
const profile = userChange(sodium, SnodeNamespaces.UserProfile, 321);
const contact = userChange(sodium, SnodeNamespaces.UserContacts, 123);
const batchResults: NotEmptyArrayOfBatchResults = [
{ code: 200, body: { hash: 123 as any as string } },
{ code: 200, body: { hash: 'hash2' } },
];
const request: UserDestinationChanges = {
allOldHashes: new Set(),
messages: [profile, contact],
};
const results = LibSessionUtil.batchResultsToUserSuccessfulChange(batchResults, request);
expect(results).to.be.deep.eq([
{
updatedHash: 'hash2',
pushed: contact,
},
]);
});
it('skip request item without data', () => {
const profile = userChange(sodium, SnodeNamespaces.UserProfile, 321);
const contact = userChange(sodium, SnodeNamespaces.UserContacts, 123);
const profileNoData = omit(profile, 'ciphertext');
const batchResults: NotEmptyArrayOfBatchResults = [
{ code: 200, body: { hash: 'hash1' } },
{ code: 200, body: { hash: 'hash2' } },
];
const request: UserDestinationChanges = {
allOldHashes: new Set(),
messages: [profileNoData as any as PendingChangesForUs, contact],
};
const results = LibSessionUtil.batchResultsToUserSuccessfulChange(batchResults, request);
expect(results).to.be.deep.eq([
{
updatedHash: 'hash2',
pushed: contact,
},
]);
});
it('skip request item without 200 code', () => {
const profile = userChange(sodium, SnodeNamespaces.UserProfile, 321);
const contact = userChange(sodium, SnodeNamespaces.UserContacts, 123);
const batchResults: NotEmptyArrayOfBatchResults = [
{ code: 200, body: { hash: 'hash1' } },
{ code: 401, body: { hash: 'hash2' } },
];
const request: UserDestinationChanges = {
allOldHashes: new Set(),
messages: [profile, contact],
};
const results = LibSessionUtil.batchResultsToUserSuccessfulChange(batchResults, request);
expect(results).to.be.deep.eq([
{
updatedHash: 'hash1',
pushed: profile,
},
]);
// another test swapping the results
batchResults[0].code = 401;
batchResults[1].code = 200;
const results2 = LibSessionUtil.batchResultsToUserSuccessfulChange(batchResults, request);
expect(results2).to.be.deep.eq([
{
updatedHash: 'hash2',
pushed: contact,
},
]);
});
});
describe('UserSyncJob pushChangesToUserSwarmIfNeeded', () => {
let sessionId: PubkeyType;
let userkeys: TestUtils.TestUserKeyPairs;
let sodium: LibSodiumWrappers;
let sendStub: TypedStub<typeof MessageSender, 'sendEncryptedDataToSnode'>;
let pendingChangesForUsStub: TypedStub<typeof LibSessionUtil, 'pendingChangesForUs'>;
let dump: TypedStub<typeof GenericWrapperActions, 'dump'>;
beforeEach(async () => {
sodium = await getSodiumNode();
userkeys = await TestUtils.generateUserKeyPairs();
sessionId = userkeys.x25519KeyPair.pubkeyHex;
Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(userkeys.x25519KeyPair.pubkeyHex);
Sinon.stub(UserUtils, 'getUserED25519KeyPairBytes').resolves(userkeys.ed25519KeyPair);
window.Whisper = {};
window.Whisper.events = {};
window.Whisper.events.trigger = Sinon.mock();
stubConfigDumpData('saveConfigDump').resolves();
pendingChangesForUsStub = Sinon.stub(LibSessionUtil, 'pendingChangesForUs');
dump = Sinon.stub(GenericWrapperActions, 'dump').resolves(new Uint8Array());
sendStub = Sinon.stub(MessageSender, 'sendEncryptedDataToSnode');
});
afterEach(() => {
Sinon.restore();
});
it('call savesDumpToDb even if no changes are required on the serverside', async () => {
Sinon.stub(GenericWrapperActions, 'needsDump').resolves(true);
const result = await UserSync.pushChangesToUserSwarmIfNeeded();
pendingChangesForUsStub.resolves(undefined);
expect(result).to.be.eq(RunJobResult.Success);
expect(sendStub.callCount).to.be.eq(0);
expect(pendingChangesForUsStub.callCount).to.be.eq(1);
expect(dump.callCount).to.be.eq(4);
expect(dump.getCalls().map(m => m.args)).to.be.deep.eq([
['UserConfig'],
['ContactsConfig'],
['UserGroupsConfig'],
['ConvoInfoVolatileConfig'],
]);
});
it('calls sendEncryptedDataToSnode with the right data x2 and retry if network returned nothing', async () => {
Sinon.stub(GenericWrapperActions, 'needsDump').resolves(false);
const profile = userChange(sodium, SnodeNamespaces.UserProfile, 321);
const contact = userChange(sodium, SnodeNamespaces.UserContacts, 123);
const networkTimestamp = 4444;
const ttl = TTL_DEFAULT.TTL_CONFIG;
Sinon.stub(GetNetworkTime, 'getNowWithNetworkOffset').returns(networkTimestamp);
pendingChangesForUsStub.resolves({
messages: [profile, contact],
allOldHashes: new Set('123'),
});
const result = await UserSync.pushChangesToUserSwarmIfNeeded();
sendStub.resolves(undefined);
expect(result).to.be.eq(RunJobResult.RetryJobIfPossible); // not returning anything in the sendstub so network issue happened
expect(sendStub.callCount).to.be.eq(1);
expect(pendingChangesForUsStub.callCount).to.be.eq(1);
expect(dump.callCount).to.be.eq(0);
function expected(details: any) {
return {
namespace: details.namespace,
data: details.ciphertext,
ttl,
networkTimestamp,
pubkey: sessionId,
};
}
throw null; //
// this test is not right. check for dump logic and make sure this matches
const expectedProfile = expected(profile);
const expectedContact = expected(contact);
expect(sendStub.firstCall.args).to.be.deep.eq([
[expectedProfile, expectedContact],
sessionId,
new Set('123'),
]);
});
it('calls sendEncryptedDataToSnode with the right data x3 and retry if network returned nothing then success', async () => {
const profile = userChange(sodium, SnodeNamespaces.UserProfile, 321);
const contact = userChange(sodium, SnodeNamespaces.UserContacts, 123);
const groups = userChange(sodium, SnodeNamespaces.UserGroups, 111);
throw null; //
// this test is not right. check for dump logic and make sure this matches
pendingChangesForUsStub.resolves({
messages: [profile, contact, groups],
allOldHashes: new Set('123'),
});
const changes: Array<UserSuccessfulChange> = [
{
pushed: profile,
updatedHash: 'hashprofile',
},
{
pushed: contact,
updatedHash: 'hashcontact',
},
{
pushed: groups,
updatedHash: 'hashgroup',
},
];
Sinon.stub(LibSessionUtil, 'batchResultsToUserSuccessfulChange').returns(changes);
const confirmPushed = Sinon.stub(GenericWrapperActions, 'confirmPushed').resolves();
const needsDump = Sinon.stub(GenericWrapperActions, 'needsDump').resolves();
sendStub.resolves([
{ code: 200, body: { hash: 'hashprofile' } },
{ code: 200, body: { hash: 'hashcontact' } },
{ code: 200, body: { hash: 'hashgroup' } },
{ code: 200, body: {} }, // because we are giving a set of allOldHashes
]);
const result = await UserSync.pushChangesToUserSwarmIfNeeded();
expect(sendStub.callCount).to.be.eq(1);
expect(pendingChangesForUsStub.callCount).to.be.eq(1);
expect(dump.callCount).to.be.eq(2);
expect(dump.firstCall.args).to.be.deep.eq([sessionId]);
expect(needsDump.callCount).to.be.eq(4);
expect(needsDump.getCalls().map(m => m.args)).to.be.deep.eq([[sessionId, []]]);
expect(confirmPushed.callCount).to.be.eq(4);
expect(confirmPushed.getCalls().map(m => m.args)).to.be.deep.eq([[sessionId, [123, 'hash1']]]);
expect(result).to.be.eq(RunJobResult.Success);
});
});
Loading…
Cancel
Save