Merge pull request #1166 from Bilb/message-sending-refactor
commit
4a09de9aae
@ -1,56 +1,315 @@
|
|||||||
// TODO: Need to flesh out these functions
|
import { SessionRequestMessage } from '../messages/outgoing';
|
||||||
// Structure of this can be changed for example sticking this all in a class
|
// import { MessageSender } from '../sending';
|
||||||
// The reason i haven't done it is to avoid having instances of the protocol, rather you should be able to call the functions directly
|
import { createOrUpdateItem, getItemById } from '../../../js/modules/data';
|
||||||
|
import { libloki, libsignal, textsecure } from '../../window';
|
||||||
|
import { MessageSender } from '../sending';
|
||||||
|
import * as MessageUtils from '../utils';
|
||||||
|
import { PubKey } from '../types';
|
||||||
|
|
||||||
import { SessionResetMessage } from '../messages/outgoing';
|
interface StringToNumberMap {
|
||||||
|
[key: string]: number;
|
||||||
export function hasSession(device: string): boolean {
|
|
||||||
return false; // TODO: Implement
|
|
||||||
}
|
}
|
||||||
|
// tslint:disable: no-unnecessary-class
|
||||||
|
export class SessionProtocol {
|
||||||
|
private static dbLoaded: Boolean = false;
|
||||||
|
/**
|
||||||
|
* This map olds the sent session timestamps, i.e. session requests message effectively sent to the recipient.
|
||||||
|
* It is backed by a database entry so it's loaded from db on startup.
|
||||||
|
* This map should not be used directly, but instead through
|
||||||
|
* `updateSendSessionTimestamp()`, or `hasSendSessionRequest()`
|
||||||
|
*/
|
||||||
|
private static sentSessionsTimestamp: StringToNumberMap;
|
||||||
|
|
||||||
export function hasSentSessionRequest(device: string): boolean {
|
/**
|
||||||
// TODO: need a way to keep track of if we've sent a session request
|
* This map olds the processed session timestamps, i.e. when we received a session request and handled it.
|
||||||
// My idea was to use the timestamp of when it was sent but there might be another better approach
|
* It is backed by a database entry so it's loaded from db on startup.
|
||||||
return false;
|
* This map should not be used directly, but instead through
|
||||||
}
|
* `updateProcessedSessionTimestamp()`, `getProcessedSessionRequest()` or `hasProcessedSessionRequest()`
|
||||||
|
*/
|
||||||
|
private static processedSessionsTimestamp: StringToNumberMap;
|
||||||
|
|
||||||
export async function sendSessionRequestIfNeeded(
|
/**
|
||||||
device: string
|
* This map olds the timestamp on which a sent session reset is triggered for a specific device.
|
||||||
): Promise<void> {
|
* Once the message is sent or failed to sent, this device is removed from here.
|
||||||
if (hasSession(device) || hasSentSessionRequest(device)) {
|
* This is a memory only map. Which means that on app restart it's starts empty.
|
||||||
return Promise.resolve();
|
*/
|
||||||
|
private static readonly pendingSendSessionsTimestamp: Set<string> = new Set();
|
||||||
|
|
||||||
|
public static getSentSessionsTimestamp(): Readonly<StringToNumberMap> {
|
||||||
|
return SessionProtocol.sentSessionsTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Call sendSessionRequest with SessionReset
|
public static getProcessedSessionsTimestamp(): Readonly<StringToNumberMap> {
|
||||||
return Promise.reject(new Error('Need to implement this function'));
|
return SessionProtocol.processedSessionsTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendSessionRequest(
|
public static getPendingSendSessionTimestamp(): Readonly<Set<string>> {
|
||||||
message: SessionResetMessage
|
return SessionProtocol.pendingSendSessionsTimestamp;
|
||||||
): Promise<void> {
|
}
|
||||||
// TODO: Optimistically store timestamp of when session request was sent
|
|
||||||
// TODO: Send out the request via MessageSender
|
|
||||||
// TODO: On failure, unset the timestamp
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sessionEstablished(device: string) {
|
/** Returns true if we already have a session with that device */
|
||||||
// TODO: this is called when we receive an encrypted message from the other user
|
public static async hasSession(pubkey: PubKey): Promise<boolean> {
|
||||||
// Maybe it should be renamed to something else
|
// Session does not use the concept of a deviceId, thus it's always 1
|
||||||
// TODO: This should make `hasSentSessionRequest` return `false`
|
const address = new libsignal.SignalProtocolAddress(pubkey.key, 1);
|
||||||
}
|
const sessionCipher = new libsignal.SessionCipher(
|
||||||
|
textsecure.storage.protocol,
|
||||||
|
address
|
||||||
|
);
|
||||||
|
|
||||||
export function shouldProcessSessionRequest(
|
return sessionCipher.hasOpenSession();
|
||||||
device: string,
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if we sent a session request to that device already OR
|
||||||
|
* if a session request to that device is right now being sent.
|
||||||
|
*/
|
||||||
|
public static async hasSentSessionRequest(pubkey: PubKey): Promise<boolean> {
|
||||||
|
const pendingSend = SessionProtocol.pendingSendSessionsTimestamp.has(
|
||||||
|
pubkey.key
|
||||||
|
);
|
||||||
|
const hasSent = await SessionProtocol.hasAlreadySentSessionRequest(
|
||||||
|
pubkey.key
|
||||||
|
);
|
||||||
|
|
||||||
|
return pendingSend || hasSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers a SessionRequestMessage to be sent if:
|
||||||
|
* - we do not already have a session and
|
||||||
|
* - we did not sent a session request already to that device and
|
||||||
|
* - we do not have a session request currently being sent to that device
|
||||||
|
*/
|
||||||
|
public static async sendSessionRequestIfNeeded(
|
||||||
|
pubkey: PubKey
|
||||||
|
): Promise<void> {
|
||||||
|
if (
|
||||||
|
(await SessionProtocol.hasSession(pubkey)) ||
|
||||||
|
(await SessionProtocol.hasSentSessionRequest(pubkey))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preKeyBundle = await libloki.storage.getPreKeyBundleForContact(
|
||||||
|
pubkey.key
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionReset = new SessionRequestMessage({
|
||||||
|
preKeyBundle,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await SessionProtocol.sendSessionRequest(sessionReset, pubkey);
|
||||||
|
} catch (error) {
|
||||||
|
window.console.warn(
|
||||||
|
'Failed to send session request to:',
|
||||||
|
pubkey.key,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a session request message to that pubkey.
|
||||||
|
* We store the sent timestamp only if the message is effectively sent.
|
||||||
|
*/
|
||||||
|
public static async sendSessionRequest(
|
||||||
|
message: SessionRequestMessage,
|
||||||
|
pubkey: PubKey
|
||||||
|
): Promise<void> {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
// mark the session as being pending send with current timestamp
|
||||||
|
// so we know we already triggered a new session with that device
|
||||||
|
// so sendSessionRequestIfNeeded does not sent another session request
|
||||||
|
SessionProtocol.pendingSendSessionsTimestamp.add(pubkey.key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawMessage = MessageUtils.toRawMessage(pubkey, message);
|
||||||
|
await MessageSender.send(rawMessage);
|
||||||
|
await SessionProtocol.updateSentSessionTimestamp(pubkey.key, timestamp);
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
SessionProtocol.pendingSendSessionsTimestamp.delete(pubkey.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a session is establish so we store on database this info.
|
||||||
|
*/
|
||||||
|
public static async onSessionEstablished(pubkey: PubKey) {
|
||||||
|
// remove our existing sent timestamp for that device
|
||||||
|
return SessionProtocol.updateSentSessionTimestamp(pubkey.key, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async shouldProcessSessionRequest(
|
||||||
|
pubkey: PubKey,
|
||||||
messageTimestamp: number
|
messageTimestamp: number
|
||||||
): boolean {
|
): Promise<boolean> {
|
||||||
// TODO: Need to do the following here
|
const existingSentTimestamp =
|
||||||
// messageTimestamp > session request sent timestamp && messageTimestamp > session request processed timestamp
|
(await SessionProtocol.getSentSessionRequest(pubkey.key)) || 0;
|
||||||
|
const existingProcessedTimestamp =
|
||||||
|
(await SessionProtocol.getProcessedSessionRequest(pubkey.key)) || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
messageTimestamp > existingSentTimestamp &&
|
||||||
|
messageTimestamp > existingProcessedTimestamp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async onSessionRequestProcessed(pubkey: PubKey) {
|
||||||
|
return SessionProtocol.updateProcessedSessionTimestamp(
|
||||||
|
pubkey.key,
|
||||||
|
Date.now()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static reset() {
|
||||||
|
SessionProtocol.dbLoaded = false;
|
||||||
|
SessionProtocol.sentSessionsTimestamp = {};
|
||||||
|
SessionProtocol.processedSessionsTimestamp = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We only need to fetch once from the database, because we are the only one writing to it
|
||||||
|
*/
|
||||||
|
private static async fetchFromDBIfNeeded(): Promise<void> {
|
||||||
|
if (!SessionProtocol.dbLoaded) {
|
||||||
|
const sentItem = await getItemById('sentSessionsTimestamp');
|
||||||
|
if (sentItem) {
|
||||||
|
SessionProtocol.sentSessionsTimestamp = sentItem.value;
|
||||||
|
} else {
|
||||||
|
SessionProtocol.sentSessionsTimestamp = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedItem = await getItemById('processedSessionsTimestamp');
|
||||||
|
if (processedItem) {
|
||||||
|
SessionProtocol.processedSessionsTimestamp = processedItem.value;
|
||||||
|
} else {
|
||||||
|
SessionProtocol.processedSessionsTimestamp = {};
|
||||||
|
}
|
||||||
|
SessionProtocol.dbLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async writeToDBSentSessions(): Promise<void> {
|
||||||
|
const data = {
|
||||||
|
id: 'sentSessionsTimestamp',
|
||||||
|
value: JSON.stringify(SessionProtocol.sentSessionsTimestamp),
|
||||||
|
};
|
||||||
|
|
||||||
|
await createOrUpdateItem(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async writeToDBProcessedSessions(): Promise<void> {
|
||||||
|
const data = {
|
||||||
|
id: 'processedSessionsTimestamp',
|
||||||
|
value: JSON.stringify(SessionProtocol.processedSessionsTimestamp),
|
||||||
|
};
|
||||||
|
|
||||||
|
await createOrUpdateItem(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a utility function to avoid duplicated code of updateSentSessionTimestamp and updateProcessedSessionTimestamp
|
||||||
|
*/
|
||||||
|
private static async updateSessionTimestamp(
|
||||||
|
device: string,
|
||||||
|
timestamp: number | undefined,
|
||||||
|
map: StringToNumberMap
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!timestamp) {
|
||||||
|
if (device in map) {
|
||||||
|
// tslint:disable-next-line: no-dynamic-delete
|
||||||
|
delete map[device];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
map[device] = timestamp;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function sessionRequestProcessed(device: string) {
|
/**
|
||||||
// TODO: this is called when we process the session request
|
*
|
||||||
// This should store the processed timestamp
|
* @param device the device id
|
||||||
// Again naming is crap so maybe some other name is better
|
* @param timestamp undefined to remove the key/value pair, otherwise updates the sent timestamp and write to DB
|
||||||
|
*/
|
||||||
|
private static async updateSentSessionTimestamp(
|
||||||
|
device: string,
|
||||||
|
timestamp: number | undefined
|
||||||
|
): Promise<void> {
|
||||||
|
await SessionProtocol.fetchFromDBIfNeeded();
|
||||||
|
if (
|
||||||
|
SessionProtocol.updateSessionTimestamp(
|
||||||
|
device,
|
||||||
|
timestamp,
|
||||||
|
SessionProtocol.sentSessionsTimestamp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await SessionProtocol.writeToDBSentSessions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* timestamp undefined to remove the key/value pair, otherwise updates the processed timestamp and writes to DB
|
||||||
|
*/
|
||||||
|
private static async updateProcessedSessionTimestamp(
|
||||||
|
device: string,
|
||||||
|
timestamp: number | undefined
|
||||||
|
): Promise<void> {
|
||||||
|
await SessionProtocol.fetchFromDBIfNeeded();
|
||||||
|
if (
|
||||||
|
SessionProtocol.updateSessionTimestamp(
|
||||||
|
device,
|
||||||
|
timestamp,
|
||||||
|
SessionProtocol.processedSessionsTimestamp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await SessionProtocol.writeToDBProcessedSessions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a utility function to avoid duplicate code between `getProcessedSessionRequest()` and `getSentSessionRequest()`
|
||||||
|
*/
|
||||||
|
private static async getSessionRequest(
|
||||||
|
device: string,
|
||||||
|
map: StringToNumberMap
|
||||||
|
): Promise<number | undefined> {
|
||||||
|
await SessionProtocol.fetchFromDBIfNeeded();
|
||||||
|
|
||||||
|
return map[device];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getSentSessionRequest(
|
||||||
|
device: string
|
||||||
|
): Promise<number | undefined> {
|
||||||
|
return SessionProtocol.getSessionRequest(
|
||||||
|
device,
|
||||||
|
SessionProtocol.sentSessionsTimestamp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getProcessedSessionRequest(
|
||||||
|
device: string
|
||||||
|
): Promise<number | undefined> {
|
||||||
|
return SessionProtocol.getSessionRequest(
|
||||||
|
device,
|
||||||
|
SessionProtocol.processedSessionsTimestamp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async hasAlreadySentSessionRequest(
|
||||||
|
device: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
await SessionProtocol.fetchFromDBIfNeeded();
|
||||||
|
|
||||||
|
return !!SessionProtocol.sentSessionsTimestamp[device];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as SessionProtocol from './SessionProtocol';
|
import { SessionProtocol } from './SessionProtocol';
|
||||||
import * as MultiDeviceProtocol from './MultiDeviceProtocol';
|
import * as MultiDeviceProtocol from './MultiDeviceProtocol';
|
||||||
|
|
||||||
export { SessionProtocol, MultiDeviceProtocol };
|
export { SessionProtocol, MultiDeviceProtocol };
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export enum EncryptionType {
|
export enum EncryptionType {
|
||||||
Signal,
|
Signal,
|
||||||
SessionReset,
|
SessionRequest,
|
||||||
MediumGroup,
|
MediumGroup,
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,329 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
import { SessionProtocol } from '../../../session/protocols';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
import { Stubs, TestUtils, timeout } from '../../test-utils';
|
||||||
|
import { UserUtil } from '../../../util';
|
||||||
|
import { SessionRequestMessage } from '../../../session/messages/outgoing';
|
||||||
|
import { TextEncoder } from 'util';
|
||||||
|
import { MessageSender } from '../../../session/sending';
|
||||||
|
import { PubKey } from '../../../session/types';
|
||||||
|
|
||||||
|
// tslint:disable-next-line: max-func-body-length
|
||||||
|
describe('SessionProtocol', () => {
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
const ourNumber = 'ourNumber';
|
||||||
|
const pubkey = new PubKey('deviceid');
|
||||||
|
let getItemById: sinon.SinonStub;
|
||||||
|
let send: sinon.SinonStub;
|
||||||
|
|
||||||
|
const resetMessage: SessionRequestMessage = new SessionRequestMessage({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
preKeyBundle: {
|
||||||
|
identityKey: new TextEncoder().encode('identityKey'),
|
||||||
|
deviceId: 1,
|
||||||
|
preKeyId: 2,
|
||||||
|
signedKeyId: 3,
|
||||||
|
preKey: new TextEncoder().encode('preKey'),
|
||||||
|
signedKey: new TextEncoder().encode('signedKey'),
|
||||||
|
signature: new TextEncoder().encode('signature'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestUtils.stubWindow('libsignal', {
|
||||||
|
SignalProtocolAddress: sandbox.stub(),
|
||||||
|
SessionCipher: Stubs.SessionCipherStub,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
TestUtils.stubWindow('libloki', {
|
||||||
|
storage: {
|
||||||
|
getPreKeyBundleForContact: sandbox.stub(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
TestUtils.stubWindow('textsecure', {
|
||||||
|
storage: {
|
||||||
|
protocol: sandbox.stub(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
TestUtils.stubData('createOrUpdateItem');
|
||||||
|
|
||||||
|
getItemById = TestUtils.stubData('getItemById').resolves({ value: {} });
|
||||||
|
|
||||||
|
sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber);
|
||||||
|
send = sandbox.stub(MessageSender, 'send' as any);
|
||||||
|
SessionProtocol.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
TestUtils.restoreStubs();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('db fetch', () => {
|
||||||
|
it('protocol: should fetch from DB `sentSessionsTimestamp` and `processedSessionsTimestamp`', async () => {
|
||||||
|
await SessionProtocol.hasSentSessionRequest(pubkey);
|
||||||
|
expect(getItemById.calledWith('sentSessionsTimestamp'));
|
||||||
|
expect(getItemById.calledWith('processedSessionsTimestamp'));
|
||||||
|
expect(getItemById.callCount).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: should fetch only once', async () => {
|
||||||
|
await SessionProtocol.hasSentSessionRequest(pubkey);
|
||||||
|
await SessionProtocol.hasSentSessionRequest(pubkey);
|
||||||
|
await SessionProtocol.hasSentSessionRequest(pubkey);
|
||||||
|
await SessionProtocol.hasSentSessionRequest(pubkey);
|
||||||
|
expect(getItemById.calledWith('sentSessionsTimestamp'));
|
||||||
|
expect(getItemById.calledWith('processedSessionsTimestamp'));
|
||||||
|
expect(getItemById.callCount).to.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendSessionRequest', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// trigger a sessionReset
|
||||||
|
await SessionProtocol.sendSessionRequest(resetMessage, pubkey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: sendSessionRequest should add the deviceID to the sentMap', async () => {
|
||||||
|
expect(SessionProtocol.getSentSessionsTimestamp())
|
||||||
|
.to.have.property('deviceid')
|
||||||
|
.to.be.approximately(Date.now(), 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: sendSessionRequest should not have pendingSend set after', async () => {
|
||||||
|
expect(
|
||||||
|
SessionProtocol.getPendingSendSessionTimestamp()
|
||||||
|
).to.not.have.property('deviceid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onSessionEstablished', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// add an existing entry in the sentMap
|
||||||
|
await SessionProtocol.sendSessionRequest(resetMessage, pubkey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: onSessionEstablished should remove the device in sentTimestamps', async () => {
|
||||||
|
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
|
||||||
|
'deviceid'
|
||||||
|
);
|
||||||
|
await SessionProtocol.onSessionEstablished(pubkey);
|
||||||
|
expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property(
|
||||||
|
'deviceid'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: onSessionEstablished should remove the device in sentTimestamps and ONLY that one', async () => {
|
||||||
|
// add a second item to the map
|
||||||
|
await SessionProtocol.sendSessionRequest(
|
||||||
|
resetMessage,
|
||||||
|
new PubKey('deviceid2')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
|
||||||
|
'deviceid'
|
||||||
|
);
|
||||||
|
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
|
||||||
|
'deviceid2'
|
||||||
|
);
|
||||||
|
|
||||||
|
await SessionProtocol.onSessionEstablished(pubkey);
|
||||||
|
expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property(
|
||||||
|
'deviceid'
|
||||||
|
);
|
||||||
|
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
|
||||||
|
'deviceid2'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasSentSessionRequest', () => {
|
||||||
|
it('protocol: hasSentSessionRequest returns false if a message was not sent to that device', async () => {
|
||||||
|
const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey);
|
||||||
|
expect(hasSent).to.be.equal(
|
||||||
|
false,
|
||||||
|
'hasSent should be false for `deviceid`'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: hasSentSessionRequest returns true if a message is already sent for that device', async () => {
|
||||||
|
// add an existing entry in the sentMap
|
||||||
|
await SessionProtocol.sendSessionRequest(resetMessage, pubkey);
|
||||||
|
const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey);
|
||||||
|
expect(hasSent).to.be.equal(
|
||||||
|
true,
|
||||||
|
'hasSent should be true for `deviceid`'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO add a test to validate that pending is filled when message is triggered and not yet sent
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendSessionRequestIfNeeded', () => {
|
||||||
|
it('protocol: sendSessionRequestIfNeeded should send a new sessionMessage ', async () => {
|
||||||
|
// not called before, so the message reset sending should be triggered
|
||||||
|
await SessionProtocol.sendSessionRequestIfNeeded(pubkey);
|
||||||
|
expect(send.callCount).to.be.equal(
|
||||||
|
1,
|
||||||
|
'MessageSender.send() should have been called'
|
||||||
|
);
|
||||||
|
|
||||||
|
// check that the map is updated with that ID
|
||||||
|
const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey);
|
||||||
|
expect(hasSent).to.be.equal(
|
||||||
|
true,
|
||||||
|
'hasSent should be true for `deviceid`'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: sendSessionRequestIfNeeded should NOT send a new sessionMessage on second try ', async () => {
|
||||||
|
await SessionProtocol.sendSessionRequestIfNeeded(pubkey);
|
||||||
|
expect(send.callCount).to.be.equal(
|
||||||
|
1,
|
||||||
|
'MessageSender.send() should have been called'
|
||||||
|
);
|
||||||
|
|
||||||
|
// check that the map is updated with that ID
|
||||||
|
const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey);
|
||||||
|
expect(hasSent).to.be.equal(
|
||||||
|
true,
|
||||||
|
'hasSent should be true for `deviceid`'
|
||||||
|
);
|
||||||
|
send.resetHistory();
|
||||||
|
|
||||||
|
// trigger a second call, Message.send().calledCount should still be 1
|
||||||
|
await SessionProtocol.sendSessionRequestIfNeeded(pubkey);
|
||||||
|
expect(send.callCount).to.be.equal(
|
||||||
|
0,
|
||||||
|
'MessageSender.send() should NOT have been called a second time'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onSessionRequestProcessed', () => {
|
||||||
|
it('protocol: onSessionRequestProcessed should insert a new item in the processedMap ', async () => {
|
||||||
|
// trigger the requestProcessed and check the map is updated
|
||||||
|
await SessionProtocol.onSessionRequestProcessed(pubkey);
|
||||||
|
expect(SessionProtocol.getProcessedSessionsTimestamp())
|
||||||
|
.to.have.property('deviceid')
|
||||||
|
.to.be.approximately(Date.now(), 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: onSessionRequestProcessed should update an existing item in the processedMap ', async () => {
|
||||||
|
// trigger the requestProcessed and check the map is updated
|
||||||
|
// then trigger it a second time, and expect a change in the processed timestamp
|
||||||
|
|
||||||
|
await SessionProtocol.onSessionRequestProcessed(pubkey);
|
||||||
|
expect(SessionProtocol.getProcessedSessionsTimestamp())
|
||||||
|
.to.have.property('deviceid')
|
||||||
|
.to.be.approximately(Date.now(), 5);
|
||||||
|
await timeout(5);
|
||||||
|
const oldTimestamp = SessionProtocol.getProcessedSessionsTimestamp()
|
||||||
|
.deviceid;
|
||||||
|
await SessionProtocol.onSessionRequestProcessed(pubkey);
|
||||||
|
expect(SessionProtocol.getProcessedSessionsTimestamp())
|
||||||
|
.to.have.property('deviceid')
|
||||||
|
.to.be.approximately(Date.now(), 5)
|
||||||
|
.to.not.be.equal(oldTimestamp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shouldProcessSessionRequest', () => {
|
||||||
|
it('protocol: shouldProcessSessionRequest returns true if timestamp is more recent than processed timestamp', async () => {
|
||||||
|
await SessionProtocol.onSessionRequestProcessed(pubkey); // adds a Date.now() entry
|
||||||
|
expect(
|
||||||
|
SessionProtocol.shouldProcessSessionRequest(pubkey, Date.now() + 1000)
|
||||||
|
).to.be.eventually.equal(
|
||||||
|
true,
|
||||||
|
'shouldProcessSessionRequest should return true when existingProcessed is less recent'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: shouldProcessSessionRequest returns true if there is no processed timestamp yet for this device', async () => {
|
||||||
|
expect(
|
||||||
|
SessionProtocol.shouldProcessSessionRequest(pubkey, 100)
|
||||||
|
).to.be.eventually.equal(
|
||||||
|
true,
|
||||||
|
'shouldProcessSessionRequest should return false when existingProcessed is empty for this device'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: shouldProcessSessionRequest returns false if timestamp is less recent than current processed timestamp', async () => {
|
||||||
|
await SessionProtocol.onSessionRequestProcessed(pubkey); // adds a Date.now() entry
|
||||||
|
expect(
|
||||||
|
SessionProtocol.shouldProcessSessionRequest(pubkey, 100)
|
||||||
|
).to.be.eventually.equal(
|
||||||
|
false,
|
||||||
|
'shouldProcessSessionRequest should return false when existingProcessed is more recent'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: shouldProcessSessionRequest returns false if timestamp is less recent than current sent timestamp', async () => {
|
||||||
|
await SessionProtocol.sendSessionRequest(resetMessage, pubkey); // adds a Date.now() entry
|
||||||
|
expect(
|
||||||
|
SessionProtocol.shouldProcessSessionRequest(pubkey, 100)
|
||||||
|
).to.be.eventually.equal(
|
||||||
|
false,
|
||||||
|
'shouldProcessSessionRequest should return false when existingSent is more recent'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: shouldProcessSessionRequest returns true if timestamp is more recent than current sent timestamp', async () => {
|
||||||
|
await SessionProtocol.sendSessionRequest(resetMessage, pubkey); // adds a Date.now() entry
|
||||||
|
expect(
|
||||||
|
SessionProtocol.shouldProcessSessionRequest(pubkey, Date.now() + 1000)
|
||||||
|
).to.be.eventually.equal(
|
||||||
|
true,
|
||||||
|
'shouldProcessSessionRequest should return true when existingSent is less recent'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: shouldProcessSessionRequest returns true if there is no sent timestamp', async () => {
|
||||||
|
expect(
|
||||||
|
SessionProtocol.shouldProcessSessionRequest(pubkey, 100)
|
||||||
|
).to.be.eventually.equal(
|
||||||
|
true,
|
||||||
|
'shouldProcessSessionRequest should return true as there is no sent timestamp'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: shouldProcessSessionRequest returns false if there is a more recent sent but a less recent processed', async () => {
|
||||||
|
await SessionProtocol.sendSessionRequest(resetMessage, pubkey); // adds a Date.now() entry
|
||||||
|
await timeout(100);
|
||||||
|
await SessionProtocol.onSessionRequestProcessed(pubkey); // adds a Date.now() entry 100ms after
|
||||||
|
|
||||||
|
expect(
|
||||||
|
SessionProtocol.shouldProcessSessionRequest(pubkey, Date.now() - 50)
|
||||||
|
).to.be.eventually.equal(
|
||||||
|
false,
|
||||||
|
'shouldProcessSessionRequest should return false if there is a more recent sent but a less recent processed'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: shouldProcessSessionRequest returns false if there is a more recent processed but a less recent sent', async () => {
|
||||||
|
await SessionProtocol.onSessionRequestProcessed(pubkey); // adds a Date.now() entry
|
||||||
|
await timeout(100);
|
||||||
|
await SessionProtocol.sendSessionRequest(resetMessage, pubkey); // adds a Date.now() entry 100ms after
|
||||||
|
|
||||||
|
expect(
|
||||||
|
SessionProtocol.shouldProcessSessionRequest(pubkey, Date.now() - 50)
|
||||||
|
).to.be.eventually.equal(
|
||||||
|
false,
|
||||||
|
'shouldProcessSessionRequest should return false if there is a more recent processed but a less recent sent'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol: shouldProcessSessionRequest returns true if both sent and processed timestamp are older', async () => {
|
||||||
|
await SessionProtocol.onSessionRequestProcessed(pubkey); // adds a Date.now() entry
|
||||||
|
await SessionProtocol.sendSessionRequest(resetMessage, pubkey); // adds a Date.now() entry
|
||||||
|
expect(
|
||||||
|
SessionProtocol.shouldProcessSessionRequest(pubkey, Date.now() + 1000)
|
||||||
|
).to.be.eventually.equal(
|
||||||
|
true,
|
||||||
|
'shouldProcessSessionRequest should return true if there if both processed and sent are set but are older'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue