diff --git a/package.json b/package.json index 71d52e53..f6cf28b3 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "check-types": "tsc" }, "jest": { + "clearMocks": true, "moduleDirectories": [ "", "node_modules" diff --git a/src/main/authManager.test.js b/src/main/authManager.test.js new file mode 100644 index 00000000..855569c6 --- /dev/null +++ b/src/main/authManager.test.js @@ -0,0 +1,283 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +'use strict'; + +import {AuthManager} from 'main/authManager'; +import WindowManager from 'main/windows/windowManager'; +import ModalManager from 'main/views/modalManager'; + +jest.mock('common/utils/url', () => { + const actualUrl = jest.requireActual('common/utils/url'); + return { + ...actualUrl.default, + getView: (url) => { + if (url.toString() === 'http://badurl.com/') { + return null; + } + return {name: 'test', url}; + }, + isTrustedURL: (url) => { + return url.toString() === 'http://trustedurl.com/'; + }, + isCustomLoginURL: (url) => { + return url.toString() === 'http://customloginurl.com/'; + }, + }; +}); + +jest.mock('electron-log', () => ({ + error: jest.fn(), +})); + +jest.mock('main/windows/windowManager', () => ({ + getMainWindow: jest.fn().mockImplementation(() => ({})), +})); + +jest.mock('main/views/modalManager', () => ({ + addModal: jest.fn(), +})); + +jest.mock('main/utils', () => ({ + getLocalPreload: (file) => file, + getLocalURLString: (file) => file, +})); + +const config = { + teams: [{ + name: 'example', + url: 'http://example.com', + order: 0, + tabs: [ + { + name: 'TAB_MESSAGING', + order: 0, + isOpen: true, + }, + { + name: 'TAB_FOCALBOARD', + order: 1, + isOpen: true, + }, + { + name: 'TAB_PLAYBOOKS', + order: 2, + isOpen: true, + }, + ], + lastActiveTab: 0, + }, { + name: 'github', + url: 'https://github.com/', + order: 1, + tabs: [ + { + name: 'TAB_MESSAGING', + order: 0, + isOpen: true, + }, + { + name: 'TAB_FOCALBOARD', + order: 1, + isOpen: true, + }, + { + name: 'TAB_PLAYBOOKS', + order: 2, + isOpen: true, + }, + ], + lastActiveTab: 0, + }], +}; + +const trustedOriginsStore = { + addPermission: jest.fn(), + checkPermission: (url) => { + return url.toString() === 'http://haspermissionurl.com/'; + }, + save: jest.fn(), +}; + +describe('main/authManager', () => { + describe('handleAppLogin', () => { + const authManager = new AuthManager(config, trustedOriginsStore); + authManager.popLoginModal = jest.fn(); + authManager.popPermissionModal = jest.fn(); + + it('should not pop any modal on null url', () => { + authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: null}, null, jest.fn()); + expect(authManager.popLoginModal).not.toBeCalled(); + expect(authManager.popPermissionModal).not.toBeCalled(); + }); + + it('should not pop any modal on null server', () => { + authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://badurl.com/'}, null, jest.fn()); + expect(authManager.popLoginModal).not.toBeCalled(); + expect(authManager.popPermissionModal).not.toBeCalled(); + }); + + it('should popLoginModal when isTrustedURL', () => { + authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://trustedurl.com/'}, null, jest.fn()); + expect(authManager.popLoginModal).toBeCalled(); + expect(authManager.popPermissionModal).not.toBeCalled(); + }); + + it('should popLoginModal when isCustomLoginURL', () => { + authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://customloginurl.com/'}, null, jest.fn()); + expect(authManager.popLoginModal).toBeCalled(); + expect(authManager.popPermissionModal).not.toBeCalled(); + }); + + it('should popLoginModal when has permission', () => { + authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://haspermissionurl.com/'}, null, jest.fn()); + expect(authManager.popLoginModal).toBeCalled(); + expect(authManager.popPermissionModal).not.toBeCalled(); + }); + + it('should popPermissionModal when anything else is true', () => { + authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://someotherurl.com/'}, null, jest.fn()); + expect(authManager.popLoginModal).not.toBeCalled(); + expect(authManager.popPermissionModal).toBeCalled(); + }); + + it('should set login callback when logging in', () => { + const callback = jest.fn(); + authManager.handleAppLogin({preventDefault: jest.fn()}, null, {url: 'http://someotherurl.com/'}, null, callback); + expect(authManager.loginCallbackMap.get('http://someotherurl.com/')).toEqual(callback); + }); + }); + + describe('popLoginModal', () => { + const authManager = new AuthManager(config, trustedOriginsStore); + + it('should not pop modal when no main window exists', () => { + WindowManager.getMainWindow.mockImplementationOnce(() => null); + authManager.popLoginModal({url: 'http://anormalurl.com'}, { + isProxy: false, + host: 'anormalurl', + }); + expect(ModalManager.addModal).not.toBeCalled(); + }); + + it('should call with prefix based on proxy setting', () => { + authManager.popLoginModal({url: 'http://anormalurl.com'}, + { + isProxy: true, + host: 'anormalurl', + }); + expect(ModalManager.addModal).toBeCalledWith( + 'proxy-anormalurl', + expect.any(String), + expect.any(String), + expect.any(Object), + expect.any(Object), + ); + + authManager.popLoginModal({url: 'http://anormalurl.com'}, + { + isProxy: false, + host: 'anormalurl', + }); + expect(ModalManager.addModal).toBeCalledWith( + 'login-http://anormalurl.com', + expect.any(String), + expect.any(String), + expect.any(Object), + expect.any(Object), + ); + }); + + it('should return login credentials when modal resolves', async () => { + authManager.handleLoginCredentialsEvent = jest.fn(); + const promise = Promise.resolve({username: 'test', password: 'password'}); + ModalManager.addModal.mockImplementationOnce(() => promise); + authManager.popLoginModal({url: 'http://anormalurl.com'}, + { + isProxy: false, + host: 'anormalurl', + }); + await promise; + expect(authManager.handleLoginCredentialsEvent).toBeCalledWith({url: 'http://anormalurl.com'}, 'test', 'password'); + }); + + it('should cancel the login event when modal rejects', async () => { + authManager.handleCancelLoginEvent = jest.fn(); + const error = new Error('oops'); + const promise = Promise.reject(error); + ModalManager.addModal.mockImplementationOnce(() => promise); + authManager.popLoginModal({url: 'http://anormalurl.com'}, + { + isProxy: false, + host: 'anormalurl', + }); + await expect(promise).rejects.toThrow(error); + expect(authManager.handleCancelLoginEvent).toBeCalledWith({url: 'http://anormalurl.com'}); + }); + }); + + describe('popPermissionModal', () => { + const authManager = new AuthManager(config, trustedOriginsStore); + + it('should not pop modal when no main window exists', () => { + WindowManager.getMainWindow.mockImplementationOnce(() => null); + authManager.popPermissionModal({url: 'http://anormalurl.com'}, { + isProxy: false, + host: 'anormalurl', + }, 'canBasicAuth'); + expect(ModalManager.addModal).not.toBeCalled(); + }); + + it('should call the login event when modal resolves', async () => { + authManager.popLoginModal = jest.fn(); + authManager.handlePermissionGranted = jest.fn(); + const promise = Promise.resolve(); + ModalManager.addModal.mockImplementationOnce(() => promise); + authManager.popPermissionModal({url: 'http://anormalurl.com'}, + { + isProxy: false, + host: 'anormalurl', + }, 'canBasicAuth'); + await promise; + expect(authManager.handlePermissionGranted).toBeCalledWith('http://anormalurl.com', 'canBasicAuth'); + expect(authManager.popLoginModal).toBeCalledWith({url: 'http://anormalurl.com'}, { + isProxy: false, + host: 'anormalurl', + }); + }); + + it('should cancel the login event when modal rejects', async () => { + authManager.handleCancelLoginEvent = jest.fn(); + const error = new Error('oops'); + const promise = Promise.reject(error); + ModalManager.addModal.mockImplementationOnce(() => promise); + authManager.popPermissionModal({url: 'http://anormalurl.com'}, + { + isProxy: false, + host: 'anormalurl', + }, 'canBasicAuth'); + await expect(promise).rejects.toThrow(error); + expect(authManager.handleCancelLoginEvent).toBeCalledWith({url: 'http://anormalurl.com'}); + }); + }); + + describe('handleLoginCredentialsEvent', () => { + const authManager = new AuthManager(config, trustedOriginsStore); + const callback = jest.fn(); + + beforeEach(() => { + authManager.loginCallbackMap.set('http://someurl.com', callback); + }); + + it('should fire callback on successful login', () => { + authManager.handleLoginCredentialsEvent({url: 'http://someurl.com'}, 'test', 'password'); + expect(callback).toBeCalledWith('test', 'password'); + expect(authManager.loginCallbackMap.get('http://someurl.com')).toBe(undefined); + }); + + it('should fail gracefully on no callback found', () => { + authManager.handleLoginCredentialsEvent({url: 'http://someotherurl.com'}, 'test', 'password'); + expect(callback).not.toBeCalled(); + expect(authManager.loginCallbackMap.get('http://someurl.com')).toBe(callback); + }); + }); +}); diff --git a/src/main/authManager.ts b/src/main/authManager.ts index 678e9afb..6d9dbf49 100644 --- a/src/main/authManager.ts +++ b/src/main/authManager.ts @@ -8,13 +8,12 @@ import {PermissionType} from 'types/trustedOrigin'; import {LoginModalData} from 'types/auth'; import {BASIC_AUTH_PERMISSION} from 'common/permissions'; - import urlUtils from 'common/utils/url'; -import * as WindowManager from './windows/windowManager'; +import * as WindowManager from 'main/windows/windowManager'; +import {addModal} from 'main/views/modalManager'; +import {getLocalURLString, getLocalPreload} from 'main/utils'; -import {addModal} from './views/modalManager'; -import {getLocalURLString, getLocalPreload} from './utils'; import TrustedOriginsStore from './trustedOrigins'; const modalPreload = getLocalPreload('modalPreload.js'); @@ -43,7 +42,10 @@ export class AuthManager { handleAppLogin = (event: Event, webContents: WebContents, request: AuthenticationResponseDetails, authInfo: AuthInfo, callback?: (username?: string, password?: string) => void) => { event.preventDefault(); - const parsedURL = new URL(request.url); + const parsedURL = urlUtils.parseURL(request.url); + if (!parsedURL) { + return; + } const server = urlUtils.getView(parsedURL, this.config.teams); if (!server) { return; diff --git a/src/main/certificateManager.test.js b/src/main/certificateManager.test.js new file mode 100644 index 00000000..507fd875 --- /dev/null +++ b/src/main/certificateManager.test.js @@ -0,0 +1,109 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +'use strict'; + +import WindowManager from 'main/windows/windowManager'; +import ModalManager from 'main/views/modalManager'; +import {CertificateManager} from 'main/certificateManager'; + +jest.mock('electron-log', () => ({ + info: jest.fn(), + error: jest.fn(), +})); + +jest.mock('main/windows/windowManager', () => ({ + getMainWindow: jest.fn().mockImplementation(() => ({})), +})); + +jest.mock('main/views/modalManager', () => ({ + addModal: jest.fn(), +})); + +jest.mock('main/utils', () => ({ + getLocalPreload: (file) => file, + getLocalURLString: (file) => file, +})); + +describe('main/certificateManager', () => { + describe('handleSelectCertificate', () => { + const certificateManager = new CertificateManager(); + certificateManager.popCertificateModal = jest.fn(); + + it('should not pop modal on no certificates', () => { + certificateManager.handleSelectCertificate({preventDefault: jest.fn()}, null, 'http://someurl.com/', [], jest.fn()); + expect(certificateManager.popCertificateModal).not.toBeCalled(); + }); + + it('should not pop modal on one certificate', () => { + certificateManager.handleSelectCertificate({preventDefault: jest.fn()}, null, 'http://someurl.com/', [{}], jest.fn()); + expect(certificateManager.popCertificateModal).not.toBeCalled(); + }); + + it('should pop modal on two certificates', () => { + certificateManager.handleSelectCertificate({preventDefault: jest.fn()}, null, 'http://someurl.com/', [{}, {}], jest.fn()); + expect(certificateManager.popCertificateModal).toBeCalled(); + }); + + it('should set callback when checking for cert', () => { + const callback = jest.fn(); + certificateManager.handleSelectCertificate({preventDefault: jest.fn()}, null, 'http://someurl.com/', [{}, {}], callback); + expect(certificateManager.certificateRequestCallbackMap.get('http://someurl.com/')).toEqual(callback); + }); + }); + + describe('popCertificateModal', () => { + const certificateManager = new CertificateManager(); + + it('should not pop modal when no main window exists', () => { + WindowManager.getMainWindow.mockImplementationOnce(() => null); + certificateManager.popCertificateModal('http://anormalurl.com', [{data: 'test 1'}, {data: 'test 2'}, {data: 'test 3'}]); + expect(ModalManager.addModal).not.toBeCalled(); + }); + + it('should return the chosen certificate when modal resolves', async () => { + certificateManager.handleSelectedCertificate = jest.fn(); + const promise = Promise.resolve({cert: {data: 'test 2'}}); + ModalManager.addModal.mockImplementationOnce(() => promise); + certificateManager.popCertificateModal('http://anormalurl.com', [{data: 'test 1'}, {data: 'test 2'}, {data: 'test 3'}]); + await promise; + expect(certificateManager.handleSelectedCertificate).toBeCalledWith('http://anormalurl.com', {data: 'test 2'}); + }); + + it('should call with no cert when modal rejects', async () => { + certificateManager.handleSelectCertificate = jest.fn(); + const error = new Error('oops'); + const promise = Promise.reject(error); + ModalManager.addModal.mockImplementationOnce(() => promise); + certificateManager.popCertificateModal('http://anormalurl.com', [{data: 'test 1'}, {data: 'test 2'}, {data: 'test 3'}]); + await expect(promise).rejects.toThrow(error); + expect(certificateManager.handleSelectedCertificate).toBeCalledWith('http://anormalurl.com'); + }); + }); + + describe('handleSelectedCertificate', () => { + const certificateManager = new CertificateManager(); + const callback = jest.fn(); + + beforeEach(() => { + certificateManager.certificateRequestCallbackMap.set('http://someurl.com', callback); + }); + + it('should fire callback on successful selection', () => { + certificateManager.handleSelectedCertificate('http://someurl.com', {data: 'test'}); + expect(callback).toBeCalledWith({data: 'test'}); + expect(certificateManager.certificateRequestCallbackMap.get('http://someurl.com')).toBe(undefined); + }); + + it('should fail gracefully on no callback found', () => { + certificateManager.handleSelectedCertificate('http://someotherurl.com', {data: 'test'}); + expect(callback).not.toBeCalled(); + expect(certificateManager.certificateRequestCallbackMap.get('http://someurl.com')).toBe(callback); + }); + + it('should fail gracefully on no certificate', () => { + certificateManager.handleSelectedCertificate('http://someurl.com'); + expect(callback).not.toBeCalled(); + expect(certificateManager.certificateRequestCallbackMap.get('http://someurl.com')).toBe(undefined); + }); + }); +}); diff --git a/src/main/certificateManager.ts b/src/main/certificateManager.ts index fdae37d8..4d8cab1d 100644 --- a/src/main/certificateManager.ts +++ b/src/main/certificateManager.ts @@ -70,5 +70,6 @@ export class CertificateManager { log.error(`There was a problem using the selected certificate: ${e}`); } } + this.certificateRequestCallbackMap.delete(server); } }