From e5c54cc45e347ab2b076419fe66346352b8ce169 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 19 Apr 2022 13:27:25 +1000 Subject: [PATCH] added some test for DecryptingAttachmentManager --- .../crypto/DecryptedAttachmentsManager.ts | 26 +++- .../decryptedAttachmentsManager_test.ts | 140 ++++++++++++++++++ ts/webworker/workers/util_worker_interface.ts | 6 +- 3 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 ts/test/session/unit/decrypted_attachments/decryptedAttachmentsManager_test.ts diff --git a/ts/session/crypto/DecryptedAttachmentsManager.ts b/ts/session/crypto/DecryptedAttachmentsManager.ts index 11a1c8e65..82bc9e6be 100644 --- a/ts/session/crypto/DecryptedAttachmentsManager.ts +++ b/ts/session/crypto/DecryptedAttachmentsManager.ts @@ -12,11 +12,11 @@ import { makeObjectUrl, urlToBlob } from '../../types/attachments/VisualAttachme import { getAttachmentPath } from '../../types/MessageAttachment'; import { decryptAttachmentBufferRenderer } from '../../util/local_attachments_encrypter'; -const urlToDecryptedBlobMap = new Map< +export const urlToDecryptedBlobMap = new Map< string, { decrypted: string; lastAccessTimestamp: number; forceRetain: boolean } >(); -const urlToDecryptingPromise = new Map>(); +export const urlToDecryptingPromise = new Map>(); export const cleanUpOldDecryptedMedias = () => { const currentTimestamp = Date.now(); @@ -47,6 +47,14 @@ export const cleanUpOldDecryptedMedias = () => { ); }; +export const getLocalAttachmentPath = () => { + return getAttachmentPath(); +}; + +export const readFileContent = async (url: string) => { + return fse.readFile(url); +}; + export const getDecryptedMediaUrl = async ( url: string, contentType: string, @@ -57,7 +65,7 @@ export const getDecryptedMediaUrl = async ( } if (url.startsWith('blob:')) { return url; - } else if (getAttachmentPath() && url.startsWith(getAttachmentPath())) { + } else if (exports.getLocalAttachmentPath && url.startsWith(exports.getLocalAttachmentPath())) { // this is a file encoded by session on our current attachments path. // we consider the file is encrypted. // if it's not, the hook caller has to fallback to setting the img src as an url to the file instead and load it @@ -84,7 +92,7 @@ export const getDecryptedMediaUrl = async ( new Promise(async resolve => { window.log.info('about to read and decrypt file :', url); try { - const encryptedFileContent = await fse.readFile(url); + const encryptedFileContent = await readFileContent(url); const decryptedContent = await decryptAttachmentBufferRenderer( encryptedFileContent.buffer ); @@ -137,7 +145,7 @@ export const getAlreadyDecryptedMediaUrl = (url: string): string | null => { } if (url.startsWith('blob:')) { return url; - } else if (getAttachmentPath() && url.startsWith(getAttachmentPath())) { + } else if (exports.getLocalAttachmentPath() && url.startsWith(exports.getLocalAttachmentPath())) { if (urlToDecryptedBlobMap.has(url)) { const existingObjUrl = urlToDecryptedBlobMap.get(url)?.decrypted as string; return existingObjUrl; @@ -150,3 +158,11 @@ export const getDecryptedBlob = async (url: string, contentType: string): Promis const decryptedUrl = await getDecryptedMediaUrl(url, contentType, false); return urlToBlob(decryptedUrl); }; + +/** + * This function should only be used for testing purpose + */ +export const resetDecryptedUrlForTesting = () => { + urlToDecryptedBlobMap.clear(); + urlToDecryptingPromise.clear(); +}; diff --git a/ts/test/session/unit/decrypted_attachments/decryptedAttachmentsManager_test.ts b/ts/test/session/unit/decrypted_attachments/decryptedAttachmentsManager_test.ts new file mode 100644 index 000000000..28e30eb1e --- /dev/null +++ b/ts/test/session/unit/decrypted_attachments/decryptedAttachmentsManager_test.ts @@ -0,0 +1,140 @@ +import { expect } from 'chai'; +import { beforeEach } from 'mocha'; +import Sinon from 'sinon'; +import * as DecryptedAttachmentsManager from '../../../../session/crypto/DecryptedAttachmentsManager'; +import { TestUtils } from '../../../test-utils'; + +describe('DecryptedAttachmentsManager', () => { + // tslint:disable-next-line: no-empty + beforeEach(() => { + DecryptedAttachmentsManager.resetDecryptedUrlForTesting(); + TestUtils.stubWindowLog(); + Sinon.stub(DecryptedAttachmentsManager, 'getLocalAttachmentPath').returns('/local/attachment'); + }); + + afterEach(() => { + Sinon.restore(); + }); + + describe('getAlreadyDecryptedMediaUrl', () => { + describe('invalid url', () => { + it('url is null', () => { + expect(DecryptedAttachmentsManager.getAlreadyDecryptedMediaUrl(null as any)).to.be.null; + }); + + it('url is undefined', () => { + expect(DecryptedAttachmentsManager.getAlreadyDecryptedMediaUrl(undefined as any)).to.be + .null; + }); + + it('url is empty string', () => { + expect(DecryptedAttachmentsManager.getAlreadyDecryptedMediaUrl('')).to.be.null; + }); + + it('url starts with something not being the attachment path', () => { + expect(DecryptedAttachmentsManager.getAlreadyDecryptedMediaUrl('/local/notvalid')).to.be + .null; + }); + }); + it('url starts with "blob:" => returns the already decrypted url right away', () => { + expect(DecryptedAttachmentsManager.getAlreadyDecryptedMediaUrl('blob:whatever')).to.be.eq( + 'blob:whatever' + ); + }); + + describe('url starts with attachmentPath', () => { + let readFileContent: Sinon.SinonStub; + let getItemById: Sinon.SinonStub; + let decryptAttachmentBufferNode: Sinon.SinonStub; + + beforeEach(() => { + readFileContent = Sinon.stub(DecryptedAttachmentsManager, 'readFileContent').resolves( + Buffer.from(new String('this is a test')) + ); + getItemById = TestUtils.stubDataItem('getItemById') + .withArgs('local_attachment_encrypted_key') + .callsFake(async () => { + return { value: 'dfdf' }; + }); + + decryptAttachmentBufferNode = TestUtils.stubUtilWorker( + 'decryptAttachmentBufferNode', + new Uint8Array(5) + ); + TestUtils.stubCreateObjectUrl(); + }); + + it('url starts with attachment path but is not already decrypted', () => { + expect( + DecryptedAttachmentsManager.getAlreadyDecryptedMediaUrl('/local/attachment/attachment1') + ).to.be.eq(null); + }); + + it('url starts with attachment path but is not already decrypted', async () => { + expect( + DecryptedAttachmentsManager.getAlreadyDecryptedMediaUrl('/local/attachment/attachment1') + ).to.be.eq(null); + + expect(readFileContent.callCount).to.be.eq(0); + expect(decryptAttachmentBufferNode.callCount).to.be.eq(0); + expect(getItemById.callCount).to.be.eq(0); + + const resolved = await DecryptedAttachmentsManager.getDecryptedMediaUrl( + '/local/attachment/attachment1', + 'image/jpeg', + false + ); + + expect(readFileContent.callCount).to.be.eq(1); + expect(decryptAttachmentBufferNode.callCount).to.be.eq(1); + expect(getItemById.callCount).to.be.eq(1); + + const now = `${Date.now()}`; + expect(resolved).to.be.not.empty; + expect(resolved.startsWith(now.slice(0, 9))).to.be.true; + }); + + it('url starts with attachment path and is already decrypted', async () => { + expect( + DecryptedAttachmentsManager.getAlreadyDecryptedMediaUrl('/local/attachment/attachment1') + ).to.be.eq(null); + + expect(readFileContent.callCount).to.be.eq(0); + expect(decryptAttachmentBufferNode.callCount).to.be.eq(0); + expect(getItemById.callCount).to.be.eq(0); + + const resolved = await DecryptedAttachmentsManager.getDecryptedMediaUrl( + '/local/attachment/attachment1', + 'image/jpeg', + false + ); + + expect(readFileContent.callCount).to.be.eq(1); + expect(decryptAttachmentBufferNode.callCount).to.be.eq(1); + expect(getItemById.callCount).to.be.eq(1); + + const now = `${Date.now()}`; + expect(resolved).to.be.not.empty; + expect(resolved.startsWith(now.slice(0, 9))).to.be.true; + + const resolved2 = await DecryptedAttachmentsManager.getDecryptedMediaUrl( + '/local/attachment/attachment1', + 'image/jpeg', + false + ); + + // should not try to decrypt nor read from file again + expect(readFileContent.callCount).to.be.eq(1); + expect(decryptAttachmentBufferNode.callCount).to.be.eq(1); + expect(getItemById.callCount).to.be.eq(1); + + const now2 = `${Date.now()}`; + expect(resolved2).to.be.not.empty; + expect(resolved2.startsWith(now2.slice(0, 9))).to.be.true; + }); + }); + }); + + it.skip('cleanUpOldDecryptedMedias', () => {}); + it.skip('getDecryptedBlob', () => {}); +}); diff --git a/ts/webworker/workers/util_worker_interface.ts b/ts/webworker/workers/util_worker_interface.ts index e29bc2230..b4adbe1e2 100644 --- a/ts/webworker/workers/util_worker_interface.ts +++ b/ts/webworker/workers/util_worker_interface.ts @@ -4,7 +4,7 @@ import { ipcRenderer } from 'electron'; let utilWorkerInterface: WorkerInterface | undefined; -export const callUtilsWorker = async (fnName: string, ...args: any): Promise => { +export const internalCallUtilsWorker = async (fnName: string, ...args: any): Promise => { if (!utilWorkerInterface) { const apDataPath = await ipcRenderer.invoke('get-data-path'); const utilWorkerPath = join(apDataPath, 'ts', 'webworker', 'workers', 'util.worker.js'); @@ -12,3 +12,7 @@ export const callUtilsWorker = async (fnName: string, ...args: any): Promise => { + return internalCallUtilsWorker(fnName, ...args); +};