From d1eaed8b32d367be5250c23074773ce8a59d51d1 Mon Sep 17 00:00:00 2001 From: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> Date: Fri, 26 Nov 2021 09:54:13 -0500 Subject: [PATCH] [MM-40266] Refactor and tests for main/allowProtocolDialog (#1875) --- src/main/allowProtocolDialog.test.js | 168 +++++++++++++++++++++++++++ src/main/allowProtocolDialog.ts | 135 ++++++++++----------- 2 files changed, 238 insertions(+), 65 deletions(-) create mode 100644 src/main/allowProtocolDialog.test.js diff --git a/src/main/allowProtocolDialog.test.js b/src/main/allowProtocolDialog.test.js new file mode 100644 index 00000000..24eaa1d9 --- /dev/null +++ b/src/main/allowProtocolDialog.test.js @@ -0,0 +1,168 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +'use strict'; + +import fs from 'fs'; + +import {shell, dialog} from 'electron'; + +import {getMainWindow} from './windows/windowManager'; + +import {AllowProtocolDialog} from './allowProtocolDialog'; + +jest.mock('fs', () => ({ + readFile: jest.fn(), + writeFile: jest.fn(), +})); + +jest.mock('path', () => ({ + resolve: () => 'path', +})); + +jest.mock('electron', () => ({ + app: { + getPath: jest.fn(), + }, + dialog: { + showMessageBox: jest.fn(), + }, + shell: { + openExternal: jest.fn(), + }, +})); + +jest.mock('electron-log', () => ({ + error: jest.fn(), +})); + +jest.mock('../../electron-builder.json', () => ({ + protocols: [{ + name: 'Mattermost', + schemes: [ + 'pone', + 'ptwo', + ], + }], +})); + +jest.mock('./Validator', () => ({ + validateAllowedProtocols: (protocols) => protocols, +})); + +jest.mock('./windows/windowManager', () => ({ + getMainWindow: jest.fn(), +})); + +describe('main/allowProtocolDialog', () => { + describe('init', () => { + it('should copy data from file when no error', () => { + fs.readFile.mockImplementation((fileName, encoding, callback) => { + callback(null, '["spotify:", "steam:", "git:"]'); + }); + + const allowProtocolDialog = new AllowProtocolDialog(); + allowProtocolDialog.init(); + + expect(allowProtocolDialog.allowedProtocols).toContain('spotify:'); + expect(allowProtocolDialog.allowedProtocols).toContain('steam:'); + expect(allowProtocolDialog.allowedProtocols).toContain('git:'); + }); + + it('should include data from electron-builder', () => { + const allowProtocolDialog = new AllowProtocolDialog(); + allowProtocolDialog.init(); + + expect(allowProtocolDialog.allowedProtocols).toContain('pone:'); + expect(allowProtocolDialog.allowedProtocols).toContain('ptwo:'); + }); + + it('should always include http and https', () => { + const allowProtocolDialog = new AllowProtocolDialog(); + allowProtocolDialog.init(); + + expect(allowProtocolDialog.allowedProtocols).toContain('http:'); + expect(allowProtocolDialog.allowedProtocols).toContain('https:'); + }); + }); + + describe('addScheme', () => { + it('should add new scheme to the list', () => { + const allowProtocolDialog = new AllowProtocolDialog(); + allowProtocolDialog.addScheme('test'); + + expect(allowProtocolDialog.allowedProtocols).toContain('test:'); + }); + + it('should not add duplicates', () => { + const allowProtocolDialog = new AllowProtocolDialog(); + allowProtocolDialog.addScheme('test'); + allowProtocolDialog.addScheme('test2'); + allowProtocolDialog.addScheme('test'); + + expect(allowProtocolDialog.allowedProtocols).toStrictEqual(['test:', 'test2:']); + }); + }); + + describe('handleDialogEvent', () => { + fs.readFile.mockImplementation((fileName, encoding, callback) => { + callback(null, '["spotify:", "steam:", "git:"]'); + }); + + let allowProtocolDialog; + beforeEach(() => { + allowProtocolDialog = new AllowProtocolDialog(); + allowProtocolDialog.init(); + }); + + it('should open protocol that is already allowed', () => { + allowProtocolDialog.handleDialogEvent('spotify:', 'spotify:album:3AQgdwMNCiN7awXch5fAaG'); + expect(shell.openExternal).toBeCalledWith('spotify:album:3AQgdwMNCiN7awXch5fAaG'); + }); + + it('should not open message box if main window is missing', () => { + getMainWindow.mockImplementation(() => null); + allowProtocolDialog.handleDialogEvent('mattermost:', 'mattermost://community.mattermost.com'); + expect(shell.openExternal).not.toBeCalled(); + expect(dialog.showMessageBox).not.toBeCalled(); + }); + + describe('main window not null', () => { + beforeEach(() => { + getMainWindow.mockImplementation(() => ({})); + }); + + it('should open the window but not save when clicking Yes', async () => { + const promise = Promise.resolve({response: 0}); + dialog.showMessageBox.mockImplementation(() => promise); + allowProtocolDialog.handleDialogEvent('mattermost:', 'mattermost://community.mattermost.com'); + await promise; + + expect(shell.openExternal).toBeCalledWith('mattermost://community.mattermost.com'); + expect(allowProtocolDialog.allowedProtocols).not.toContain('mattermost:'); + expect(fs.writeFile).not.toBeCalled(); + }); + + it('should open the window and save when clicking Yes and Save', async () => { + const promise = Promise.resolve({response: 1}); + dialog.showMessageBox.mockImplementation(() => promise); + allowProtocolDialog.handleDialogEvent('mattermost:', 'mattermost://community.mattermost.com'); + await promise; + + expect(shell.openExternal).toBeCalledWith('mattermost://community.mattermost.com'); + expect(allowProtocolDialog.allowedProtocols).toContain('mattermost:'); + expect(fs.writeFile).toBeCalled(); + }); + + it('should do nothing when clicking No', async () => { + const promise = Promise.resolve({response: 2}); + dialog.showMessageBox.mockImplementation(() => promise); + allowProtocolDialog.handleDialogEvent('mattermost:', 'mattermost://community.mattermost.com'); + await promise; + + expect(shell.openExternal).not.toBeCalled(); + expect(allowProtocolDialog.allowedProtocols).not.toContain('mattermost:'); + expect(fs.writeFile).not.toBeCalled(); + }); + }); + }); +}); diff --git a/src/main/allowProtocolDialog.ts b/src/main/allowProtocolDialog.ts index 2a51f354..abf36485 100644 --- a/src/main/allowProtocolDialog.ts +++ b/src/main/allowProtocolDialog.ts @@ -16,76 +16,81 @@ import * as Validator from './Validator'; import {getMainWindow} from './windows/windowManager'; const allowedProtocolFile = path.resolve(app.getPath('userData'), 'allowedProtocols.json'); -let allowedProtocols: string[] = []; -function addScheme(scheme: string) { - const proto = `${scheme}:`; - if (!allowedProtocols.includes(proto)) { - allowedProtocols.push(proto); +export class AllowProtocolDialog { + allowedProtocols: string[]; + + constructor() { + this.allowedProtocols = []; } -} -function init() { - fs.readFile(allowedProtocolFile, 'utf-8', (err, data) => { - if (!err) { - allowedProtocols = JSON.parse(data); - allowedProtocols = Validator.validateAllowedProtocols(allowedProtocols) || []; + init = () => { + fs.readFile(allowedProtocolFile, 'utf-8', (err, data) => { + if (!err) { + this.allowedProtocols = JSON.parse(data); + this.allowedProtocols = Validator.validateAllowedProtocols(this.allowedProtocols) || []; + } + this.addScheme('http'); + this.addScheme('https'); + protocols.forEach((protocol) => { + if (protocol.schemes && protocol.schemes.length > 0) { + protocol.schemes.forEach(this.addScheme); + } + }); + }); + } + + addScheme = (scheme: string) => { + const proto = `${scheme}:`; + if (!this.allowedProtocols.includes(proto)) { + this.allowedProtocols.push(proto); } - addScheme('http'); - addScheme('https'); - protocols.forEach((protocol) => { - if (protocol.schemes && protocol.schemes.length > 0) { - protocol.schemes.forEach(addScheme); + } + + handleDialogEvent = (protocol: string, URL: string) => { + if (this.allowedProtocols.indexOf(protocol) !== -1) { + shell.openExternal(URL); + return; + } + const mainWindow = getMainWindow(); + if (!mainWindow) { + return; + } + dialog.showMessageBox(mainWindow, { + title: 'Non http(s) protocol', + message: `${protocol} link requires an external application.`, + detail: `The requested link is ${URL} . Do you want to continue?`, + defaultId: 2, + type: 'warning', + buttons: [ + 'Yes', + `Yes (Save ${protocol} as allowed)`, + 'No', + ], + cancelId: 2, + noLink: true, + }).then(({response}) => { + switch (response) { + case 1: { + this.allowedProtocols.push(protocol); + function handleError(err: NodeJS.ErrnoException | null) { + if (err) { + log.error(err); + } + } + fs.writeFile(allowedProtocolFile, JSON.stringify(this.allowedProtocols), handleError); + shell.openExternal(URL); + break; + } + case 0: + shell.openExternal(URL); + break; + default: + break; } }); - }); + } } -function handleDialogEvent(protocol: string, URL: string) { - if (allowedProtocols.indexOf(protocol) !== -1) { - shell.openExternal(URL); - return; - } - const mainWindow = getMainWindow(); - if (!mainWindow) { - return; - } - dialog.showMessageBox(mainWindow, { - title: 'Non http(s) protocol', - message: `${protocol} link requires an external application.`, - detail: `The requested link is ${URL} . Do you want to continue?`, - defaultId: 2, - type: 'warning', - buttons: [ - 'Yes', - `Yes (Save ${protocol} as allowed)`, - 'No', - ], - cancelId: 2, - noLink: true, - }).then(({response}) => { - switch (response) { - case 1: { - allowedProtocols.push(protocol); - function handleError(err: NodeJS.ErrnoException | null) { - if (err) { - log.error(err); - } - } - fs.writeFile(allowedProtocolFile, JSON.stringify(allowedProtocols), handleError); - shell.openExternal(URL); - break; - } - case 0: - shell.openExternal(URL); - break; - default: - break; - } - }); -} - -export default { - init, - handleDialogEvent, -}; +const allowProtocolDialog = new AllowProtocolDialog(); +export default allowProtocolDialog;