// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; import * as sinon from 'sinon'; import { noop } from 'lodash'; import { sleep } from '../../util/sleep'; import { constantTimeEqual } from '../../Crypto'; import { OurProfileKeyService } from '../../services/ourProfileKey'; describe('"our profile key" service', () => { const createFakeStorage = () => ({ get: sinon.stub(), put: sinon.stub().resolves(), remove: sinon.stub().resolves(), onready: sinon.stub().callsArg(0), }); describe('get', () => { it("fetches the key from storage if it's there", async () => { const fakeProfileKey = new Uint8Array(2); const fakeStorage = createFakeStorage(); fakeStorage.get.withArgs('profileKey').returns(fakeProfileKey); const service = new OurProfileKeyService(); service.initialize(fakeStorage); const profileKey = await service.get(); assert.isTrue( profileKey && constantTimeEqual(profileKey, fakeProfileKey) ); }); it('resolves with undefined if the key is not in storage', async () => { const service = new OurProfileKeyService(); service.initialize(createFakeStorage()); assert.isUndefined(await service.get()); }); it("doesn't grab the profile key from storage until storage is ready", async () => { let onReadyCallback = noop; const fakeStorage = { ...createFakeStorage(), get: sinon.stub().returns(new Uint8Array(2)), onready: sinon.stub().callsFake(callback => { onReadyCallback = callback; }), }; const service = new OurProfileKeyService(); service.initialize(fakeStorage); const getPromise = service.get(); // We want to make sure this isn't called even after a tick of the event loop. await sleep(1); sinon.assert.notCalled(fakeStorage.get); onReadyCallback(); await getPromise; sinon.assert.calledOnce(fakeStorage.get); }); it("doesn't grab the profile key until all blocking promises are ready", async () => { const fakeStorage = createFakeStorage(); const service = new OurProfileKeyService(); service.initialize(fakeStorage); let resolve1 = noop; service.blockGetWithPromise( new Promise(resolve => { resolve1 = resolve; }) ); let reject2 = noop; service.blockGetWithPromise( new Promise((_resolve, reject) => { reject2 = reject; }) ); let reject3 = noop; service.blockGetWithPromise( new Promise((_resolve, reject) => { reject3 = reject; }) ); const getPromise = service.get(); resolve1(); await sleep(1); sinon.assert.notCalled(fakeStorage.get); reject2(new Error('uh oh')); await sleep(1); sinon.assert.notCalled(fakeStorage.get); reject3(new Error('oh no')); await getPromise; sinon.assert.calledOnce(fakeStorage.get); }); it("if there are blocking promises, doesn't grab the profile key from storage more than once (in other words, subsequent calls piggyback)", async () => { const fakeStorage = createFakeStorage(); fakeStorage.get.returns(new Uint8Array(2)); const service = new OurProfileKeyService(); service.initialize(fakeStorage); let resolve = noop; service.blockGetWithPromise( new Promise(innerResolve => { resolve = innerResolve; }) ); const getPromises = [service.get(), service.get(), service.get()]; resolve(); const results = await Promise.all(getPromises); assert(new Set(results).size === 1, 'All results should be the same'); sinon.assert.calledOnce(fakeStorage.get); }); it('removes all of the blocking promises after waiting for them once', async () => { const fakeStorage = createFakeStorage(); const service = new OurProfileKeyService(); service.initialize(fakeStorage); let resolve = noop; service.blockGetWithPromise( new Promise(innerResolve => { resolve = innerResolve; }) ); const getPromise = service.get(); sinon.assert.notCalled(fakeStorage.get); resolve(); await getPromise; sinon.assert.calledOnce(fakeStorage.get); await service.get(); sinon.assert.calledTwice(fakeStorage.get); }); }); describe('set', () => { it('updates the key in storage', async () => { const fakeProfileKey = new Uint8Array(2); const fakeStorage = createFakeStorage(); const service = new OurProfileKeyService(); service.initialize(fakeStorage); await service.set(fakeProfileKey); sinon.assert.calledOnce(fakeStorage.put); sinon.assert.calledWith(fakeStorage.put, 'profileKey', fakeProfileKey); }); it('clears the key in storage', async () => { const fakeStorage = createFakeStorage(); const service = new OurProfileKeyService(); service.initialize(fakeStorage); await service.set(undefined); sinon.assert.calledOnce(fakeStorage.remove); sinon.assert.calledWith(fakeStorage.remove, 'profileKey'); }); }); });