Merge branch 'clearnet' of https://github.com/loki-project/session-desktop into utils-tests

This commit is contained in:
Vincent 2020-06-29 08:29:47 +10:00
commit e2e867d5c1
8 changed files with 129 additions and 156 deletions

View file

@ -1640,6 +1640,15 @@
}
}
libsession.Protocols.SessionProtocol.checkSessionRequestExpiry().catch(
e => {
window.log.error(
'Error occured which checking for session request expiry',
e
);
}
);
storage.onready(async () => {
idleDetector.start();
});

View file

@ -1,5 +1,5 @@
import { ConversationType } from '../../ts/state/ducks/conversations';
import { Mesasge } from '../../ts/types/Message';
import { Message } from '../../ts/types/Message';
export type IdentityKey = {
id: string;

View file

@ -17,6 +17,7 @@ interface SessionRequestParams extends MessageParams {
}
export class SessionRequestMessage extends ContentMessage {
public static readonly ttl = 4 * 24 * 60 * 60 * 1000; // 4 days
private readonly preKeyBundle: PreKeyBundleType;
constructor(params: SessionRequestParams) {
@ -25,7 +26,7 @@ export class SessionRequestMessage extends ContentMessage {
}
public ttl(): number {
return 4 * 24 * 60 * 60 * 1000; // 4 days
return SessionRequestMessage.ttl;
}
protected getPreKeyBundleMessage(): SignalService.PreKeyBundleMessage {

View file

@ -12,7 +12,7 @@ interface StringToNumberMap {
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.
* This map holds 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()`
@ -73,6 +73,29 @@ export class SessionProtocol {
return pendingSend || hasSent;
}
/**
* Checks to see if any outgoing session requests have expired and re-sends them again if they have.
*/
public static async checkSessionRequestExpiry(): Promise<any> {
await this.fetchFromDBIfNeeded();
const now = Date.now();
const sentTimestamps = Object.entries(this.sentSessionsTimestamp);
const promises = sentTimestamps.map(async ([device, sent]) => {
const expireTime = sent + SessionRequestMessage.ttl;
// Check if we need to send a session request
if (now < expireTime) {
return;
}
// Unset the timestamp, so that if it fails to send in this function, it will be guaranteed to send later on.
await this.updateSentSessionTimestamp(device, undefined);
await this.sendSessionRequestIfNeeded(new PubKey(device));
});
return Promise.all(promises) as Promise<any>;
}
/**
* Triggers a SessionRequestMessage to be sent if:
* - we do not already have a session and

View file

@ -11,7 +11,7 @@ import { PubKey } from '../../../session/types';
// tslint:disable-next-line: max-func-body-length
describe('SessionProtocol', () => {
const sandbox = sinon.createSandbox();
const ourNumber = 'ourNumber';
const ourNumber = TestUtils.generateFakePubKey();
const pubkey = TestUtils.generateFakePubKey();
let getItemById: sinon.SinonStub;
let send: sinon.SinonStub;
@ -51,7 +51,7 @@ describe('SessionProtocol', () => {
getItemById = TestUtils.stubData('getItemById').resolves({ value: {} });
sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber);
sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber.key);
send = sandbox.stub(MessageSender, 'send' as any);
SessionProtocol.reset();
});
@ -99,6 +99,80 @@ describe('SessionProtocol', () => {
});
});
describe('checkSessionRequestExpiry', () => {
let clock: sinon.SinonFakeTimers;
let now: number;
let sendSessionRequestStub: sinon.SinonStub<
[SessionRequestMessage, PubKey],
Promise<void>
>;
beforeEach(() => {
now = Date.now();
clock = sandbox.useFakeTimers(now);
sendSessionRequestStub = sandbox
.stub(SessionProtocol, 'sendSessionRequest')
.resolves();
});
it('should not send a session request if none have expired', async () => {
getItemById.withArgs('sentSessionsTimestamp').resolves({
id: 'sentSessionsTimestamp',
value: {
[pubkey.key]: now,
},
});
// Set the time just before expiry
clock.tick(SessionRequestMessage.ttl - 100);
await SessionProtocol.checkSessionRequestExpiry();
expect(getItemById.calledWith('sentSessionsTimestamp'));
expect(sendSessionRequestStub.callCount).to.equal(0);
});
it('should send a session request if expired', async () => {
getItemById.withArgs('sentSessionsTimestamp').resolves({
id: 'sentSessionsTimestamp',
value: {
[pubkey.key]: now,
},
});
// Expire the request
clock.tick(SessionRequestMessage.ttl + 100);
await SessionProtocol.checkSessionRequestExpiry();
expect(getItemById.calledWith('sentSessionsTimestamp'));
expect(sendSessionRequestStub.callCount).to.equal(1);
});
it('should remove the old sent timestamp when expired', async () => {
getItemById.withArgs('sentSessionsTimestamp').resolves({
id: 'sentSessionsTimestamp',
value: {
[pubkey.key]: now,
},
});
// Remove this call from the equation
sandbox.stub(SessionProtocol, 'sendSessionRequestIfNeeded').resolves();
// Expire the request
clock.tick(SessionRequestMessage.ttl + 100);
await SessionProtocol.checkSessionRequestExpiry();
expect(getItemById.calledWith('sentSessionsTimestamp'));
expect(await SessionProtocol.hasSentSessionRequest(pubkey)).to.equal(
false,
'hasSentSessionRequest should return false.'
);
expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property(
pubkey.key
);
});
});
describe('onSessionEstablished', () => {
beforeEach(async () => {
// add an existing entry in the sentMap

View file

@ -9,12 +9,23 @@ chai.use(chaiAsPromised);
const { expect } = chai;
describe('Promise Utils', () => {
const sandbox = sinon.createSandbox();
let pollSpy: sinon.SinonSpy<[(done: (arg: any) => void) => Promise<void> | void, (Partial<PromiseUtils.PollOptions> | undefined)?], Promise<void>>;
let waitForTaskSpy: sinon.SinonSpy<[(done: (arg: any) => void) => Promise<void> | void, (number | undefined)?], Promise<unknown>>;
let waitUntilSpy: sinon.SinonSpy<[() => Promise<boolean> | boolean, (number | undefined)?], Promise<void>>;
let pollSpy: sinon.SinonSpy<
[
(done: (arg: any) => void) => Promise<void> | void,
(Partial<PromiseUtils.PollOptions> | undefined)?
],
Promise<void>
>;
let waitForTaskSpy: sinon.SinonSpy<
[(done: (arg: any) => void) => Promise<void> | void, (number | undefined)?],
Promise<unknown>
>;
let waitUntilSpy: sinon.SinonSpy<
[() => Promise<boolean> | boolean, (number | undefined)?],
Promise<void>
>;
beforeEach(() => {
pollSpy = sandbox.spy(PromiseUtils, 'poll');

View file

@ -51,10 +51,10 @@ describe('Sync Message Utils', () => {
const numConversations = 20;
const primaryConversations = new Array(numConversations / 2)
.fill({})
.map(() => new TestUtils.MockPrivateConversation({ isPrimary: true }));
.map(() => new TestUtils.MockConversation({ type: 'primary' }));
const secondaryConversations = new Array(numConversations / 2)
.fill({})
.map(() => new TestUtils.MockPrivateConversation({ isPrimary: false }));
.map(() => new TestUtils.MockConversation({ type: 'secondary' }));
const conversations = [...primaryConversations, ...secondaryConversations];
const sandbox = sinon.createSandbox();

View file

@ -1,145 +0,0 @@
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const sinon = __importStar(require("sinon"));
const crypto = __importStar(require("crypto"));
const uuid_1 = require("uuid");
const types_1 = require("../../../ts/session/types");
const outgoing_1 = require("../../session/messages/outgoing");
const _1 = require(".");
const globalAny = global;
const sandbox = sinon.createSandbox();
// We have to do this in a weird way because Data uses module.exports
// which doesn't play well with sinon or ImportMock
// tslint:disable-next-line: no-require-imports no-var-requires
const Data = require('../../../js/modules/data');
/**
* Stub a function inside Data.
*
* Note: This uses a custom sandbox.
* Please call `restoreStubs()` or `stub.restore()` to restore original functionality.
*/
function stubData(fn) {
return sandbox.stub(Data, fn);
}
exports.stubData = stubData;
/**
* Stub a window object.
*
* Note: This uses a custom sandbox.
* Please call `restoreStubs()` or `stub.restore()` to restore original functionality.
*/
function stubWindow(fn, value) {
// tslint:disable-next-line: no-typeof-undefined
if (typeof globalAny.window === 'undefined') {
globalAny.window = {};
}
const set = (newValue) => {
globalAny.window[fn] = newValue;
};
const get = () => {
return globalAny.window[fn];
};
globalAny.window[fn] = value;
return {
get,
set,
};
}
exports.stubWindow = stubWindow;
function restoreStubs() {
globalAny.window = undefined;
sandbox.restore();
}
exports.restoreStubs = restoreStubs;
function generateFakePubKey() {
// Generates a mock pubkey for testing
const numBytes = types_1.PubKey.PUBKEY_LEN / 2 - 1;
const hexBuffer = crypto.randomBytes(numBytes).toString('hex');
const pubkeyString = `05${hexBuffer}`;
return new types_1.PubKey(pubkeyString);
}
exports.generateFakePubKey = generateFakePubKey;
function generateFakePubKeys(amount) {
const numPubKeys = amount > 0 ? Math.floor(amount) : 0;
// tslint:disable-next-line: no-unnecessary-callback-wrapper
return new Array(numPubKeys).fill(0).map(() => generateFakePubKey());
}
exports.generateFakePubKeys = generateFakePubKeys;
function generateChatMessage(identifier) {
return new outgoing_1.ChatMessage({
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
identifier: (identifier !== null && identifier !== void 0 ? identifier : uuid_1.v4()),
timestamp: Date.now(),
attachments: undefined,
quote: undefined,
expireTimer: undefined,
lokiProfile: undefined,
preview: undefined,
});
}
exports.generateChatMessage = generateChatMessage;
function generateOpenGroupMessage() {
const group = new types_1.OpenGroup({
server: 'chat.example.server',
channel: 0,
conversationId: '0',
});
return new outgoing_1.OpenGroupMessage({
timestamp: Date.now(),
group,
attachments: undefined,
preview: undefined,
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
quote: undefined,
});
}
exports.generateOpenGroupMessage = generateOpenGroupMessage;
function generateClosedGroupMessage(groupId) {
return new outgoing_1.ClosedGroupChatMessage({
identifier: uuid_1.v4(),
groupId: (groupId !== null && groupId !== void 0 ? groupId : generateFakePubKey().key),
chatMessage: generateChatMessage(),
});
}
exports.generateClosedGroupMessage = generateClosedGroupMessage;
class MockPrivateConversation {
constructor(params) {
var _a;
const dayInSeconds = 86400;
this.isPrimary = params.isPrimary;
this.id = (_a = params.id, (_a !== null && _a !== void 0 ? _a : _1.TestUtils.generateFakePubKey().key));
this.attributes = {
members: [],
left: false,
expireTimer: dayInSeconds,
profileSharing: true,
mentionedUs: false,
unreadCount: 99,
isArchived: false,
active_at: Date.now(),
timestamp: Date.now(),
secondaryStatus: !this.isPrimary,
};
}
isPrivate() {
return true;
}
isOurLocalDevice() {
return false;
}
isBlocked() {
return false;
}
getPrimaryDevicePubKey() {
return this.isPrimary ? this.id : _1.TestUtils.generateFakePubKey().key;
}
}
exports.MockPrivateConversation = MockPrivateConversation;
//# sourceMappingURL=testUtils.js.map