// Copyright 2018-2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only const fs = require('fs'); const fse = require('fs-extra'); const path = require('path'); const tmp = require('tmp'); const { assert } = require('chai'); const { app } = require('electron'); const Attachments = require('../../app/attachments'); const { stringToArrayBuffer, } = require('../../js/modules/string_to_array_buffer'); const PREFIX_LENGTH = 2; const NUM_SEPARATORS = 1; const NAME_LENGTH = 64; const PATH_LENGTH = PREFIX_LENGTH + NUM_SEPARATORS + NAME_LENGTH; describe('Attachments', () => { describe('createWriterForNew', () => { let tempRootDirectory = null; before(() => { tempRootDirectory = tmp.dirSync().name; }); after(async () => { await fse.remove(tempRootDirectory); }); it('should write file to disk and return path', async () => { const input = stringToArrayBuffer('test string'); const tempDirectory = path.join( tempRootDirectory, 'Attachments_createWriterForNew' ); const outputPath = await Attachments.createWriterForNew(tempDirectory)( input ); const output = await fse.readFile(path.join(tempDirectory, outputPath)); assert.lengthOf(outputPath, PATH_LENGTH); const inputBuffer = Buffer.from(input); assert.deepEqual(inputBuffer, output); }); }); describe('createWriterForExisting', () => { let tempRootDirectory = null; before(() => { tempRootDirectory = tmp.dirSync().name; }); after(async () => { await fse.remove(tempRootDirectory); }); it('should write file to disk on given path and return path', async () => { const input = stringToArrayBuffer('test string'); const tempDirectory = path.join( tempRootDirectory, 'Attachments_createWriterForExisting' ); const relativePath = Attachments.getRelativePath( Attachments.createName() ); const attachment = { path: relativePath, data: input, }; const outputPath = await Attachments.createWriterForExisting( tempDirectory )(attachment); const output = await fse.readFile(path.join(tempDirectory, outputPath)); assert.equal(outputPath, relativePath); const inputBuffer = Buffer.from(input); assert.deepEqual(inputBuffer, output); }); it('throws if relative path goes higher than root', async () => { const input = stringToArrayBuffer('test string'); const tempDirectory = path.join( tempRootDirectory, 'Attachments_createWriterForExisting' ); const relativePath = '../../parent'; const attachment = { path: relativePath, data: input, }; try { await Attachments.createWriterForExisting(tempDirectory)(attachment); } catch (error) { assert.strictEqual(error.message, 'Invalid relative path'); return; } throw new Error('Expected an error'); }); }); describe('createReader', () => { let tempRootDirectory = null; before(() => { tempRootDirectory = tmp.dirSync().name; }); after(async () => { await fse.remove(tempRootDirectory); }); it('should read file from disk', async () => { const tempDirectory = path.join( tempRootDirectory, 'Attachments_createReader' ); const relativePath = Attachments.getRelativePath( Attachments.createName() ); const fullPath = path.join(tempDirectory, relativePath); const input = stringToArrayBuffer('test string'); const inputBuffer = Buffer.from(input); await fse.ensureFile(fullPath); await fse.writeFile(fullPath, inputBuffer); const output = await Attachments.createReader(tempDirectory)( relativePath ); assert.deepEqual(input, output); }); it('throws if relative path goes higher than root', async () => { const tempDirectory = path.join( tempRootDirectory, 'Attachments_createReader' ); const relativePath = '../../parent'; try { await Attachments.createReader(tempDirectory)(relativePath); } catch (error) { assert.strictEqual(error.message, 'Invalid relative path'); return; } throw new Error('Expected an error'); }); }); describe('copyIntoAttachmentsDirectory', () => { // These tests use the `userData` path. In `electron-mocha`, these are temporary // directories; no need to be concerned about messing with the "real" directory. before(function thisNeeded() { this.filesToRemove = []; this.getFakeAttachmentsDirectory = () => { const result = path.join( app.getPath('userData'), `fake-attachments-${Date.now()}-${Math.random() .toString() .substring(2)}` ); this.filesToRemove.push(result); return result; }; this.getTempFile = () => { const result = tmp.fileSync().name; this.filesToRemove.push(result); return result; }; }); after(async function thisNeeded() { await Promise.all( this.filesToRemove.map(toRemove => fse.remove(toRemove)) ); }); it('throws if passed a non-string', () => { assert.throws(() => { Attachments.copyIntoAttachmentsDirectory(1234); }, TypeError); assert.throws(() => { Attachments.copyIntoAttachmentsDirectory(null); }, TypeError); }); it('returns a function that rejects if the source path is not a string', async function thisNeeded() { const copier = Attachments.copyIntoAttachmentsDirectory( await this.getFakeAttachmentsDirectory() ); return copier(123) .then(() => { assert.fail('This should never be run'); }) .catch(err => { assert.instanceOf(err, TypeError); }); }); it('returns a function that rejects if the source path is not in the user config directory', async function thisNeeded() { const copier = Attachments.copyIntoAttachmentsDirectory( await this.getFakeAttachmentsDirectory() ); return copier(this.getTempFile()) .then(() => { assert.fail('This should never be run'); }) .catch(err => { assert.instanceOf(err, Error); assert.strictEqual( err.message, "'sourcePath' must be relative to the user config directory" ); }); }); it('returns a function that copies the source path into the attachments directory', async function thisNeeded() { const attachmentsPath = await this.getFakeAttachmentsDirectory(); const someOtherPath = path.join(app.getPath('userData'), 'somethingElse'); await fse.outputFile(someOtherPath, 'hello world'); this.filesToRemove.push(someOtherPath); const copier = Attachments.copyIntoAttachmentsDirectory(attachmentsPath); const relativePath = await copier(someOtherPath); const absolutePath = path.join(attachmentsPath, relativePath); assert.notEqual(someOtherPath, absolutePath); assert.strictEqual( await fs.promises.readFile(absolutePath, 'utf8'), 'hello world' ); }); }); describe('createDeleter', () => { let tempRootDirectory = null; before(() => { tempRootDirectory = tmp.dirSync().name; }); after(async () => { await fse.remove(tempRootDirectory); }); it('should delete file from disk', async () => { const tempDirectory = path.join( tempRootDirectory, 'Attachments_createDeleter' ); const relativePath = Attachments.getRelativePath( Attachments.createName() ); const fullPath = path.join(tempDirectory, relativePath); const input = stringToArrayBuffer('test string'); const inputBuffer = Buffer.from(input); await fse.ensureFile(fullPath); await fse.writeFile(fullPath, inputBuffer); await Attachments.createDeleter(tempDirectory)(relativePath); const existsFile = await fse.exists(fullPath); assert.isFalse(existsFile); }); it('throws if relative path goes higher than root', async () => { const tempDirectory = path.join( tempRootDirectory, 'Attachments_createDeleter' ); const relativePath = '../../parent'; try { await Attachments.createDeleter(tempDirectory)(relativePath); } catch (error) { assert.strictEqual(error.message, 'Invalid relative path'); return; } throw new Error('Expected an error'); }); }); describe('createName', () => { it('should return random file name with correct length', () => { assert.lengthOf(Attachments.createName(), NAME_LENGTH); }); }); describe('getRelativePath', () => { it('should return correct path', () => { const name = '608ce3bc536edbf7637a6aeb6040bdfec49349140c0dd43e97c7ce263b15ff7e'; assert.lengthOf(Attachments.getRelativePath(name), PATH_LENGTH); }); }); describe('createAbsolutePathGetter', () => { const isWindows = process.platform === 'win32'; it('combines root and relative path', () => { const root = isWindows ? 'C:\\temp' : '/tmp'; const relative = 'ab/abcdef'; const pathGetter = Attachments.createAbsolutePathGetter(root); const absolutePath = pathGetter(relative); assert.strictEqual( absolutePath, isWindows ? 'C:\\temp\\ab\\abcdef' : '/tmp/ab/abcdef' ); }); it('throws if relative path goes higher than root', () => { const root = isWindows ? 'C:\\temp' : 'tmp'; const relative = '../../ab/abcdef'; const pathGetter = Attachments.createAbsolutePathGetter(root); try { pathGetter(relative); } catch (error) { assert.strictEqual(error.message, 'Invalid relative path'); return; } throw new Error('Expected an error'); }); }); });