diff --git a/features/keychain/module/__tests__/memoized-keychain.test.js b/features/keychain/module/__tests__/memoized-keychain.test.js new file mode 100644 index 00000000..024ef6e4 --- /dev/null +++ b/features/keychain/module/__tests__/memoized-keychain.test.js @@ -0,0 +1,98 @@ +import KeyIdentifier from '@exodus/key-identifier' +import BJSON from 'buffer-json' +import stableStringify from 'json-stable-stringify' + +import memoizedKeychainDefinition, { CACHE_KEY } from '../memoized-keychain' +import { Keychain } from '../keychain' + +const tick = () => new Promise((resolve) => setTimeout(resolve, 0)) + +describe('MemoizedKeychain', () => { + const keyId = new KeyIdentifier({ + assetName: 'ethereum', + derivationAlgorithm: 'BIP32', + derivationPath: "m/44'/60'/0'/0/1", + }) + + const cachedKey = { + xpub: 'cached-xpub', + publicKey: 'cached-compressed-public-key', + } + const retrievedKey = { + xpub: 'retrieved-xpub', + publicKey: 'retrieved-compressed-public-key', + privateKey: 'retrieved-compressed-public-key', + } + + const setup = async ({ prefilledCache = true } = {}) => { + jest.spyOn(Keychain.prototype, 'exportKey').mockResolvedValue(retrievedKey) + + const storage = { + get: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + } + + storage.get.mockResolvedValue( + BJSON.stringify( + prefilledCache + ? { + [stableStringify(keyId)]: cachedKey, + } + : {} + ) + ) + + const keychain = memoizedKeychainDefinition.factory({ storage }) + await tick() + + return { + keychain, + storage, + } + } + + it('should load storage into memory when initialized', async () => { + const { keychain } = await setup() + + await expect(keychain.exportKey(keyId)).resolves.toEqual(cachedKey) + }) + + it('should get cached value when available', async () => { + const { keychain } = await setup() + + await expect(keychain.exportKey(keyId)).resolves.toEqual(cachedKey) + }) + + it('should avoid cache when requesting private key', async () => { + const { keychain } = await setup() + + await expect(keychain.exportKey(keyId, { exportPrivate: true })).resolves.toEqual(retrievedKey) + }) + + it('should only cache public key', async () => { + const { keychain, storage } = await setup({ prefilledCache: false }) + + await expect(keychain.exportKey(keyId)).resolves.toEqual(retrievedKey) + expect(storage.set).toHaveBeenCalledWith( + CACHE_KEY, + BJSON.stringify({ + [stableStringify(keyId)]: { + xpub: retrievedKey.xpub, + publicKey: retrievedKey.publicKey, + }, + }) + ) + }) + + it('should properly clear all caches', async () => { + const { keychain, storage } = await setup() + await expect(keychain.exportKey(keyId)).resolves.toEqual(cachedKey) + + await keychain.clear() + + const key = await keychain.exportKey(keyId) + expect(key).toEqual(retrievedKey) + expect(storage.delete).toHaveBeenCalledWith(CACHE_KEY) + }) +}) diff --git a/features/keychain/module/memoized-keychain.js b/features/keychain/module/memoized-keychain.js index 409f7b21..cc896ccb 100644 --- a/features/keychain/module/memoized-keychain.js +++ b/features/keychain/module/memoized-keychain.js @@ -5,7 +5,7 @@ import { Keychain } from './keychain' const keyIdToCacheKey = stableStringify -const CACHE_KEY = 'data' +export const CACHE_KEY = 'data' const getPublicKeyData = ({ xpub, publicKey }) => ({ xpub, publicKey }) @@ -52,6 +52,7 @@ class MemoizedKeychain extends Keychain { clear = async () => { await super.clear() + this.#publicKeys = Object.create(null) await this.#storage.delete(CACHE_KEY) } }