Merge pull request #1180 from Mikunj/multi-device-protocol-test

Added multi device protocol tests
pull/1182/head
Mikunj Varsani 5 years ago committed by GitHub
commit 48fc60f0d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -625,7 +625,7 @@ async function createOrUpdatePairingAuthorisation(data) {
}
async function getPairingAuthorisationsFor(pubKey) {
const authorisations = channels.getPairingAuthorisationsFor(pubKey);
const authorisations = await channels.getPairingAuthorisationsFor(pubKey);
return authorisations.map(authorisation => ({
...authorisation,

@ -7,6 +7,7 @@ import {
} from '../../../js/modules/data';
import { PrimaryPubKey, PubKey, SecondaryPubKey } from '../types';
import { UserUtil } from '../../util';
import { BufferUtils } from '../utils';
/*
The reason we're exporing a class here instead of just exporting the functions directly is for the sake of testing.
@ -34,14 +35,14 @@ export class MultiDeviceProtocol {
}
// We always prefer our local pairing over the one on the server
const ourDevices = await this.getAllDevices(ourKey);
if (ourDevices.some(d => d.key === device.key)) {
const isOurDevice = await this.isOurDevice(device);
if (isOurDevice) {
return;
}
// Only fetch if we hit the refresh delay
const lastFetchTime = this.lastFetch[device.key];
if (lastFetchTime && lastFetchTime + this.refreshDelay < Date.now()) {
if (lastFetchTime && lastFetchTime + this.refreshDelay > Date.now()) {
return;
}
@ -49,7 +50,6 @@ export class MultiDeviceProtocol {
try {
const authorisations = await this.fetchPairingAuthorisations(device);
// TODO: validate?
await Promise.all(authorisations.map(this.savePairingAuthorisation));
} catch (e) {
// Something went wrong, let it re-try another time
@ -93,8 +93,9 @@ export class MultiDeviceProtocol {
}) => ({
primaryDevicePubKey,
secondaryDevicePubKey,
requestSignature: Buffer.from(requestSignature, 'base64').buffer,
grantSignature: Buffer.from(grantSignature, 'base64').buffer,
requestSignature: BufferUtils.base64toUint8Array(requestSignature)
.buffer,
grantSignature: BufferUtils.base64toUint8Array(grantSignature).buffer,
})
);
}

@ -0,0 +1,18 @@
/**
* Convert base64 string into a Uint8Array.
*
* The reason for this function is to avoid a very weird issue when converting to and from base64.
* ```
* const base64 = <base64string>;
* const arrayBuffer = Buffer.from(base64, 'base64').buffer;
* const reconstructedBase64 = Buffer.from(arrayBuffer).toString('base64');
* expect(base64 === reconstructedBase64) // This returns false!!
* ```
*
* I have no idea why that doesn't work but a work around is to wrap the original base64 buffer in a Uin8Array before calling `buffer` on it.
*
* @param base64 The base 64 string.
*/
export function base64toUint8Array(base64: string): Uint8Array {
return new Uint8Array(Buffer.from(base64, 'base64'));
}

@ -1,8 +1,9 @@
import * as MessageUtils from './Messages';
import * as GroupUtils from './Groups';
import * as SyncMessageUtils from './SyncMessageUtils';
import * as BufferUtils from './Buffer';
export * from './TypedEmitter';
export * from './JobQueue';
export { MessageUtils, SyncMessageUtils, GroupUtils };
export { MessageUtils, SyncMessageUtils, GroupUtils, BufferUtils };

@ -0,0 +1,290 @@
import { expect } from 'chai';
import * as sinon from 'sinon';
import { TestUtils, timeout } from '../../test-utils';
import { PairingAuthorisation } from '../../../../js/modules/data';
import { MultiDeviceProtocol } from '../../../session/protocols';
import { PubKey } from '../../../session/types';
import { UserUtil } from '../../../util';
function generateFakeAuthorisations(
primary: PubKey,
otherDevices: Array<PubKey>
): Array<PairingAuthorisation> {
return otherDevices.map(
device =>
({
primaryDevicePubKey: primary.key,
secondaryDevicePubKey: device.key,
requestSignature: new Uint8Array(0),
grantSignature: new Uint8Array(1),
} as PairingAuthorisation)
);
}
describe('MultiDeviceProtocol', () => {
const sandbox = sinon.createSandbox();
afterEach(() => {
TestUtils.restoreStubs();
sandbox.restore();
});
describe('getPairingAuthorisations', () => {
let fetchPairingStub: sinon.SinonStub<[PubKey], Promise<void>>;
beforeEach(() => {
fetchPairingStub = sandbox
.stub(MultiDeviceProtocol, 'fetchPairingAuthorisationsIfNeeded')
.resolves();
});
it('should fetch pairing authorisations before getting authorisations from the database', async () => {
const dataStub = TestUtils.stubData(
'getPairingAuthorisationsFor'
).resolves([]);
await MultiDeviceProtocol.getPairingAuthorisations(
TestUtils.generateFakePubkey()
);
expect(fetchPairingStub.called).to.equal(true, 'Pairing is not fetched.');
expect(fetchPairingStub.calledBefore(dataStub)).to.equal(
true,
'Database result was fetched before network result'
);
});
it('should return the authorisations from the database', async () => {
const device1 = TestUtils.generateFakePubkey();
const device2 = TestUtils.generateFakePubkey();
const pairing: PairingAuthorisation = {
primaryDevicePubKey: device1.key,
secondaryDevicePubKey: device2.key,
requestSignature: new Uint8Array(1),
grantSignature: new Uint8Array(2),
};
TestUtils.stubData('getPairingAuthorisationsFor').resolves([pairing]);
const a1 = await MultiDeviceProtocol.getPairingAuthorisations(device1);
expect(a1).to.deep.equal([pairing]);
const a2 = await MultiDeviceProtocol.getPairingAuthorisations(device2);
expect(a2).to.deep.equal([pairing]);
});
});
describe('fetchPairingAuthorisations', () => {
it('should throw if lokiFileServerAPI does not exist', async () => {
TestUtils.stubWindow('lokiFileServerAPI', undefined);
expect(
MultiDeviceProtocol.fetchPairingAuthorisations(
TestUtils.generateFakePubkey()
)
).to.be.rejectedWith('lokiFileServerAPI is not initialised.');
});
it('should return the authorisations', async () => {
const networkAuth = {
primaryDevicePubKey:
'05caa6310a490415df45f8f4ad1b3655ad7a11e722257887a30cf71601d679720b',
secondaryDevicePubKey:
'051296b9588641eea268d60ad6636eecb53a95150e91c0531a00203e01a2c16a39',
requestSignature:
'+knEdlenTV+MooRqlFsZRPWW8s9pcjKwB40fY5o0GJmAi2RPZtaVGRTqgApTIn2zPBTE4GQlmPD7uxcczHDjAg==',
grantSignature:
'eKzcOWMEVetybkuiVK2u18B9en5pywohn2Hn25/VOVTMrIsKSCW4xXpqwipfqvgvi62WtUt6SA9bCEB5Ngcyiw==',
};
const stub = sinon.stub().resolves({
isPrimary: false,
authorisations: [networkAuth],
});
TestUtils.stubWindow('lokiFileServerAPI', {
getUserDeviceMapping: stub,
});
const authorisations = await MultiDeviceProtocol.fetchPairingAuthorisations(
TestUtils.generateFakePubkey()
);
expect(authorisations.length).to.equal(1);
const {
primaryDevicePubKey,
secondaryDevicePubKey,
requestSignature,
grantSignature,
} = authorisations[0];
expect(primaryDevicePubKey).to.equal(networkAuth.primaryDevicePubKey);
expect(secondaryDevicePubKey).to.equal(networkAuth.secondaryDevicePubKey);
expect(Buffer.from(requestSignature).toString('base64')).to.equal(
networkAuth.requestSignature
);
expect(grantSignature).to.not.equal(
undefined,
'Grant signature should not be undefined.'
);
// tslint:disable-next-line: no-non-null-assertion
expect(Buffer.from(grantSignature!).toString('base64')).to.equal(
networkAuth.grantSignature
);
});
});
describe('fetchPairingAuthorisationIfNeeded', () => {
let fetchPairingAuthorisationStub: sinon.SinonStub<
[PubKey],
Promise<Array<PairingAuthorisation>>
>;
let currentDevice: PubKey;
let device: PubKey;
beforeEach(() => {
MultiDeviceProtocol.resetFetchCache();
fetchPairingAuthorisationStub = sandbox
.stub(MultiDeviceProtocol, 'fetchPairingAuthorisations')
.resolves([]);
currentDevice = TestUtils.generateFakePubkey();
device = TestUtils.generateFakePubkey();
sandbox
.stub(UserUtil, 'getCurrentDevicePubKey')
.resolves(currentDevice.key);
});
it('should not fetch authorisations for our devices', async () => {
const otherDevices = TestUtils.generateFakePubKeys(2);
const authorisations = generateFakeAuthorisations(
currentDevice,
otherDevices
);
sandbox
.stub(MultiDeviceProtocol, 'getPairingAuthorisations')
.resolves(authorisations);
for (const ourDevice of [currentDevice, ...otherDevices]) {
// Ensure cache is not getting in our way
MultiDeviceProtocol.resetFetchCache();
await MultiDeviceProtocol.fetchPairingAuthorisationsIfNeeded(ourDevice);
expect(fetchPairingAuthorisationStub.called).to.equal(
false,
'Pairing should not be fetched from the server'
);
}
});
it('should fetch if it has not fetched before', async () => {
await MultiDeviceProtocol.fetchPairingAuthorisationsIfNeeded(device);
expect(fetchPairingAuthorisationStub.calledWith(device)).to.equal(
true,
'Device does not match'
);
expect(fetchPairingAuthorisationStub.called).to.equal(
true,
'Pairing should be fetched from the server'
);
});
it('should not fetch if the refresh delay has not been met', async () => {
await MultiDeviceProtocol.fetchPairingAuthorisationsIfNeeded(device);
await timeout(100);
await MultiDeviceProtocol.fetchPairingAuthorisationsIfNeeded(device);
expect(fetchPairingAuthorisationStub.callCount).to.equal(
1,
'Pairing should only be fetched once every refresh delay'
);
});
it('should fetch again if time since last fetch is more than refresh delay', async () => {
const clock = sandbox.useFakeTimers();
await MultiDeviceProtocol.fetchPairingAuthorisationsIfNeeded(device);
clock.tick(MultiDeviceProtocol.refreshDelay + 10);
await MultiDeviceProtocol.fetchPairingAuthorisationsIfNeeded(device);
expect(fetchPairingAuthorisationStub.callCount).to.equal(2);
});
it('should fetch again if something went wrong while fetching', async () => {
fetchPairingAuthorisationStub.throws(new Error('42'));
await MultiDeviceProtocol.fetchPairingAuthorisationsIfNeeded(device);
await timeout(100);
await MultiDeviceProtocol.fetchPairingAuthorisationsIfNeeded(device);
expect(fetchPairingAuthorisationStub.callCount).to.equal(2);
});
it('should fetch only once if called rapidly', async () => {
fetchPairingAuthorisationStub.callsFake(async () => {
await timeout(200);
return [];
});
void MultiDeviceProtocol.fetchPairingAuthorisationsIfNeeded(device);
await timeout(10);
void MultiDeviceProtocol.fetchPairingAuthorisationsIfNeeded(device);
await timeout(200);
expect(fetchPairingAuthorisationStub.callCount).to.equal(1);
});
it('should save the fetched authorisations', async () => {
const saveStub = sandbox
.stub(MultiDeviceProtocol, 'savePairingAuthorisation')
.resolves();
const authorisations = generateFakeAuthorisations(
device,
TestUtils.generateFakePubKeys(3)
);
fetchPairingAuthorisationStub.resolves(authorisations);
await MultiDeviceProtocol.fetchPairingAuthorisationsIfNeeded(device);
expect(saveStub.callCount).to.equal(authorisations.length);
});
});
describe('getAllDevices', () => {
it('should return all devices', async () => {
const primary = TestUtils.generateFakePubkey();
const otherDevices = TestUtils.generateFakePubKeys(2);
const authorisations = generateFakeAuthorisations(primary, otherDevices);
sandbox
.stub(MultiDeviceProtocol, 'getPairingAuthorisations')
.resolves(authorisations);
const devices = [primary, ...otherDevices];
for (const device of devices) {
const allDevices = await MultiDeviceProtocol.getAllDevices(device);
const allDevicePubKeys = allDevices.map(p => p.key);
expect(allDevicePubKeys).to.have.same.members(devices.map(d => d.key));
}
});
});
describe('getPrimaryDevice', () => {
it('should return the primary device', async () => {
const primary = TestUtils.generateFakePubkey();
const otherDevices = TestUtils.generateFakePubKeys(2);
const authorisations = generateFakeAuthorisations(primary, otherDevices);
sandbox
.stub(MultiDeviceProtocol, 'getPairingAuthorisations')
.resolves(authorisations);
const devices = [primary, ...otherDevices];
for (const device of devices) {
const actual = await MultiDeviceProtocol.getPrimaryDevice(device);
expect(actual.key).to.equal(primary.key);
}
});
});
describe('getSecondaryDevices', () => {
it('should return the secondary devices', async () => {
const primary = TestUtils.generateFakePubkey();
const otherDevices = TestUtils.generateFakePubKeys(2);
const authorisations = generateFakeAuthorisations(primary, otherDevices);
sandbox
.stub(MultiDeviceProtocol, 'getPairingAuthorisations')
.resolves(authorisations);
const devices = [primary, ...otherDevices];
for (const device of devices) {
const secondaryDevices = await MultiDeviceProtocol.getSecondaryDevices(
device
);
const pubKeys = secondaryDevices.map(p => p.key);
expect(pubKeys).to.have.same.members(otherDevices.map(d => d.key));
}
});
});
});

@ -22,7 +22,7 @@ type DataFunction = typeof DataShape;
* Note: This uses a custom sandbox.
* Please call `restoreStubs()` or `stub.restore()` to restore original functionality.
*/
export function stubData(fn: keyof DataFunction): sinon.SinonStub {
export function stubData<K extends keyof DataFunction>(fn: K): sinon.SinonStub {
return sandbox.stub(Data, fn);
}
@ -73,6 +73,11 @@ export function generateFakePubkey(): PubKey {
return new PubKey(pubkeyString);
}
export function generateFakePubKeys(amount: number): Array<PubKey> {
// tslint:disable-next-line: no-unnecessary-callback-wrapper
return new Array(amount).fill(0).map(() => generateFakePubkey());
}
export function generateChatMessage(): ChatMessage {
return new ChatMessage({
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',

Loading…
Cancel
Save