From dec7aa10c8818ddc624bfffae5d4a034a3ef192c Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 4 Jun 2020 13:16:37 +1000 Subject: [PATCH] Revert pulls --- js/modules/data.d.ts | 2 +- preload.js | 46 ++-- .../session/LeftPaneMessageSection.tsx | 48 +++- ts/session/sending/MessageQueue.ts | 136 ++++++++++- ts/session/sending/MessageQueueInterface.ts | 12 +- ts/session/sending/PendingMessageCache.ts | 119 ++++++++-- ts/session/utils/MessageUtils.ts | 91 ++++++++ ts/session/utils/index.ts | 1 + ts/test/session/sending/MessageQueue_test.ts | 33 +++ .../sending/PendingMessageCache_test.ts | 212 ++++++++++++++++++ ts/test/session/utils/MessageUtils_test.ts | 72 ++++++ .../utils/stubs/SignalAddressProtocolStub.ts | 27 +++ 12 files changed, 740 insertions(+), 59 deletions(-) create mode 100644 ts/session/utils/MessageUtils.ts create mode 100644 ts/test/session/sending/MessageQueue_test.ts create mode 100644 ts/test/session/sending/PendingMessageCache_test.ts create mode 100644 ts/test/session/utils/MessageUtils_test.ts create mode 100644 ts/test/utils/stubs/SignalAddressProtocolStub.ts diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts index c70fd17e1..b0cb2961e 100644 --- a/js/modules/data.d.ts +++ b/js/modules/data.d.ts @@ -415,4 +415,4 @@ export function getMessagesWithFileAttachments( // Sender Keys export function getSenderKeys(groupId: any, senderIdentity: any): Promise; -export function createOrUpdateSenderKeys(data: any): Promise; +export function createOrUpdateSenderKeys(data: any): Promise; \ No newline at end of file diff --git a/preload.js b/preload.js index 31d435787..cf6f2d9eb 100644 --- a/preload.js +++ b/preload.js @@ -3,6 +3,7 @@ /* global window: false */ const path = require('path'); const electron = require('electron'); +const Data = require('./js/modules/data') const { webFrame } = electron; const semver = require('semver'); @@ -70,18 +71,31 @@ window.isBeforeVersion = (toCheck, baseVersion) => { } }; -window.CONSTANTS = { - MAX_LOGIN_TRIES: 3, - MAX_PASSWORD_LENGTH: 64, - MAX_USERNAME_LENGTH: 20, - MAX_GROUP_NAME_LENGTH: 64, - DEFAULT_PUBLIC_CHAT_URL: appConfig.get('defaultPublicChatServer'), - MAX_CONNECTION_DURATION: 5000, - MAX_MESSAGE_BODY_LENGTH: 64 * 1024, +// eslint-disable-next-line func-names +window.CONSTANTS = new function() { + this.MAX_LOGIN_TRIES = 3; + this.MAX_PASSWORD_LENGTH = 64; + this.MAX_USERNAME_LENGTH = 20; + this.MAX_GROUP_NAME_LENGTH = 64; + this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer'); + this.MAX_CONNECTION_DURATION = 5000; + this.MAX_MESSAGE_BODY_LENGTH = 64 * 1024; // Limited due to the proof-of-work requirement - SMALL_GROUP_SIZE_LIMIT: 10, - NOTIFICATION_ENABLE_TIMEOUT_SECONDS: 10, // number of seconds to turn on notifications after reconnect/start of app -}; + this.SMALL_GROUP_SIZE_LIMIT = 10; + // Number of seconds to turn on notifications after reconnect/start of app + this.NOTIFICATION_ENABLE_TIMEOUT_SECONDS = 10; + this.SESSION_ID_LENGTH = 66; + + // Loki Name System (LNS) + this.LNS_DEFAULT_LOOKUP_TIMEOUT = 6000; + // Minimum nodes version for LNS lookup + this.LNS_CAPABLE_NODES_VERSION = '2.0.3'; + this.LNS_MAX_LENGTH = 64; + // Conforms to naming rules here + // https://loki.network/2020/03/25/loki-name-system-the-facts/ + this.LNS_REGEX = `^[a-zA-Z0-9_]([a-zA-Z0-9_-]{0,${this.LNS_MAX_LENGTH - + 2}}[a-zA-Z0-9_]){0,1}$`; +}(); window.versionInfo = { environment: window.getEnvironment(), @@ -95,6 +109,9 @@ window.wrapDeferred = deferredToPromise; const ipc = electron.ipcRenderer; const localeMessages = ipc.sendSync('locale-data'); +// Initialise Data +Data.init(); + window.blake2b = input => new Promise((resolve, reject) => { ipc.once('blake2b-digest-response', (event, error, res) => { @@ -327,6 +344,7 @@ window.LokiMessageAPI = require('./js/modules/loki_message_api'); if (process.env.USE_STUBBED_NETWORK) { window.StubMessageAPI = require('./integration_test/stubs/stub_message_api'); window.StubAppDotNetApi = require('./integration_test/stubs/stub_app_dot_net_api'); + window.StubLokiSnodeAPI = require('./integration_test/stubs/stub_loki_snode_api'); } window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api'); @@ -414,6 +432,8 @@ window.lokiFeatureFlags = { privateGroupChats: true, useSnodeProxy: !process.env.USE_STUBBED_NETWORK, useOnionRequests: true, + useFileOnionRequests: false, + enableSenderKeys: false, onionRequestHops: 1, }; @@ -440,7 +460,7 @@ if ( }; /* eslint-enable global-require, import/no-extraneous-dependencies */ window.lokiFeatureFlags = {}; - window.lokiSnodeAPI = {}; // no need stub out each function here + window.lokiSnodeAPI = new window.StubLokiSnodeAPI(); // no need stub out each function here } if (config.environment.includes('test-integration')) { window.lokiFeatureFlags = { @@ -448,4 +468,4 @@ if (config.environment.includes('test-integration')) { privateGroupChats: true, useSnodeProxy: !process.env.USE_STUBBED_NETWORK, }; -} +} \ No newline at end of file diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index e29b942b1..4c825a427 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -27,6 +27,12 @@ import { import { SessionSpinner } from './SessionSpinner'; import { joinChannelStateManager } from './LeftPaneChannelSection'; +// HIJACKING BUTTON FOR TESTING +import { PendingMessageCache } from '../../session/sending/PendingMessageCache'; +import { MessageQueue } from '../../session/sending'; +import { ExampleMessage } from '../../session/sending/MessageQueue'; + + export interface Props { searchTerm: string; isSecondaryDevice: boolean; @@ -45,6 +51,10 @@ export class LeftPaneMessageSection extends React.Component { private readonly updateSearchBound: (searchedString: string) => void; private readonly debouncedSearch: (searchTerm: string) => void; + // HIJACKED FOR TESTING + private readonly messageQueue: any; + private readonly pendingMessageCache: any; + public constructor(props: Props) { super(props); @@ -82,6 +92,11 @@ export class LeftPaneMessageSection extends React.Component { this.handleOnPasteSessionID = this.handleOnPasteSessionID.bind(this); this.handleMessageButtonClick = this.handleMessageButtonClick.bind(this); this.debouncedSearch = debounce(this.search.bind(this), 20); + + + // HIJACKING FOR TESTING + this.messageQueue = new MessageQueue(); + this.pendingMessageCache = new PendingMessageCache(); } public componentWillUnmount() { @@ -97,7 +112,7 @@ export class LeftPaneMessageSection extends React.Component { if (conversationList !== undefined) { conversationList = conversationList.filter( conversation => - !conversation.isSecondary && !conversation.isPendingFriendRequest + !conversation.isPendingFriendRequest && !conversation.isSecondary ); } @@ -361,12 +376,29 @@ export class LeftPaneMessageSection extends React.Component { ); } - private handleToggleOverlay() { - this.setState((state: any) => { - return { showComposeView: !state.showComposeView }; - }); - // empty our generalized searchedString (one for the whole app) - this.updateSearch(''); + private async handleToggleOverlay() { + // HIJACKING BUTTON FOR TESTING + console.log('[vince] pendingMessageCache:', this.pendingMessageCache); + + const pubkey = window.textsecure.storage.user.getNumber(); + const exampleMessage = new ExampleMessage(); + + console.log('[vince] exampleMessage:', exampleMessage); + + const devices = this.pendingMessageCache.getPendingDevices(); + console.log('[vince] devices:', devices); + + if ($('.session-search-input input').val()) { + this.pendingMessageCache.removePendingMessageByIdentifier(exampleMessage.identifier); + } else { + this.pendingMessageCache.addPendingMessage(pubkey, exampleMessage); + } + + // this.setState((state: any) => { + // return { showComposeView: !state.showComposeView }; + // }); + // // empty our generalized searchedString (one for the whole app) + // this.updateSearch(''); } private handleOnPasteSessionID(value: string) { @@ -408,4 +440,4 @@ export class LeftPaneMessageSection extends React.Component { const serverURL = window.CONSTANTS.DEFAULT_PUBLIC_CHAT_URL; joinChannelStateManager(this, serverURL, this.handleCloseOnboarding); } -} +} \ No newline at end of file diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index 274640ac4..31b4e2080 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -1,12 +1,45 @@ +import * as Data from '../../../js/modules/data'; import { EventEmitter } from 'events'; import { MessageQueueInterface, MessageQueueInterfaceEvents, } from './MessageQueueInterface'; -import { OpenGroupMessage, OutgoingContentMessage } from '../messages/outgoing'; +import { + ContentMessage, + OpenGroupMessage, + SessionResetMessage, +} from '../messages/outgoing'; import { PendingMessageCache } from './PendingMessageCache'; import { JobQueue, TypedEventEmitter } from '../utils'; + +// Used for ExampleMessage +import { v4 as uuid } from 'uuid'; +import { SignalService } from '../../protobuf'; + +export class ExampleMessage extends ContentMessage { + constructor() { + super({ + timestamp: Math.floor(Math.random() * 10000000000000), + identifier: uuid(), + }); + } + + public ttl(): number { + // throw new Error("Method not implemented."); + return 5; + } + + protected contentProto(): SignalService.Content { + // throw new Error("Method not implemented."); + + // TODO - get actual content + const content = SignalService.Content.create(); + + return content; + } +} + export class MessageQueue implements MessageQueueInterface { public readonly events: TypedEventEmitter; private readonly jobQueues: Map = new Map(); @@ -18,29 +51,110 @@ export class MessageQueue implements MessageQueueInterface { this.processAllPending(); } - public sendUsingMultiDevice(user: string, message: OutgoingContentMessage) { - throw new Error('Method not implemented.'); + public async sendUsingMultiDevice(user: string, message: ContentMessage) { + // throw new Error('Method not implemented.'); + + // Update from TS Globals + const pairedDevices = await Data.getPairedDevicesFor(user); + const userDevices = [...pairedDevices, user]; + + console.log('[vince] userDevices:', userDevices); + } - public send(device: string, message: OutgoingContentMessage) { - throw new Error('Method not implemented.'); + public send(device: string, message: ContentMessage) { + // throw new Error('Method not implemented.'); + + // Validation; early exists? + + + // TESTING + console.log(`[vince] send: Queueing message`, message); + this.queue(device, message); + } - public sendToGroup(message: OutgoingContentMessage | OpenGroupMessage) { + public sendToGroup(message: ContentMessage | OpenGroupMessage) { throw new Error('Method not implemented.'); + + // If you see an open group message just call + // MessageSender.sendToOpenGroup directly. } - public sendSyncMessage(message: OutgoingContentMessage) { - throw new Error('Method not implemented.'); + public sendSyncMessage(message: ContentMessage) { + // PSEDUOCODE + // if message is undefined + // returnt + + + // for each of our device excluding current device: + // queue(device, syncMessage) + + // throw new Error('Method not implemented.'); } - public processPending(device: string) { + public async processPending(device: string) { // TODO: implement + + // PSEDUDOCODE + // messages = PendingMessageCache.getPendingMessages(device) + // isMediumGroup = device is medium group + // hasSession = SessionManager.hasSession(device) + + // if !isMediumGroup && !hasSession + // SessionManager.sendSessionRequestIfNeeded() + // return // Don't process any more messages + + // jobQueue = getJobQueue(device) + // for each message: + // if !jobQueue.has(message.uuid) + // promise = jobQueue.queue(message.uuid, MessageSender.send(message)) + // promise.then().catch() // Add or remove from pending message cache on success and failure + + + // Promise shouldn't be returned; we're firing an event when processed. + + } + private processAllPending() { // TODO: Get all devices which are pending here } - private queue(device: string, message: OutgoingContentMessage) { + private queue(device: string, message: ContentMessage) { + // This should simply add to the queue. No processing + // TODO: implement + // PSEUDOCODE + + // if message is Session Request + // SessionManager.sendSessionRequest(device, message) + // return + + // PendingMessageCache.addPendingMessage(device, message) + // processPending(device) + + if (message instanceof SessionResetMessage) { + return; + } + + + + console.log(`[vince] queue: Message added to the queue`, message); + + // Add the item to the queue + const queue = this.getJobQueue(device); + const job = new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 3000); + }); + + // tslint:disable-next-line: no-floating-promises + queue.add(async () => job); + + // Saving offline and stuff + + // Attach to event + } private queueOpenGroupMessage(message: OpenGroupMessage) { @@ -57,4 +171,4 @@ export class MessageQueue implements MessageQueueInterface { return queue; } -} +} \ No newline at end of file diff --git a/ts/session/sending/MessageQueueInterface.ts b/ts/session/sending/MessageQueueInterface.ts index 553b25ed0..b28040d1b 100644 --- a/ts/session/sending/MessageQueueInterface.ts +++ b/ts/session/sending/MessageQueueInterface.ts @@ -1,9 +1,9 @@ -import { OpenGroupMessage, OutgoingContentMessage } from '../messages/outgoing'; +import { OpenGroupMessage, ContentMessage, SyncMessage } from '../messages/outgoing'; import { RawMessage } from '../types/RawMessage'; import { TypedEventEmitter } from '../utils'; // TODO: add all group messages here, replace OutgoingContentMessage with them -type GroupMessageType = OpenGroupMessage | OutgoingContentMessage; +type GroupMessageType = OpenGroupMessage | ContentMessage; export interface MessageQueueInterfaceEvents { success: (message: RawMessage) => void; @@ -12,8 +12,8 @@ export interface MessageQueueInterfaceEvents { export interface MessageQueueInterface { events: TypedEventEmitter; - sendUsingMultiDevice(user: string, message: OutgoingContentMessage): void; - send(device: string, message: OutgoingContentMessage): void; + sendUsingMultiDevice(user: string, message: ContentMessage): void; + send(device: string, message: ContentMessage): void; sendToGroup(message: GroupMessageType): void; - sendSyncMessage(message: OutgoingContentMessage): void; -} + sendSyncMessage(message: SyncMessage): void; +} \ No newline at end of file diff --git a/ts/session/sending/PendingMessageCache.ts b/ts/session/sending/PendingMessageCache.ts index 2f10e58a6..8dd7f4d07 100644 --- a/ts/session/sending/PendingMessageCache.ts +++ b/ts/session/sending/PendingMessageCache.ts @@ -1,36 +1,115 @@ +import * as Data from '../../../js/modules/data'; import { RawMessage } from '../types/RawMessage'; -import { OutgoingContentMessage } from '../messages/outgoing'; +import { ChatMessage, ContentMessage } from '../messages/outgoing'; +import { MessageUtils, PubKey } from '../utils'; // TODO: We should be able to import functions straight from the db here without going through the window object + +// This is an abstraction for storing pending messages. +// Ideally we want to store pending messages in the database so that +// on next launch we can re-send the pending messages, but we don't want +// to constantly fetch pending messages from the database. +// Thus we have an intermediary cache which will store pending messagesin +// memory and sync its state with the database on modification (add or remove). + export class PendingMessageCache { - private readonly cachedMessages: Array = []; + public cache: Array; constructor() { - // TODO: We should load pending messages from db here + // Load pending messages from the database + // You must call init() on this class in order to load from DB. + // const pendingMessageCache = new PendingMessageCache(); + // await pendingMessageCache.init() + // >> do stuff + this.cache = []; + } + + public async add(device: PubKey, message: ContentMessage): Promise { + const rawMessage = MessageUtils.toRawMessage(device, message); + + // Does it exist in cache already? + if(this.find(rawMessage)) { + return rawMessage; + } + + this.cache.push(rawMessage); + await this.syncCacheWithDB(); + + return rawMessage; } - public addPendingMessage( - device: string, - message: OutgoingContentMessage - ): RawMessage { - // TODO: Maybe have a util for converting OutgoingContentMessage to RawMessage? - // TODO: Raw message has uuid, how are we going to set that? maybe use a different identifier? - // One could be device + timestamp would make a unique identifier - // TODO: Return previous pending message if it exists - return {} as RawMessage; + public async remove(message: RawMessage): Promise | undefined> { + // Should only be called after message is processed + + // Return if message doesn't exist in cache + if (!this.find(message)) { + return; + } + + // Remove item from cache and sync with database + const updatedCache = this.cache.filter( + m => m.identifier !== message.identifier + ); + this.cache = updatedCache; + await this.syncCacheWithDB(); + + return updatedCache; } - public removePendingMessage(message: RawMessage) { - // TODO: implement + public find(message: RawMessage): RawMessage | undefined { + // Find a message in the cache + return this.cache.find( + m => m.device === message.device && m.timestamp === message.timestamp + ); } - public getPendingDevices(): Array { - // TODO: this should return all devices which have pending messages - return []; + public getForDevice(device: PubKey): Array { + return this.cache.filter(m => m.device === device.key); } - public getPendingMessages(device: string): Array { - return []; + public async clear() { + // Clears the cache and syncs to DB + this.cache = []; + await this.syncCacheWithDB(); } -} + + public getDevices(): Array { + // Gets all devices with pending messages + const pubkeys = [...new Set(this.cache.map(m => m.device))]; + + return pubkeys.map(d => PubKey.from(d)); + } + + public async init() { + const messages = await this.getFromStorage(); + this.cache = messages; + } + + public async getFromStorage(): Promise> { + // tslint:disable-next-line: no-backbone-get-set-outside-model + const pendingMessagesData = await Data.getItemById('pendingMessages'); + const pendingMessagesJSON = pendingMessagesData + ? String(pendingMessagesData.value) + : ''; + + // tslint:disable-next-line: no-unnecessary-local-variable + const encodedPendingMessages = pendingMessagesJSON + ? JSON.parse(pendingMessagesJSON) + : []; + + // Set pubkey from string to PubKey.from() + + + // TODO: + // Build up Uint8Array from painTextBuffer in JSON + return encodedPendingMessages; + } + + public async syncCacheWithDB() { + // Only call when adding / removing from cache. + const encodedPendingMessages = JSON.stringify(this.cache) || ''; + await Data.createOrUpdateItem({id: 'pendingMessages', value: encodedPendingMessages}); + } + +} \ No newline at end of file diff --git a/ts/session/utils/MessageUtils.ts b/ts/session/utils/MessageUtils.ts new file mode 100644 index 000000000..4f2eef5c4 --- /dev/null +++ b/ts/session/utils/MessageUtils.ts @@ -0,0 +1,91 @@ +import { RawMessage } from '../types/RawMessage'; +import { ContentMessage } from '../messages/outgoing'; +import { EncryptionType } from '../types/EncryptionType'; +import * as crypto from 'crypto'; + + +function toRawMessage(device: PubKey, message: ContentMessage): RawMessage { + const ttl = message.ttl(); + const timestamp = message.timestamp; + const plainTextBuffer = message.plainTextBuffer(); + + // Get EncryptionType depending on message type. + // let encryption: EncryptionType; + + // switch (message.constructor.name) { + // case MessageType.Chat: + // encryption = EncryptionType.Signal; + // break; + // case MessageType.SessionReset: + // encryption = EncryptionType + // } + + // export enum EncryptionType { + // Signal, + // SessionReset, + // MediumGroup, + // } + + // tslint:disable-next-line: no-unnecessary-local-variable + const rawMessage: RawMessage = { + identifier: message.identifier, + plainTextBuffer, + timestamp, + device: device.key, + ttl, + encryption: EncryptionType.Signal, + }; + + return rawMessage; +} + + +export enum PubKeyType { + Primary = 'priamry', + Secondary = 'secondary', + Group = 'group', +} + +export class PubKey { + private static readonly regex: string = '^0[0-9a-fA-F]{65}$'; + public readonly key: string; + public type?: PubKeyType; + + constructor(pubkeyString: string, type?: PubKeyType) { + PubKey.validate(pubkeyString); + this.key = pubkeyString; + this.type = type; + } + + public static from(pubkeyString: string): PubKey { + // Returns a new instance if the pubkey is valid + if (PubKey.validate(pubkeyString)) { + return new PubKey(pubkeyString); + } + + throw new Error('Invalid pubkey format'); + } + + public static validate(pubkeyString: string): boolean { + if (pubkeyString.match(PubKey.regex)) { + return true; + } + + throw new Error('Invalid pubkey format'); + } + + public static generate(): PubKey { + // Generates a mock pubkey for testing + const PUBKEY_LEN = 66; + const numBytes = PUBKEY_LEN / 2; + const hexBuffer = crypto.randomBytes(numBytes).toString('hex'); + const pubkeyString = `0${hexBuffer}`.slice(0, PUBKEY_LEN); + + return new PubKey(pubkeyString); + } +} + +// Functions / Tools +export const MessageUtils = { + toRawMessage, +}; \ No newline at end of file diff --git a/ts/session/utils/index.ts b/ts/session/utils/index.ts index a33d528ba..aa6aef99e 100644 --- a/ts/session/utils/index.ts +++ b/ts/session/utils/index.ts @@ -1,2 +1,3 @@ export * from './TypedEmitter'; export * from './JobQueue'; +export * from './MessageUtils'; diff --git a/ts/test/session/sending/MessageQueue_test.ts b/ts/test/session/sending/MessageQueue_test.ts new file mode 100644 index 000000000..73b1cf404 --- /dev/null +++ b/ts/test/session/sending/MessageQueue_test.ts @@ -0,0 +1,33 @@ +// import { expect } from 'chai'; + +// import { ChatMessage, SessionResetMessage, } from '../../../session/messages/outgoing'; +// import { TextEncoder } from 'util'; +// import { MessageUtils } from '../../../session/utils'; +// import { PendingMessageCache } from '../../../session/sending/PendingMessageCache'; + +// describe('PendingMessageCache', () => { +// const pendingMessageCache = new PendingMessageCache(); + +// let sessionResetMessage: SessionResetMessage; +// const preKeyBundle = { +// deviceId: 123456, +// preKeyId: 654321, +// signedKeyId: 111111, +// preKey: new TextEncoder().encode('preKey'), +// signature: new TextEncoder().encode('signature'), +// signedKey: new TextEncoder().encode('signedKey'), +// identityKey: new TextEncoder().encode('identityKey'), +// }; + + +// // queue with session reset message. +// // should return undefined +// // TOOD: Send me to MESSAGE QUEUE TEST +// it('queue session reset message', () => { +// const timestamp = Date.now(); +// sessionResetMessage = new SessionResetMessage({timestamp, preKeyBundle}); + + +// }); + +// }); \ No newline at end of file diff --git a/ts/test/session/sending/PendingMessageCache_test.ts b/ts/test/session/sending/PendingMessageCache_test.ts new file mode 100644 index 000000000..e7899ca9b --- /dev/null +++ b/ts/test/session/sending/PendingMessageCache_test.ts @@ -0,0 +1,212 @@ +// tslint:disable-next-line: no-require-imports no-var-requires +const Data = require('../../../../js/modules/data'); + +import { expect, assert } from 'chai'; +import sinon from 'sinon'; +import uuid from 'uuid'; + +import { ChatMessage } from '../../../session/messages/outgoing'; +import { MessageUtils, PubKey } from '../../../session/utils'; +import { PendingMessageCache } from '../../../session/sending/PendingMessageCache'; +import { RawMessage } from '../../../session/types/RawMessage'; +import { SignalService } from '../../../protobuf'; + +describe('PendingMessageCache', () => { + const sandbox = sinon.createSandbox(); + let pendingMessageCacheStub: PendingMessageCache; + + // tslint:disable-next-line: promise-function-async + const wrapInPromise = (value: any) => new Promise(r => { + r(value); + }); + + const generateUniqueMessage = (): ChatMessage => { + return new ChatMessage({ + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + identifier: uuid(), + timestamp: Date.now(), + attachments: undefined, + quote: undefined, + expireTimer: undefined, + lokiProfile: undefined, + preview: undefined, + }); + }; + + beforeEach(async () => { + const mockStorageObject = wrapInPromise([] as Array); + const voidPromise = wrapInPromise(undefined); + + // Stub out methods which touch the database. + sandbox.stub(PendingMessageCache.prototype, 'getFromStorage').returns(mockStorageObject); + sandbox.stub(PendingMessageCache.prototype, 'syncCacheWithDB').returns(voidPromise); + + // Initialize new stubbed cache + pendingMessageCacheStub = new PendingMessageCache(); + await pendingMessageCacheStub.init(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + + it('can initialize cache', async () => { + const { cache } = pendingMessageCacheStub; + + // We expect the cache to initialise as an empty array + expect(cache).to.be.instanceOf(Array); + expect(cache).to.have.length(0); + }); + + + it('can add to cache', async () => { + const device = PubKey.generate(); + const message = generateUniqueMessage(); + const rawMessage = MessageUtils.toRawMessage(device, message); + + await pendingMessageCacheStub.add(device, message); + + // Verify that the message is in the cache + const finalCache = pendingMessageCacheStub.cache; + + expect(finalCache).to.have.length(1); + + const addedMessage = finalCache[0]; + expect(addedMessage.device).to.deep.equal(rawMessage.device); + expect(addedMessage.timestamp).to.deep.equal(rawMessage.timestamp); + }); + + it('can remove from cache', async () => { + const device = PubKey.generate(); + const message = generateUniqueMessage(); + const rawMessage = MessageUtils.toRawMessage(device, message); + + await pendingMessageCacheStub.add(device, message); + + const initialCache = pendingMessageCacheStub.cache; + expect(initialCache).to.have.length(1); + + // Remove the message + await pendingMessageCacheStub.remove(rawMessage); + + const finalCache = pendingMessageCacheStub.cache; + + // Verify that the message was removed + expect(finalCache).to.have.length(0); + }); + + it('can get devices', async () => { + const cacheItems = [ + { + device: PubKey.generate(), + message: generateUniqueMessage(), + }, + { + device: PubKey.generate(), + message: generateUniqueMessage(), + }, + { + device: PubKey.generate(), + message: generateUniqueMessage(), + }, + ]; + + cacheItems.forEach(async item => { + await pendingMessageCacheStub.add(item.device, item.message); + }); + + const { cache } = pendingMessageCacheStub; + expect(cache).to.have.length(cacheItems.length); + + // Get list of devices + const devicesKeys = cacheItems.map(item => item.device.key); + const pulledDevices = pendingMessageCacheStub.getDevices(); + const pulledDevicesKeys = pulledDevices.map(d => d.key); + + // Verify that device list from cache is equivalent to devices added + expect(pulledDevicesKeys).to.have.members(devicesKeys); + }); + + it('can get pending for device', async () => { + const cacheItems = [ + { + device: PubKey.generate(), + message: generateUniqueMessage(), + }, + { + device: PubKey.generate(), + message: generateUniqueMessage(), + }, + ]; + + cacheItems.forEach(async item => { + await pendingMessageCacheStub.add(item.device, item.message); + }); + + const initialCache = pendingMessageCacheStub.cache; + expect(initialCache).to.have.length(cacheItems.length); + + // Get pending for each specific device + cacheItems.forEach(item => { + const pendingForDevice = pendingMessageCacheStub.getForDevice(item.device); + expect(pendingForDevice).to.have.length(1); + expect(pendingForDevice[0].device).to.equal(item.device.key); + }); + }); + + it('can find nothing when empty', async () => { + const device = PubKey.generate(); + const message = generateUniqueMessage(); + const rawMessage = MessageUtils.toRawMessage(device, message); + + const foundMessage = pendingMessageCacheStub.find(rawMessage); + expect(foundMessage, 'a message was found in empty cache').to.be.undefined; + }); + + it('can find message in cache', async () => { + const device = PubKey.generate(); + const message = generateUniqueMessage(); + const rawMessage = MessageUtils.toRawMessage(device, message); + + await pendingMessageCacheStub.add(device, message); + + const finalCache = pendingMessageCacheStub.cache; + expect(finalCache).to.have.length(1); + + const foundMessage = pendingMessageCacheStub.find(rawMessage); + expect(foundMessage, 'message not found in cache').to.be.ok; + foundMessage && expect(foundMessage.device).to.equal(device.key); + }); + + it('can clear cache', async () => { + const cacheItems = [ + { + device: PubKey.generate(), + message: generateUniqueMessage(), + }, + { + device: PubKey.generate(), + message: generateUniqueMessage(), + }, + { + device: PubKey.generate(), + message: generateUniqueMessage(), + }, + ]; + + cacheItems.forEach(async item => { + await pendingMessageCacheStub.add(item.device, item.message); + }); + + const initialCache = pendingMessageCacheStub.cache; + expect(initialCache).to.have.length(cacheItems.length); + + // Clear cache + await pendingMessageCacheStub.clear(); + + const finalCache = pendingMessageCacheStub.cache; + expect(finalCache).to.have.length(0); + }); + +}); diff --git a/ts/test/session/utils/MessageUtils_test.ts b/ts/test/session/utils/MessageUtils_test.ts new file mode 100644 index 000000000..548846f01 --- /dev/null +++ b/ts/test/session/utils/MessageUtils_test.ts @@ -0,0 +1,72 @@ +import { expect, should } from 'chai'; +import { SignalService } from '../../../protobuf'; +import { ChatMessage } from '../../../session/messages/outgoing'; +import { RawMessage } from '../../../session/types/RawMessage'; +import { MessageUtils, PubKey, PubKeyType } from '../../../session/utils'; + +describe('MessageUtils', () => { + it('can convert to RawMessage', () => { + // TOOD: MOVE ME TO MESSAGE UTILS TEST + const pubkey = "0582fe8822c684999663cc6636148328fbd47c0836814c118af4e326bb4f0e1000"; + const messageText = "This is some message content"; + + const isRawMessage = (object: any): object is RawMessage => { + return ( + 'identifier' in object && + 'plainTextBuffer' in object && + 'timestamp' in object && + 'device' in object && + 'ttl' in object && + 'encryption' in object + ); + } + + const message = new ChatMessage({ + body: messageText, + identifier: '1234567890', + timestamp: Date.now(), + attachments: undefined, + quote: undefined, + expireTimer: undefined, + lokiProfile: undefined, + preview: undefined, + }); + + // Explicitly check that it's a RawMessage + const rawMessage = MessageUtils.toRawMessage(pubkey, message); + expect(isRawMessage(rawMessage)).to.be.equal(true); + + // console.log('[vince] isRawMessage(rawMessage):', isRawMessage(rawMessage)); + + // Check plaintext + const plainText = message.plainTextBuffer(); + const decoded = SignalService.Content.decode(plainText); + expect(decoded.dataMessage?.body).to.be.equal(messageText); + }); + + // Pubkeys + it('can create new valid pubkey', () => { + const validPubkey = '0582fe8822c684999663cc6636148328fbd47c0836814c118af4e326bb4f0e1000'; + should().not.Throw(() => new PubKey(validPubkey), Error); + + const pubkey = new PubKey(validPubkey); + expect(pubkey instanceof PubKey).to.be.equal(true); + }); + + it('invalid pubkey should throw error', () => { + const invalidPubkey = 'Lorem Ipsum'; + + should().Throw(() => new PubKey(invalidPubkey), Error); + }); + + it('can set pubkey type', () => { + const validPubkey = '0582fe8822c684999663cc6636148328fbd47c0836814c118af4e326bb4f0e1000'; + const pubkeyType = PubKeyType.Primary; + + should().not.Throw(() => new PubKey(validPubkey, pubkeyType), Error); + + const pubkey = new PubKey(validPubkey, pubkeyType); + expect(pubkey.type).to.be.equal(PubKeyType.Primary); + }); + +}); \ No newline at end of file diff --git a/ts/test/utils/stubs/SignalAddressProtocolStub.ts b/ts/test/utils/stubs/SignalAddressProtocolStub.ts new file mode 100644 index 000000000..4fb5231a7 --- /dev/null +++ b/ts/test/utils/stubs/SignalAddressProtocolStub.ts @@ -0,0 +1,27 @@ +import { SignalProtocolAddress } from '../../../window/types/libsignal-protocol'; + +export class SignalProtocolAddressStub extends SignalProtocolAddress { + private readonly hexEncodedPublicKey: string; + private readonly deviceId: number; + constructor(hexEncodedPublicKey: string, deviceId: number) { + super(hexEncodedPublicKey, deviceId); + this.hexEncodedPublicKey = hexEncodedPublicKey; + this.deviceId = deviceId; + } + + // tslint:disable-next-line: function-name + public static fromString(encodedAddress: string): SignalProtocolAddressStub { + const values = encodedAddress.split('.'); + + return new SignalProtocolAddressStub(values[0], Number(values[1])); + } + + public getName(): string { return this.hexEncodedPublicKey; } + public getDeviceId(): number { return this.deviceId; } + + public equals(other: SignalProtocolAddress): boolean { + return other.getName() === this.hexEncodedPublicKey; + } + + public toString(): string { return this.hexEncodedPublicKey; } +} \ No newline at end of file